From 04a3c41bedb3dfdc8625323914b8942991586a96 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Mon, 25 May 2026 16:27:32 -0700 Subject: [PATCH] Fall back to ffmpeg when Go's JPEG decoder rejects a source Go's stdlib image/jpeg refuses some valid-but-rare features such as 4:1:1 luma/chroma subsampling (ANN's CDN serves these). When the in-process decode fails, route the bytes through ffmpeg the same way we already do for video sources. --- internal/web/thumbs.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/internal/web/thumbs.go b/internal/web/thumbs.go index a4573c7..0655202 100644 --- a/internal/web/thumbs.go +++ b/internal/web/thumbs.go @@ -178,7 +178,7 @@ func buildAvifThumb(src, outPath string) error { } if urlIsVideo || isVideoContentType(resp.Header.Get("Content-Type")) { - return buildAvifFromVideoFrame(body, outPath) + return buildAvifFromFFmpeg(body, outPath) } if isAvif(body) { @@ -195,6 +195,13 @@ func buildAvifThumb(src, outPath string) error { } src2, _, err := image.Decode(bytes.NewReader(body)) if err != nil { + // Go's stdlib JPEG decoder rejects some valid-but-rare features + // (e.g. 4:1:1 luma/chroma subsampling served by ANN's CDN). Hand + // the bytes to ffmpeg as a permissive fallback — it produces the + // same shape of output as the video path. + if ffErr := buildAvifFromFFmpeg(body, outPath); ffErr == nil { + return nil + } return fmt.Errorf("decode: %w", err) } b := src2.Bounds() @@ -265,12 +272,13 @@ func isVideoContentType(ct string) bool { return strings.HasPrefix(strings.ToLower(strings.TrimSpace(ct)), "video/") } -// buildAvifFromVideoFrame writes the downloaded video to a temp file, asks +// buildAvifFromFFmpeg writes the downloaded bytes to a temp file, asks // ffmpeg to extract a single resized frame as PNG, then hands it to avifenc. -// ffmpeg is optional — if it's missing we return an error and the caller -// falls back to redirecting to the source URL, just like any other build -// failure. -func buildAvifFromVideoFrame(body []byte, outPath string) error { +// Used both for video sources and as a permissive fallback for images whose +// JPEG features Go's stdlib decoder rejects. ffmpeg is optional — if it's +// missing we return an error and the caller falls back to redirecting to +// the source URL, just like any other build failure. +func buildAvifFromFFmpeg(body []byte, outPath string) error { if _, err := exec.LookPath("ffmpeg"); err != nil { return fmt.Errorf("ffmpeg not available: %w", err) }