fix(artwork): preserve animation for square thumbnails with animated images

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan
2026-04-01 08:38:29 -04:00
parent c5bb920b88
commit 4030bfe06f
2 changed files with 36 additions and 32 deletions
+1 -3
View File
@@ -98,8 +98,7 @@ func (a *resizedArtworkReader) resizeImage(ctx context.Context, reader io.Reader
return nil, 0, fmt.Errorf("reading image data: %w", err) return nil, 0, fmt.Errorf("reading image data: %w", err)
} }
// Preserve animation for animated images (skip for square thumbnails) // Preserve animation for animated images
if !a.square {
if isAnimatedGIF(data) { if isAnimatedGIF(data) {
if a.a.ffmpeg.IsAvailable() { if a.a.ffmpeg.IsAvailable() {
// Animated GIF: convert to animated WebP via ffmpeg (with optional resize) // Animated GIF: convert to animated WebP via ffmpeg (with optional resize)
@@ -113,7 +112,6 @@ func (a *resizedArtworkReader) resizeImage(ctx context.Context, reader io.Reader
// Animated WebP/APNG: return original as-is (ffmpeg can't re-encode these) // Animated WebP/APNG: return original as-is (ffmpeg can't re-encode these)
return bytes.NewReader(data), 0, nil return bytes.NewReader(data), 0, nil
} }
}
return resizeStaticImage(data, a.size, a.square) return resizeStaticImage(data, a.size, a.square)
} }
+24 -18
View File
@@ -54,17 +54,17 @@ var _ = Describe("resizeImage", func() {
Expect(len(output)).To(BeNumerically(">", 0)) Expect(len(output)).To(BeNumerically(">", 0))
}) })
It("skips animation for square thumbnails even with animated GIF", func() { It("preserves animation for square thumbnails with animated GIF", func() {
r.square = true r.square = true
data := createAnimatedGIF(3) data := createAnimatedGIF(3)
result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data))
// Should fall through to static resize (not ffmpeg conversion) Expect(err).ToNot(HaveOccurred())
// The minimal test GIF may or may not resize successfully, Expect(result).ToNot(BeNil())
// but ffmpeg should NOT have been called for animated conversion
_ = result // Should have been processed by ffmpeg (mock returns input data)
_ = err output, err := io.ReadAll(result)
// Verify by checking the mock wasn't used for animated conversion: Expect(err).ToNot(HaveOccurred())
// If ffmpeg was called, it would return mock data, not static resize result Expect(output).To(Equal(data))
}) })
}) })
@@ -81,13 +81,17 @@ var _ = Describe("resizeImage", func() {
Expect(output).To(Equal(data)) Expect(output).To(Equal(data))
}) })
It("does not passthrough animated WebP for square thumbnails", func() { It("preserves animated WebP for square thumbnails", func() {
r.square = true r.square = true
data := createAnimatedWebPBytes() data := createAnimatedWebPBytes()
// Should fall through to static resize, which will fail on fake WebP data result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data))
_, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) Expect(err).ToNot(HaveOccurred())
// Static decode will fail on our minimal test WebP bytes (not a real image) Expect(result).ToNot(BeNil())
Expect(err).To(HaveOccurred())
// Should return original data unchanged
output, err := io.ReadAll(result)
Expect(err).ToNot(HaveOccurred())
Expect(output).To(Equal(data))
}) })
}) })
@@ -104,15 +108,17 @@ var _ = Describe("resizeImage", func() {
Expect(output).To(Equal(data)) Expect(output).To(Equal(data))
}) })
It("does not passthrough animated PNG for square thumbnails", func() { It("preserves animated PNG for square thumbnails", func() {
r.square = true r.square = true
data := createAPNGBytes() data := createAPNGBytes()
// Should fall through to static resize
result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data))
// Static PNG decode should succeed on our APNG (it's a valid PNG) Expect(err).ToNot(HaveOccurred())
if err == nil {
Expect(result).ToNot(BeNil()) Expect(result).ToNot(BeNil())
}
// Should return original data unchanged
output, err := io.ReadAll(result)
Expect(err).ToNot(HaveOccurred())
Expect(output).To(Equal(data))
}) })
}) })