feat(artwork): preserve animated image artwork during resize (#5184)

* feat(artwork): preserve animated image artwork during resize

Detect animated GIFs, WebPs, and APNGs via lightweight byte scanning
and preserve their animation when serving resized artwork. Animated GIFs
are converted to animated WebP via ffmpeg with optional downscaling;
animated WebP/APNG are returned as-is since ffmpeg cannot re-encode them.

Adds ConvertAnimatedImage to the FFmpeg interface for piping stdin data
through ffmpeg with animated WebP output.

* fix(artwork): address code review feedback for animated artwork

Fix ReadCloser leak where ffmpeg pipe's Close was discarded by
io.NopCloser wrapping — now preserves ReadCloser semantics when the
resized reader already supports Close. Use uint64 for PNG chunk position
to prevent potential overflow on 32-bit platforms. Add integration tests
for the animation branching logic in resizeImage.
This commit is contained in:
Deluan Quintão 2026-03-13 18:11:12 -04:00 committed by GitHub
parent 4ddb0774ec
commit a50b2a1e72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 532 additions and 10 deletions

View file

@ -320,6 +320,10 @@ func (n noopFFmpeg) ProbeAudioStream(context.Context, string) (*ffmpeg.AudioProb
return nil, errors.New("noop ffmpeg: probe not supported")
}
func (n noopFFmpeg) ConvertAnimatedImage(context.Context, io.Reader, int, int) (io.ReadCloser, error) {
return nil, errors.New("noop ffmpeg: convert animated image not supported")
}
func (n noopFFmpeg) CmdPath() (string, error) { return "", nil }
func (n noopFFmpeg) IsAvailable() bool { return false }
func (n noopFFmpeg) Version() string { return "noop" }