Files
pixa/internal/magic/magic.go
T
sneak 4515d09c49
check / check (push) Waiting to run
Detect a fetched image's format from its bytes once (closes #319)
magic.DetectFormat returns an imagerequest.Format. ValidateMagicBytes
returns the format it checked against the upstream Content-Type. The
cache reads a cached source's format from its file's first bytes, as it
reads a variant's. The proxy hands the format to Process. Process
refuses a format that is not a source format, and input libvips would
load with another loader, such as an SVG passing as AVIF.
Format.MIMEType declares each MIME string once, and the fetcher, the
cache's variant read and the Accept matching use it. magic.MIMEType,
magic.ImageFormat, MIMEToImageFormat, magic's SVG detection,
detectFormat, formatFromString, FormatToMIME and Result.InputFormat are
gone.

Model: opus-5-5
2026-10-26 20:08:10 +00:00

133 lines
3.9 KiB
Go

// 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
// 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,
}
)
// 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, SVG included, 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
}
return "", ErrUnknownFormat
}
// ValidateMagicBytes detects the format of data from its magic bytes, and
// returns it when it is the format the declared MIME type names. Data in
// another format is ErrMagicByteMismatch.
func ValidateMagicBytes(
data []byte, declaredType string,
) (imagerequest.Format, error) {
detected, err := DetectFormat(data)
if err != nil {
return "", err
}
// Normalize the declared type (remove parameters like charset)
normalizedDeclared := normalizeMIMEType(declaredType)
// Check if they match
if detected.MIMEType() != normalizedDeclared {
return "", ErrMagicByteMismatch
}
return detected, nil
}
// normalizeMIMEType extracts just the media type, removing parameters.
func normalizeMIMEType(mimeType string) string {
// Handle "image/jpeg; charset=utf-8" -> "image/jpeg"
if idx := strings.Index(mimeType, ";"); idx != -1 {
mimeType = mimeType[:idx]
}
return strings.TrimSpace(strings.ToLower(mimeType))
}