Detect a fetched image's format from its bytes once (closes #319)
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:
2026-10-26 21:21:39 +01:00
parent ac850823f4
commit b73bbde46e
37 changed files with 570 additions and 589 deletions
+55 -92
View File
@@ -70,8 +70,6 @@ type Result struct {
InputWidth int
// InputHeight is the original image height before processing.
InputHeight int
// InputFormat is the detected input format (e.g., "jpeg", "png").
InputFormat string
}
// MaxInputDimension is the maximum allowed width or height for input images.
@@ -113,9 +111,10 @@ var ErrAVIFOutputTooLarge = errors.New("AVIF output has too many pixels")
var ErrUnsupportedOutputFormat = errors.New("unsupported output format")
// ErrUnsupportedInputFormat is returned, before the input is decoded, when
// libvips reads the input's first bytes as another format than the source
// formats, or as none: for example an SVG whose first bytes pass the
// first-bytes check for AVIF.
// libvips reads the input's first bytes as another format than its source
// format, or as none. An SVG whose first bytes pass the first-bytes check for
// AVIF is one example. A source format other than the six pixa reads is also
// refused with it.
var ErrUnsupportedInputFormat = errors.New("unsupported input format")
// ErrTooManyImages is returned when MaxConcurrentProcessing images are being
@@ -157,9 +156,10 @@ func New(params Params) *ImageProcessor {
}
}
// Process transforms an image according to transform. When
// MaxConcurrentProcessing images are already being processed, it waits up
// to ProcessingWaitTimeout for one to finish, then fails with
// Process transforms the image read from input according to transform.
// sourceFormat is the input's format, which magic detected from its first
// bytes. When MaxConcurrentProcessing images are already being processed,
// it waits up to ProcessingWaitTimeout for one to finish, then fails with
// ErrTooManyImages. When ctx has ended once Process has a slot, or before it
// encodes, it returns ctx's error and gives the slot back. govips gives pixa
// no way to stop libvips once an encode has begun, so an encode keeps its
@@ -168,6 +168,7 @@ func New(params Params) *ImageProcessor {
func (p *ImageProcessor) Process(
ctx context.Context,
input io.Reader,
sourceFormat imagerequest.Format,
transform *imagerequest.Transform,
) (*Result, error) {
release, err := p.acquireSlot(ctx)
@@ -182,13 +183,9 @@ func (p *ImageProcessor) Process(
return nil, err
}
// govips picks the libvips loader from the input's first bytes with
// DetermineImageType. Input whose first bytes it reads as no source
// format, such as SVG, which librsvg would render, is refused before
// libvips reads it.
inputFormat := p.detectFormat(vips.DetermineImageType(data))
sourceFormat, err := p.formatFromString(inputFormat)
// Input libvips would not load as sourceFormat is refused before it is
// decoded
err = checkInputFormat(data, sourceFormat)
if err != nil {
return nil, err
}
@@ -254,12 +251,11 @@ func (p *ImageProcessor) Process(
return &Result{
Content: io.NopCloser(bytes.NewReader(output)),
ContentLength: int64(len(output)),
ContentType: FormatToMIME(outputFormat),
ContentType: outputFormat.MIMEType(),
Width: img.Width(),
Height: img.Height(),
InputWidth: origWidth,
InputHeight: origHeight,
InputFormat: inputFormat,
}, nil
}
@@ -294,38 +290,6 @@ func targetDimensions(
return width, height, nil
}
// MIME types for the supported image formats.
const (
mimeJPEG = "image/jpeg"
mimePNG = "image/png"
mimeGIF = "image/gif"
mimeWebP = "image/webp"
mimeAVIF = "image/avif"
mimeJXL = "image/jxl"
)
// FormatToMIME converts a Format to its MIME type string.
func FormatToMIME(format imagerequest.Format) string {
switch format {
case imagerequest.FormatJPEG:
return mimeJPEG
case imagerequest.FormatPNG:
return mimePNG
case imagerequest.FormatWebP:
return mimeWebP
case imagerequest.FormatGIF:
return mimeGIF
case imagerequest.FormatAVIF:
return mimeAVIF
case imagerequest.FormatJXL:
return mimeJXL
case imagerequest.FormatOriginal, imagerequest.FormatAuto:
return "application/octet-stream"
default:
return "application/octet-stream"
}
}
// WaitForProcessing waits until no image is being processed, or until ctx
// ends, and returns how many images were still being processed then. It
// waits by taking each slot in processingSemaphore as it frees up until it
@@ -394,27 +358,49 @@ func (p *ImageProcessor) readInput(input io.Reader) ([]byte, error) {
return data, nil
}
// detectFormat returns the format string for a govips image type.
func (p *ImageProcessor) detectFormat(imageType vips.ImageType) string {
switch imageType {
case vips.ImageTypeJPEG:
return "jpeg"
case vips.ImageTypePNG:
return "png"
case vips.ImageTypeGIF:
return "gif"
case vips.ImageTypeWEBP:
return "webp"
case vips.ImageTypeAVIF, vips.ImageTypeHEIF:
return string(imagerequest.FormatAVIF)
case vips.ImageTypeJXL:
return string(imagerequest.FormatJXL)
case vips.ImageTypeUnknown, vips.ImageTypeMagick, vips.ImageTypePDF,
vips.ImageTypeSVG, vips.ImageTypeTIFF, vips.ImageTypeBMP,
vips.ImageTypeJP2K:
return "unknown"
// checkInputFormat returns ErrUnsupportedInputFormat unless libvips would
// load data with the loader of sourceFormat. govips picks the libvips loader
// from the input's first bytes with DetermineImageType. It refuses an SVG
// whose first bytes pass the first-bytes check for AVIF, which librsvg would
// otherwise render.
func checkInputFormat(data []byte, sourceFormat imagerequest.Format) error {
// A format with no loader, such as orig or auto, has the type govips also
// gives input it cannot read, which the check below would let through.
sourceType := vipsImageType(sourceFormat)
if sourceType == vips.ImageTypeUnknown {
return fmt.Errorf("%w: %q is not a source format",
ErrUnsupportedInputFormat, sourceFormat)
}
if vips.DetermineImageType(data) != sourceType {
return fmt.Errorf("%w: libvips does not read the input as %s",
ErrUnsupportedInputFormat, sourceFormat)
}
return nil
}
// vipsImageType returns the govips image type whose libvips loader reads
// format. orig and auto stand for another format, so they have
// vips.ImageTypeUnknown, which govips has no loader for.
func vipsImageType(format imagerequest.Format) vips.ImageType {
switch format {
case imagerequest.FormatJPEG:
return vips.ImageTypeJPEG
case imagerequest.FormatPNG:
return vips.ImageTypePNG
case imagerequest.FormatGIF:
return vips.ImageTypeGIF
case imagerequest.FormatWebP:
return vips.ImageTypeWEBP
case imagerequest.FormatAVIF:
return vips.ImageTypeAVIF
case imagerequest.FormatJXL:
return vips.ImageTypeJXL
case imagerequest.FormatOriginal, imagerequest.FormatAuto:
return vips.ImageTypeUnknown
default:
return "unknown"
return vips.ImageTypeUnknown
}
}
@@ -768,26 +754,3 @@ func jxlDistance(quality int) float64 {
return 53.0/3000.0*q*q - 23.0/20.0*q + 25.0
}
// formatFromString converts a format string from detectFormat to Format. A
// format detectFormat could not name is ErrUnsupportedInputFormat.
func (p *ImageProcessor) formatFromString(
format string,
) (imagerequest.Format, error) {
switch format {
case "jpeg":
return imagerequest.FormatJPEG, nil
case "png":
return imagerequest.FormatPNG, nil
case "gif":
return imagerequest.FormatGIF, nil
case "webp":
return imagerequest.FormatWebP, nil
case string(imagerequest.FormatAVIF):
return imagerequest.FormatAVIF, nil
case string(imagerequest.FormatJXL):
return imagerequest.FormatJXL, nil
default:
return "", fmt.Errorf("%w: %s", ErrUnsupportedInputFormat, format)
}
}