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.
This commit is contained in:
prosolis
2026-05-25 16:27:32 -07:00
parent b3002c8e12
commit 04a3c41bed

View File

@@ -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)
}