🏡 index : quine-global/quintodrome.git

package artwork

import (
	"bytes"
	"encoding/binary"
)

// isAnimatedGIF checks for multiple image descriptor blocks (0x2C) in a GIF file.
// Animated GIFs use GIF89a and contain multiple image blocks.
func isAnimatedGIF(data []byte) bool {
	// GIF header: "GIF87a" or "GIF89a"
	if !bytes.HasPrefix(data, []byte("GIF")) {
		return false
	}

	// Skip header (6 bytes) + logical screen descriptor (7 bytes)
	pos := 13
	if pos >= len(data) {
		return false
	}

	// Skip Global Color Table if present (bit 7 of packed byte at offset 10)
	if len(data) > 10 && data[10]&0x80 != 0 {
		// GCT size = 3 * 2^(N+1) where N = bits 0-2 of packed byte
		gctSize := 3 * (1 << ((data[10] & 0x07) + 1))
		pos += gctSize
	}

	frameCount := 0
	for pos < len(data) {
		switch data[pos] {
		case 0x2C: // Image Descriptor - marks a frame
			frameCount++
			if frameCount > 1 {
				return true
			}
			pos++ // skip introducer
			if pos+8 >= len(data) {
				return false
			}
			pos += 8 // skip x, y, w, h (each 2 bytes)
			packed := data[pos]
			pos++ // skip packed byte
			// Skip Local Color Table if present
			if packed&0x80 != 0 {
				lctSize := 3 * (1 << ((packed & 0x07) + 1))
				pos += lctSize
			}
			// Skip LZW minimum code size
			pos++
			// Skip sub-blocks
			pos = skipGIFSubBlocks(data, pos)
		case 0x21: // Extension block
			pos++ // skip introducer
			if pos >= len(data) {
				return false
			}
			pos++ // skip extension label
			// Skip sub-blocks
			pos = skipGIFSubBlocks(data, pos)
		case 0x3B: // Trailer
			return false
		default:
			// Unknown block, bail
			return false
		}
	}
	return false
}

// skipGIFSubBlocks advances past a sequence of GIF sub-blocks (terminated by a zero-length block).
func skipGIFSubBlocks(data []byte, pos int) int {
	for pos < len(data) {
		blockSize := int(data[pos])
		pos++ // skip size byte
		if blockSize == 0 {
			break
		}
		pos += blockSize
	}
	return pos
}

// isAnimatedWebP checks for ANMF (animation frame) chunks in a WebP RIFF container.
func isAnimatedWebP(data []byte) bool {
	// WebP header: "RIFF" + 4 bytes size + "WEBP"
	if !bytes.HasPrefix(data, []byte("RIFF")) || len(data) < 12 {
		return false
	}
	if !bytes.Equal(data[8:12], []byte("WEBP")) {
		return false
	}
	// Scan for ANMF chunk identifier
	return bytes.Contains(data[12:], []byte("ANMF"))
}

// isAnimatedPNG checks for the acTL (animation control) chunk in a PNG file.
// APNG files contain an acTL chunk that is not present in static PNGs.
func isAnimatedPNG(data []byte) bool {
	// PNG signature: 8 bytes
	pngSig := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}
	if !bytes.HasPrefix(data, pngSig) {
		return false
	}

	// Scan chunks for "acTL" (animation control)
	pos := uint64(8)
	dataLen := uint64(len(data))
	for pos+8 <= dataLen {
		chunkLen := uint64(binary.BigEndian.Uint32(data[pos : pos+4]))
		chunkType := string(data[pos+4 : pos+8])

		if chunkType == "acTL" {
			return true
		}
		// Move to next chunk: 4 (length) + 4 (type) + chunkLen (data) + 4 (CRC)
		pos += 12 + chunkLen
	}
	return false
}