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
8 changed files with 532 additions and 10 deletions
+12
View File
@@ -1,6 +1,7 @@
package tests
import (
"bytes"
"context"
"io"
"strings"
@@ -40,6 +41,17 @@ func (ff *MockFFmpeg) ExtractImage(context.Context, string) (io.ReadCloser, erro
return ff, nil
}
func (ff *MockFFmpeg) ConvertAnimatedImage(_ context.Context, reader io.Reader, _ int, _ int) (io.ReadCloser, error) {
if ff.Error != nil {
return nil, ff.Error
}
data, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return io.NopCloser(bytes.NewReader(data)), nil
}
func (ff *MockFFmpeg) Probe(context.Context, []string) (string, error) {
if ff.Error != nil {
return "", ff.Error