// Package magic detects image formats from magic bytes and validates // content against declared MIME types. package magic import ( "bytes" "errors" "strings" "sneak.berlin/go/pixa/internal/imagerequest" ) // Magic byte errors. var ( ErrUnknownFormat = errors.New("unknown image format") ErrMagicByteMismatch = errors.New("content does not match declared Content-Type") ErrNotEnoughData = errors.New("not enough data to detect format") ) // MinMagicBytes is the minimum number of bytes needed to detect format. const MinMagicBytes = 12 // MaxMagicBytes is the most bytes DetectFormat looks at. An SVG's root // element must start within them, as libvips requires. const MaxMagicBytes = 1024 // Magic byte signatures for supported formats. // These are effectively constants but Go doesn't support const slices. // //nolint:gochecknoglobals // immutable lookup data var ( magicJPEG = []byte{0xFF, 0xD8, 0xFF} magicPNG = []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} magicGIF = []byte{0x47, 0x49, 0x46, 0x38} // GIF8 (GIF87a or GIF89a) magicWebP = []byte{0x52, 0x49, 0x46, 0x46} // RIFF (WebP starts with RIFF....WEBP) // AVIF uses the ftyp box with brand "avif" or "avis" // Format: size(4 bytes) + "ftyp" + brand(4 bytes) magicFtyp = []byte{0x66, 0x74, 0x79, 0x70} // "ftyp" // JPEG XL is a bare codestream or a codestream in a container magicJXLCodestream = []byte{0xFF, 0x0A} magicJXLContainer = []byte{ 0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A, } ) // utf8BOM is the UTF-8 byte order mark, which a text file such as an SVG may // start with. // //nolint:gochecknoglobals // immutable lookup data var utf8BOM = []byte{0xEF, 0xBB, 0xBF} // WebP identifier appears at offset 8 after RIFF header. // //nolint:gochecknoglobals // immutable lookup data var webpIdent = []byte{0x57, 0x45, 0x42, 0x50} // "WEBP" // AVIF brand identifiers. // //nolint:gochecknoglobals // immutable lookup data var ( avifBrand = []byte{0x61, 0x76, 0x69, 0x66} // "avif" avisBrand = []byte{0x61, 0x76, 0x69, 0x73} // "avis" (AVIF sequence) ) // DetectFormat detects the image format from magic bytes. Data in none of // the formats pixa reads is ErrUnknownFormat. func DetectFormat(data []byte) (imagerequest.Format, error) { if len(data) < MinMagicBytes { return "", ErrNotEnoughData } // Check JPEG (FFD8FF) if bytes.HasPrefix(data, magicJPEG) { return imagerequest.FormatJPEG, nil } // Check PNG (89504E47 0D0A1A0A) if bytes.HasPrefix(data, magicPNG) { return imagerequest.FormatPNG, nil } // Check GIF (GIF87a or GIF89a) if bytes.HasPrefix(data, magicGIF) { return imagerequest.FormatGIF, nil } // Check WebP (RIFF....WEBP) if bytes.HasPrefix(data, magicWebP) && len(data) >= 12 { if bytes.Equal(data[8:12], webpIdent) { return imagerequest.FormatWebP, nil } } // Check AVIF (....ftypavif or ....ftypavis) // The ftyp box can start at offset 4 (after size bytes) if len(data) >= 12 && bytes.Equal(data[4:8], magicFtyp) { brand := data[8:12] if bytes.Equal(brand, avifBrand) || bytes.Equal(brand, avisBrand) { return imagerequest.FormatAVIF, nil } } // Check JPEG XL (FF0A, or the 12-byte container signature) if bytes.HasPrefix(data, magicJXLCodestream) || bytes.HasPrefix(data, magicJXLContainer) { return imagerequest.FormatJXL, nil } if isSVG(data) { return imagerequest.FormatSVG, nil } return "", ErrUnknownFormat } // isSVG reports whether data starts as an SVG does: with a "<", after an // optional UTF-8 byte order mark and white space, and with " "image/jpeg" if idx := strings.Index(mimeType, ";"); idx != -1 { mimeType = mimeType[:idx] } return strings.TrimSpace(strings.ToLower(mimeType)) }