162 lines
4.4 KiB
Go
162 lines
4.4 KiB
Go
|
|
package artwork
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/binary"
|
||
|
|
"image"
|
||
|
|
"image/color"
|
||
|
|
"image/gif"
|
||
|
|
"image/png"
|
||
|
|
|
||
|
|
. "github.com/onsi/ginkgo/v2"
|
||
|
|
. "github.com/onsi/gomega"
|
||
|
|
)
|
||
|
|
|
||
|
|
var _ = Describe("Animation detection", func() {
|
||
|
|
Describe("isAnimatedGIF", func() {
|
||
|
|
It("detects an animated GIF with multiple frames", func() {
|
||
|
|
Expect(isAnimatedGIF(createAnimatedGIF(2))).To(BeTrue())
|
||
|
|
})
|
||
|
|
|
||
|
|
It("detects an animated GIF with many frames", func() {
|
||
|
|
Expect(isAnimatedGIF(createAnimatedGIF(5))).To(BeTrue())
|
||
|
|
})
|
||
|
|
|
||
|
|
It("does not flag a static GIF (single frame)", func() {
|
||
|
|
Expect(isAnimatedGIF(createAnimatedGIF(1))).To(BeFalse())
|
||
|
|
})
|
||
|
|
|
||
|
|
It("returns false for non-GIF data", func() {
|
||
|
|
Expect(isAnimatedGIF(nil)).To(BeFalse())
|
||
|
|
Expect(isAnimatedGIF([]byte{0xFF, 0xD8})).To(BeFalse())
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
Describe("isAnimatedWebP", func() {
|
||
|
|
It("detects an animated WebP with ANMF chunk", func() {
|
||
|
|
Expect(isAnimatedWebP(createAnimatedWebPBytes())).To(BeTrue())
|
||
|
|
})
|
||
|
|
|
||
|
|
It("does not flag a static WebP (no ANMF chunk)", func() {
|
||
|
|
Expect(isAnimatedWebP(createStaticWebPBytes())).To(BeFalse())
|
||
|
|
})
|
||
|
|
|
||
|
|
It("returns false for non-WebP data", func() {
|
||
|
|
Expect(isAnimatedWebP(nil)).To(BeFalse())
|
||
|
|
Expect(isAnimatedWebP([]byte{0xFF, 0xD8})).To(BeFalse())
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
Describe("isAnimatedPNG", func() {
|
||
|
|
It("detects an APNG with acTL chunk", func() {
|
||
|
|
Expect(isAnimatedPNG(createAPNGBytes())).To(BeTrue())
|
||
|
|
})
|
||
|
|
|
||
|
|
It("does not flag a static PNG (no acTL chunk)", func() {
|
||
|
|
Expect(isAnimatedPNG(createStaticPNGBytes())).To(BeFalse())
|
||
|
|
})
|
||
|
|
|
||
|
|
It("returns false for non-PNG data", func() {
|
||
|
|
Expect(isAnimatedPNG(nil)).To(BeFalse())
|
||
|
|
Expect(isAnimatedPNG([]byte{0xFF, 0xD8})).To(BeFalse())
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
// createAnimatedGIF creates a minimal animated GIF with the given number of frames.
|
||
|
|
func createAnimatedGIF(frames int) []byte {
|
||
|
|
g := &gif.GIF{
|
||
|
|
LoopCount: 0,
|
||
|
|
}
|
||
|
|
for range frames {
|
||
|
|
img := image.NewPaletted(image.Rect(0, 0, 2, 2), color.Palette{color.Black, color.White})
|
||
|
|
g.Image = append(g.Image, img)
|
||
|
|
g.Delay = append(g.Delay, 10)
|
||
|
|
}
|
||
|
|
var buf bytes.Buffer
|
||
|
|
err := gif.EncodeAll(&buf, g)
|
||
|
|
if err != nil {
|
||
|
|
panic(err)
|
||
|
|
}
|
||
|
|
return buf.Bytes()
|
||
|
|
}
|
||
|
|
|
||
|
|
// writeUint32LE appends a little-endian uint32 to the buffer.
|
||
|
|
func writeUint32LE(buf *bytes.Buffer, v uint32) {
|
||
|
|
b := make([]byte, 4)
|
||
|
|
binary.LittleEndian.PutUint32(b, v)
|
||
|
|
buf.Write(b)
|
||
|
|
}
|
||
|
|
|
||
|
|
// writeUint32BE appends a big-endian uint32 to the buffer.
|
||
|
|
func writeUint32BE(buf *bytes.Buffer, v uint32) {
|
||
|
|
b := make([]byte, 4)
|
||
|
|
binary.BigEndian.PutUint32(b, v)
|
||
|
|
buf.Write(b)
|
||
|
|
}
|
||
|
|
|
||
|
|
// createAnimatedWebPBytes creates a minimal RIFF/WEBP container with an ANMF chunk.
|
||
|
|
func createAnimatedWebPBytes() []byte {
|
||
|
|
var buf bytes.Buffer
|
||
|
|
buf.WriteString("RIFF")
|
||
|
|
writeUint32LE(&buf, 100) // file size placeholder
|
||
|
|
buf.WriteString("WEBP")
|
||
|
|
// VP8X chunk (extended format, required for animation)
|
||
|
|
buf.WriteString("VP8X")
|
||
|
|
writeUint32LE(&buf, 10)
|
||
|
|
buf.Write(make([]byte, 10))
|
||
|
|
// ANIM chunk (animation parameters)
|
||
|
|
buf.WriteString("ANIM")
|
||
|
|
writeUint32LE(&buf, 6)
|
||
|
|
buf.Write(make([]byte, 6))
|
||
|
|
// ANMF chunk (animation frame)
|
||
|
|
buf.WriteString("ANMF")
|
||
|
|
writeUint32LE(&buf, 16)
|
||
|
|
buf.Write(make([]byte, 16))
|
||
|
|
return buf.Bytes()
|
||
|
|
}
|
||
|
|
|
||
|
|
// createStaticWebPBytes creates a minimal RIFF/WEBP container without ANMF chunks.
|
||
|
|
func createStaticWebPBytes() []byte {
|
||
|
|
var buf bytes.Buffer
|
||
|
|
buf.WriteString("RIFF")
|
||
|
|
writeUint32LE(&buf, 20) // file size
|
||
|
|
buf.WriteString("WEBP")
|
||
|
|
// VP8 chunk (simple lossy format)
|
||
|
|
buf.WriteString("VP8 ")
|
||
|
|
writeUint32LE(&buf, 4)
|
||
|
|
buf.Write(make([]byte, 4))
|
||
|
|
return buf.Bytes()
|
||
|
|
}
|
||
|
|
|
||
|
|
// createAPNGBytes creates a minimal PNG with an acTL chunk (making it APNG).
|
||
|
|
func createAPNGBytes() []byte {
|
||
|
|
// Start with a real PNG
|
||
|
|
staticPNG := createStaticPNGBytes()
|
||
|
|
|
||
|
|
// Insert an acTL chunk after the IHDR chunk.
|
||
|
|
// PNG structure: signature (8) + IHDR chunk (4 len + 4 type + 13 data + 4 crc = 25)
|
||
|
|
ihdrEnd := 8 + 25
|
||
|
|
var buf bytes.Buffer
|
||
|
|
buf.Write(staticPNG[:ihdrEnd])
|
||
|
|
// Write acTL chunk: length=8, type="acTL", data=num_frames(4)+num_plays(4), CRC=4
|
||
|
|
writeUint32BE(&buf, 8) // chunk data length
|
||
|
|
buf.WriteString("acTL")
|
||
|
|
writeUint32BE(&buf, 2) // num_frames
|
||
|
|
writeUint32BE(&buf, 0) // num_plays (0 = infinite)
|
||
|
|
writeUint32BE(&buf, 0) // CRC placeholder
|
||
|
|
buf.Write(staticPNG[ihdrEnd:])
|
||
|
|
return buf.Bytes()
|
||
|
|
}
|
||
|
|
|
||
|
|
// createStaticPNGBytes creates a minimal valid static PNG.
|
||
|
|
func createStaticPNGBytes() []byte {
|
||
|
|
img := image.NewRGBA(image.Rect(0, 0, 2, 2))
|
||
|
|
var buf bytes.Buffer
|
||
|
|
err := png.Encode(&buf, img)
|
||
|
|
if err != nil {
|
||
|
|
panic(err)
|
||
|
|
}
|
||
|
|
return buf.Bytes()
|
||
|
|
}
|