fix(artwork): preserve animation for square thumbnails with animated images
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
@@ -98,21 +98,19 @@ 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)
|
r, err := a.a.ffmpeg.ConvertAnimatedImage(ctx, bytes.NewReader(data), a.size, conf.Server.CoverArtQuality)
|
||||||
r, err := a.a.ffmpeg.ConvertAnimatedImage(ctx, bytes.NewReader(data), a.size, conf.Server.CoverArtQuality)
|
if err == nil {
|
||||||
if err == nil {
|
return r, 0, nil
|
||||||
return r, 0, nil
|
|
||||||
}
|
|
||||||
log.Warn(ctx, "Could not convert animated GIF, falling back to static", err)
|
|
||||||
}
|
}
|
||||||
} else if isAnimatedWebP(data) || isAnimatedPNG(data) {
|
log.Warn(ctx, "Could not convert animated GIF, falling back to static", err)
|
||||||
// Animated WebP/APNG: return original as-is (ffmpeg can't re-encode these)
|
|
||||||
return bytes.NewReader(data), 0, nil
|
|
||||||
}
|
}
|
||||||
|
} else if isAnimatedWebP(data) || isAnimatedPNG(data) {
|
||||||
|
// Animated WebP/APNG: return original as-is (ffmpeg can't re-encode these)
|
||||||
|
return bytes.NewReader(data), 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return resizeStaticImage(data, a.size, a.square)
|
return resizeStaticImage(data, a.size, a.square)
|
||||||
|
|||||||
@@ -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))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user