🏡 index : ironmagma/quintodrome.git

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()
}