Detect a fetched image's format from its bytes once (closes #319)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
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
This commit was merged in pull request #336.
This commit is contained in:
+21
-98
@@ -6,6 +6,8 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"sneak.berlin/go/pixa/internal/imagerequest"
|
||||
)
|
||||
|
||||
// Magic byte errors.
|
||||
@@ -15,34 +17,6 @@ var (
|
||||
ErrNotEnoughData = errors.New("not enough data to detect format")
|
||||
)
|
||||
|
||||
// MIMEType represents a supported MIME type for input images.
|
||||
type MIMEType string
|
||||
|
||||
// Supported input MIME types.
|
||||
const (
|
||||
MIMETypeJPEG = MIMEType("image/jpeg")
|
||||
MIMETypePNG = MIMEType("image/png")
|
||||
MIMETypeWebP = MIMEType("image/webp")
|
||||
MIMETypeGIF = MIMEType("image/gif")
|
||||
MIMETypeAVIF = MIMEType("image/avif")
|
||||
MIMETypeJXL = MIMEType("image/jxl")
|
||||
MIMETypeSVG = MIMEType("image/svg+xml")
|
||||
)
|
||||
|
||||
// ImageFormat represents supported output image formats.
|
||||
// This mirrors the type in imgcache to avoid circular imports.
|
||||
type ImageFormat string
|
||||
|
||||
// Supported image output formats.
|
||||
const (
|
||||
FormatJPEG ImageFormat = "jpeg"
|
||||
FormatPNG ImageFormat = "png"
|
||||
FormatWebP ImageFormat = "webp"
|
||||
FormatAVIF ImageFormat = "avif"
|
||||
FormatJXL ImageFormat = "jxl"
|
||||
FormatGIF ImageFormat = "gif"
|
||||
)
|
||||
|
||||
// MinMagicBytes is the minimum number of bytes needed to detect format.
|
||||
const MinMagicBytes = 12
|
||||
|
||||
@@ -78,32 +52,32 @@ var (
|
||||
avisBrand = []byte{0x61, 0x76, 0x69, 0x73} // "avis" (AVIF sequence)
|
||||
)
|
||||
|
||||
// DetectFormat detects the image format from magic bytes.
|
||||
// Returns the MIME type and nil error on success.
|
||||
func DetectFormat(data []byte) (MIMEType, error) {
|
||||
// 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 MIMETypeJPEG, nil
|
||||
return imagerequest.FormatJPEG, nil
|
||||
}
|
||||
|
||||
// Check PNG (89504E47 0D0A1A0A)
|
||||
if bytes.HasPrefix(data, magicPNG) {
|
||||
return MIMETypePNG, nil
|
||||
return imagerequest.FormatPNG, nil
|
||||
}
|
||||
|
||||
// Check GIF (GIF87a or GIF89a)
|
||||
if bytes.HasPrefix(data, magicGIF) {
|
||||
return MIMETypeGIF, nil
|
||||
return imagerequest.FormatGIF, nil
|
||||
}
|
||||
|
||||
// Check WebP (RIFF....WEBP)
|
||||
if bytes.HasPrefix(data, magicWebP) && len(data) >= 12 {
|
||||
if bytes.Equal(data[8:12], webpIdent) {
|
||||
return MIMETypeWebP, nil
|
||||
return imagerequest.FormatWebP, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,66 +86,39 @@ func DetectFormat(data []byte) (MIMEType, error) {
|
||||
if len(data) >= 12 && bytes.Equal(data[4:8], magicFtyp) {
|
||||
brand := data[8:12]
|
||||
if bytes.Equal(brand, avifBrand) || bytes.Equal(brand, avisBrand) {
|
||||
return MIMETypeAVIF, nil
|
||||
return imagerequest.FormatAVIF, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check JPEG XL (FF0A, or the 12-byte container signature)
|
||||
if bytes.HasPrefix(data, magicJXLCodestream) ||
|
||||
bytes.HasPrefix(data, magicJXLContainer) {
|
||||
return MIMETypeJXL, nil
|
||||
}
|
||||
|
||||
// Check SVG - look for XML declaration or SVG tag
|
||||
if detectSVG(data) {
|
||||
return MIMETypeSVG, nil
|
||||
return imagerequest.FormatJXL, nil
|
||||
}
|
||||
|
||||
return "", ErrUnknownFormat
|
||||
}
|
||||
|
||||
// detectSVG checks if data appears to be SVG content.
|
||||
func detectSVG(data []byte) bool {
|
||||
// Skip BOM if present
|
||||
content := skipBOM(data)
|
||||
|
||||
// Convert to string for easier pattern matching
|
||||
s := strings.ToLower(string(content))
|
||||
|
||||
// Skip leading whitespace
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
// Check for XML declaration or SVG element
|
||||
return strings.HasPrefix(s, "<?xml") ||
|
||||
strings.HasPrefix(s, "<svg") ||
|
||||
strings.HasPrefix(s, "<!doctype svg")
|
||||
}
|
||||
|
||||
// skipBOM removes UTF-8 BOM if present.
|
||||
func skipBOM(data []byte) []byte {
|
||||
if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
|
||||
return data[3:]
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// ValidateMagicBytes validates that the content matches the declared MIME type.
|
||||
func ValidateMagicBytes(data []byte, declaredType string) error {
|
||||
// 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
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Normalize the declared type (remove parameters like charset)
|
||||
normalizedDeclared := normalizeMIMEType(declaredType)
|
||||
|
||||
// Check if they match
|
||||
if string(detected) != normalizedDeclared {
|
||||
return ErrMagicByteMismatch
|
||||
if detected.MIMEType() != normalizedDeclared {
|
||||
return "", ErrMagicByteMismatch
|
||||
}
|
||||
|
||||
return nil
|
||||
return detected, nil
|
||||
}
|
||||
|
||||
// normalizeMIMEType extracts just the media type, removing parameters.
|
||||
@@ -183,27 +130,3 @@ func normalizeMIMEType(mimeType string) string {
|
||||
|
||||
return strings.TrimSpace(strings.ToLower(mimeType))
|
||||
}
|
||||
|
||||
// MIMEToImageFormat converts a MIME type to an ImageFormat.
|
||||
func MIMEToImageFormat(mimeType string) (ImageFormat, bool) {
|
||||
normalized := normalizeMIMEType(mimeType)
|
||||
switch MIMEType(normalized) {
|
||||
case MIMETypeJPEG:
|
||||
return FormatJPEG, true
|
||||
case MIMETypePNG:
|
||||
return FormatPNG, true
|
||||
case MIMETypeWebP:
|
||||
return FormatWebP, true
|
||||
case MIMETypeGIF:
|
||||
return FormatGIF, true
|
||||
case MIMETypeAVIF:
|
||||
return FormatAVIF, true
|
||||
case MIMETypeJXL:
|
||||
return FormatJXL, true
|
||||
case MIMETypeSVG:
|
||||
// SVG has no corresponding output format.
|
||||
return "", false
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user