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
@@ -39,30 +39,31 @@ func TestImageProcessor_RefusesAVIFAboveMaxPixels(t *testing.T) {
tests := []struct {
name string
input []byte
sourceFormat imagerequest.Format
format imagerequest.Format
width, height int
fit imagerequest.Fit
refused bool
}{
{"AVIF at the limit", cutOff, imagerequest.FormatAVIF,
4096, 4096, imagerequest.FitCover, false},
{"AVIF above the limit", cutOff, imagerequest.FormatAVIF,
4096, 4097, imagerequest.FitCover, true},
{"WebP above the AVIF limit", cutOff, imagerequest.FormatWebP,
4096, 4097, imagerequest.FitCover, false},
{"AVIF at the limit", cutOff, imagerequest.FormatPNG,
imagerequest.FormatAVIF, 4096, 4096, imagerequest.FitCover, false},
{"AVIF above the limit", cutOff, imagerequest.FormatPNG,
imagerequest.FormatAVIF, 4096, 4097, imagerequest.FitCover, true},
{"WebP above the AVIF limit", cutOff, imagerequest.FormatPNG,
imagerequest.FormatWebP, 4096, 4097, imagerequest.FitCover, false},
// FitInside does not enlarge, so the output stays 64x32
{"AVIF inside a larger size", cutOff, imagerequest.FormatAVIF,
8192, 8192, imagerequest.FitInside, false},
{"AVIF inside a larger size", cutOff, imagerequest.FormatPNG,
imagerequest.FormatAVIF, 8192, 8192, imagerequest.FitInside, false},
// FitOutside scales 64x32 by 128 to cover 8192x1: 8192x4096
{"AVIF outside a smaller size", cutOff, imagerequest.FormatAVIF,
8192, 1, imagerequest.FitOutside, true},
{"AVIF outside a smaller size", cutOff, imagerequest.FormatPNG,
imagerequest.FormatAVIF, 8192, 1, imagerequest.FitOutside, true},
// With no size, the output keeps the source's 4097x4096
{"AVIF of the source's size", largeAVIF, imagerequest.FormatAVIF,
0, 0, imagerequest.FitCover, true},
{"orig of an AVIF source", largeAVIF, imagerequest.FormatOriginal,
0, 0, imagerequest.FitCover, true},
{"JPEG of the source's size", largeAVIF, imagerequest.FormatJPEG,
0, 0, imagerequest.FitCover, false},
imagerequest.FormatAVIF, 0, 0, imagerequest.FitCover, true},
{"orig of an AVIF source", largeAVIF, imagerequest.FormatAVIF,
imagerequest.FormatOriginal, 0, 0, imagerequest.FitCover, true},
{"JPEG of the source's size", largeAVIF, imagerequest.FormatAVIF,
imagerequest.FormatJPEG, 0, 0, imagerequest.FitCover, false},
}
for _, tt := range tests {
@@ -72,7 +73,7 @@ func TestImageProcessor_RefusesAVIFAboveMaxPixels(t *testing.T) {
_, err := New(Params{
MaxInputBytes: 50 << 20,
MaxConcurrentProcessing: 1,
}).Process(t.Context(), bytes.NewReader(tt.input),
}).Process(t.Context(), bytes.NewReader(tt.input), tt.sourceFormat,
&imagerequest.Transform{
Size: imagerequest.Size{Width: tt.width, Height: tt.height},
Format: tt.format,
@@ -42,7 +42,7 @@ func TestImageProcessor_RefusesComputedSideAboveMaximum(t *testing.T) {
MaxInputBytes: 50 << 20,
MaxConcurrentProcessing: 1,
}).Process(t.Context(),
bytes.NewReader(input),
bytes.NewReader(input), imagerequest.FormatJPEG,
&imagerequest.Transform{
Size: tt.size,
Format: imagerequest.FormatJPEG,
@@ -111,7 +111,7 @@ func TestImageProcessor_ComputedSideIsAtLeastOne(t *testing.T) {
MaxInputBytes: 50 << 20,
MaxConcurrentProcessing: 1,
}).Process(t.Context(),
bytes.NewReader(input),
bytes.NewReader(input), imagerequest.FormatJPEG,
&imagerequest.Transform{
Size: tt.size,
Format: imagerequest.FormatJPEG,
@@ -7,6 +7,8 @@ import (
"io"
"testing"
"time"
"sneak.berlin/go/pixa/internal/imagerequest"
)
// readerAfterDeadline is a Process input whose first Read waits until done
@@ -86,7 +88,8 @@ func checkProcessStopsAtDeadline(
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
result, err := proc.Process(ctx, input, smallJPEGTransform())
result, err := proc.Process(ctx, input, imagerequest.FormatJPEG,
smallJPEGTransform())
if result != nil {
t.Logf("Process() made a %dx%d image after its deadline passed",
result.Width, result.Height)
@@ -19,7 +19,7 @@ func TestImageProcessor_EmptyFormatRefused(t *testing.T) {
MaxConcurrentProcessing: 1,
}).Process(
t.Context(), bytes.NewReader(createTestJPEG(t, 20, 20)),
&imagerequest.Transform{},
imagerequest.FormatJPEG, &imagerequest.Transform{},
)
if !errors.Is(err, ErrUnsupportedOutputFormat) {
t.Errorf("Process() error = %v, want %v", err, ErrUnsupportedOutputFormat)
@@ -11,17 +11,18 @@ import (
"sneak.berlin/go/pixa/internal/imagerequest"
)
// processedSize runs input through Process and returns the output's size in
// bytes.
// processedSize runs input, an image in sourceFormat, through Process and
// returns the output's size in bytes.
func processedSize(
t *testing.T, input []byte, transform *imagerequest.Transform,
t *testing.T, input []byte, sourceFormat imagerequest.Format,
transform *imagerequest.Transform,
) int64 {
t.Helper()
result, err := New(Params{
MaxInputBytes: 50 << 20,
MaxConcurrentProcessing: 1,
}).Process(t.Context(), bytes.NewReader(input), transform)
}).Process(t.Context(), bytes.NewReader(input), sourceFormat, transform)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
@@ -74,7 +75,7 @@ func TestImageProcessor_PNGIsCompressed(t *testing.T) {
}
}
size := processedSize(t, encodePNG(t, flat),
size := processedSize(t, encodePNG(t, flat), imagerequest.FormatPNG,
&imagerequest.Transform{Format: imagerequest.FormatPNG})
const rawBytes = width * height * 3
@@ -92,7 +93,7 @@ func TestImageProcessor_WebPAtEffort2(t *testing.T) {
input := createTestJPEG(t, 200, 150)
size := processedSize(t, input,
size := processedSize(t, input, imagerequest.FormatJPEG,
&imagerequest.Transform{Format: imagerequest.FormatWebP, Quality: 85})
want, _, err := decode(t, input).ExportWebp(&vips.WebpExportParams{
@@ -134,7 +135,7 @@ func TestImageProcessor_AVIFAtEffort1(t *testing.T) {
input := encodePNG(t, source)
size := processedSize(t, input,
size := processedSize(t, input, imagerequest.FormatPNG,
&imagerequest.Transform{Format: imagerequest.FormatAVIF, Quality: 85})
want, _, err := decode(t, input).ExportAvif(&vips.AvifExportParams{
@@ -161,7 +162,7 @@ func TestImageProcessor_JPEGXLAtEffort4(t *testing.T) {
input := createTestJPEG(t, 200, 150)
size := processedSize(t, input,
size := processedSize(t, input, imagerequest.FormatJPEG,
&imagerequest.Transform{Format: imagerequest.FormatJXL, Quality: 85})
params := vips.NewJxlExportParams()
@@ -40,7 +40,7 @@ func TestImageProcessor_FitCoverServesThinSource(t *testing.T) {
MaxInputBytes: 50 << 20,
MaxConcurrentProcessing: 1,
}).Process(t.Context(),
bytes.NewReader(input),
bytes.NewReader(input), imagerequest.FormatJPEG,
&imagerequest.Transform{
Size: tt.size,
Format: imagerequest.FormatJPEG,
@@ -113,7 +113,7 @@ func TestImageProcessor_FitCoverCropsTheCentre(t *testing.T) {
MaxInputBytes: 50 << 20,
MaxConcurrentProcessing: 1,
}).Process(t.Context(),
bytes.NewReader(input),
bytes.NewReader(input), imagerequest.FormatPNG,
&imagerequest.Transform{
Size: imagerequest.Size{Width: 50, Height: 50},
Format: imagerequest.FormatPNG,
@@ -240,7 +240,7 @@ func TestImageProcessor_FitCoverKeepsProportions(t *testing.T) {
MaxInputBytes: 50 << 20,
MaxConcurrentProcessing: 1,
}).Process(t.Context(),
bytes.NewReader(input),
bytes.NewReader(input), imagerequest.FormatPNG,
&imagerequest.Transform{
Size: tt.size,
Format: imagerequest.FormatPNG,
@@ -45,7 +45,7 @@ func TestImageProcessor_RefusesFitOutsideAboveMaximum(t *testing.T) {
MaxInputBytes: 50 << 20,
MaxConcurrentProcessing: 1,
}).Process(t.Context(),
bytes.NewReader(input),
bytes.NewReader(input), imagerequest.FormatJPEG,
&imagerequest.Transform{
Size: tt.size,
Format: imagerequest.FormatJPEG,
@@ -102,7 +102,7 @@ func TestImageProcessor_FitOutsideSideIsAtLeastOne(t *testing.T) {
MaxInputBytes: 50 << 20,
MaxConcurrentProcessing: 1,
}).Process(t.Context(),
bytes.NewReader(input),
bytes.NewReader(input), imagerequest.FormatJPEG,
&imagerequest.Transform{
Size: tt.size,
Format: imagerequest.FormatJPEG,
+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)
}
}
@@ -94,23 +94,23 @@ func isAVIF(data []byte) bool {
// detectMIME is a minimal magic-byte detector for test assertions.
func detectMIME(data []byte) string {
if len(data) >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
return mimeJPEG
return imagerequest.FormatJPEG.MIMEType()
}
if len(data) >= 8 && string(data[:8]) == "\x89PNG\r\n\x1a\n" {
return mimePNG
return imagerequest.FormatPNG.MIMEType()
}
if len(data) >= 4 && string(data[:4]) == "GIF8" {
return mimeGIF
return imagerequest.FormatGIF.MIMEType()
}
if len(data) >= 12 && string(data[:4]) == "RIFF" && string(data[8:12]) == "WEBP" {
return mimeWebP
return imagerequest.FormatWebP.MIMEType()
}
if isAVIF(data) {
return mimeAVIF
return imagerequest.FormatAVIF.MIMEType()
}
return ""
@@ -131,7 +131,8 @@ func TestImageProcessor_ResizeJPEG(t *testing.T) {
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
result, err := proc.Process(ctx, bytes.NewReader(input),
imagerequest.FormatJPEG, transform)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
@@ -157,7 +158,7 @@ func TestImageProcessor_ResizeJPEG(t *testing.T) {
}
mime := detectMIME(data)
if mime != mimeJPEG {
if mime != imagerequest.FormatJPEG.MIMEType() {
t.Errorf("Output format = %v, want image/jpeg", mime)
}
}
@@ -176,7 +177,8 @@ func TestImageProcessor_ConvertToPNG(t *testing.T) {
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
result, err := proc.Process(ctx, bytes.NewReader(input),
imagerequest.FormatJPEG, transform)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
@@ -189,7 +191,7 @@ func TestImageProcessor_ConvertToPNG(t *testing.T) {
}
mime := detectMIME(data)
if mime != mimePNG {
if mime != imagerequest.FormatPNG.MIMEType() {
t.Errorf("Output format = %v, want image/png", mime)
}
}
@@ -213,7 +215,8 @@ func processAndCheckSize(
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
result, err := proc.Process(ctx, bytes.NewReader(input),
imagerequest.FormatJPEG, transform)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
@@ -253,7 +256,8 @@ func TestImageProcessor_FitContain(t *testing.T) {
FitMode: imagerequest.FitContain,
}
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
result, err := proc.Process(ctx, bytes.NewReader(input),
imagerequest.FormatJPEG, transform)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
@@ -296,7 +300,8 @@ func TestImageProcessor_ProcessPNG(t *testing.T) {
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
result, err := proc.Process(ctx, bytes.NewReader(input),
imagerequest.FormatPNG, transform)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
@@ -341,7 +346,8 @@ func TestImageProcessor_RejectsOversizedInput(t *testing.T) {
FitMode: imagerequest.FitCover,
}
_, err := proc.Process(ctx, bytes.NewReader(input), transform)
_, err := proc.Process(ctx, bytes.NewReader(input),
imagerequest.FormatJPEG, transform)
if err == nil {
t.Error("Process() should reject oversized input images")
}
@@ -369,7 +375,8 @@ func TestImageProcessor_AcceptsMaxDimensionInput(t *testing.T) {
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
result, err := proc.Process(ctx, bytes.NewReader(input),
imagerequest.FormatJPEG, transform)
if err != nil {
t.Fatalf(
"Process() should accept images at MaxInputDimension, got error: %v",
@@ -399,7 +406,8 @@ func encodeAndCheck(
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
result, err := proc.Process(ctx, bytes.NewReader(input),
imagerequest.FormatJPEG, transform)
if err != nil {
t.Fatalf("Process() error = %v, want nil", err)
}
@@ -430,7 +438,7 @@ func encodeAndCheck(
func TestImageProcessor_EncodeWebP(t *testing.T) {
t.Parallel()
encodeAndCheck(t, imagerequest.FormatWebP, 80, mimeWebP)
encodeAndCheck(t, imagerequest.FormatWebP, 80, imagerequest.FormatWebP.MIMEType())
}
func TestImageProcessor_DecodeAVIF(t *testing.T) {
@@ -453,7 +461,8 @@ func TestImageProcessor_DecodeAVIF(t *testing.T) {
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
result, err := proc.Process(ctx, bytes.NewReader(input),
imagerequest.FormatAVIF, transform)
if err != nil {
t.Fatalf("Process() error = %v, want nil (AVIF decoding should work)", err)
}
@@ -467,7 +476,7 @@ func TestImageProcessor_DecodeAVIF(t *testing.T) {
}
mime := detectMIME(data)
if mime != mimeJPEG {
if mime != imagerequest.FormatJPEG.MIMEType() {
t.Errorf("Output format = %v, want image/jpeg", mime)
}
}
@@ -494,7 +503,8 @@ func TestImageProcessor_RejectsOversizedInputData(t *testing.T) {
FitMode: imagerequest.FitCover,
}
_, err := proc.Process(ctx, bytes.NewReader(input), transform)
_, err := proc.Process(ctx, bytes.NewReader(input),
imagerequest.FormatJPEG, transform)
if err == nil {
t.Fatal("Process() should reject input exceeding maxInputBytes")
}
@@ -521,7 +531,8 @@ func TestImageProcessor_AcceptsInputWithinLimit(t *testing.T) {
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
result, err := proc.Process(ctx, bytes.NewReader(input),
imagerequest.FormatJPEG, transform)
if err != nil {
t.Fatalf("Process() error = %v, want nil", err)
}
@@ -532,11 +543,11 @@ func TestImageProcessor_AcceptsInputWithinLimit(t *testing.T) {
func TestImageProcessor_EncodeAVIF(t *testing.T) {
t.Parallel()
encodeAndCheck(t, imagerequest.FormatAVIF, 85, mimeAVIF)
encodeAndCheck(t, imagerequest.FormatAVIF, 85, imagerequest.FormatAVIF.MIMEType())
}
// processAndDecode runs input through Process and decodes the output with
// vips, so a test can inspect the image a client would receive.
// processAndDecode runs input, a JPEG, through Process and decodes the output
// with vips, so a test can inspect the image a client would receive.
func processAndDecode(
t *testing.T, input []byte, transform *imagerequest.Transform,
) *vips.ImageRef {
@@ -546,7 +557,8 @@ func processAndDecode(
MaxInputBytes: 50 << 20,
MaxConcurrentProcessing: 1,
}).Process(
context.Background(), bytes.NewReader(input), transform,
context.Background(), bytes.NewReader(input), imagerequest.FormatJPEG,
transform,
)
if err != nil {
t.Fatalf("Process() error = %v", err)
@@ -25,7 +25,8 @@ func TestImageProcessor_EncodeJPEGXL(t *testing.T) {
MaxInputBytes: 50 << 20,
MaxConcurrentProcessing: 1,
}).Process(
t.Context(), bytes.NewReader(createTestJPEG(t, 200, 150)), transform,
t.Context(), bytes.NewReader(createTestJPEG(t, 200, 150)),
imagerequest.FormatJPEG, transform,
)
if err != nil {
t.Fatalf("Process() error = %v", err)
@@ -71,7 +72,7 @@ func TestImageProcessor_JPEGXLQuality(t *testing.T) {
MaxInputBytes: 50 << 20,
MaxConcurrentProcessing: 1,
}).Process(t.Context(),
bytes.NewReader(input),
bytes.NewReader(input), imagerequest.FormatJPEG,
&imagerequest.Transform{Format: imagerequest.FormatJXL, Quality: quality})
if err != nil {
t.Fatalf("Process() at quality %d error = %v", quality, err)
@@ -93,7 +93,8 @@ func processInBackground(
proc *ImageProcessor, reader *gatedReader, results chan<- error,
) {
go func() {
result, err := proc.Process(context.Background(), reader, smallJPEGTransform())
result, err := proc.Process(context.Background(), reader,
imagerequest.FormatJPEG, smallJPEGTransform())
if err == nil {
_ = result.Content.Close()
}
@@ -206,7 +207,7 @@ func TestProcessWaitsThenFailsWhenNoSlotFrees(t *testing.T) {
start := time.Now()
_, err := proc.Process(context.Background(), bytes.NewReader(input),
smallJPEGTransform())
imagerequest.FormatJPEG, smallJPEGTransform())
if !errors.Is(err, ErrTooManyImages) {
t.Fatalf("Process() error = %v, want ErrTooManyImages", err)
}
@@ -274,7 +275,8 @@ func TestProcessReleasesSlotOnError(t *testing.T) {
proc := New(Params{MaxInputBytes: 4096, MaxConcurrentProcessing: 1})
proc.processingWaitTimeout = 100 * time.Millisecond
_, err := proc.Process(context.Background(), tc.input, tc.transform)
_, err := proc.Process(context.Background(), tc.input,
imagerequest.FormatJPEG, tc.transform)
if err == nil || (tc.want != nil && !errors.Is(err, tc.want)) {
t.Fatalf("Process() error = %v, want %v", err, tc.want)
}
@@ -284,7 +286,7 @@ func TestProcessReleasesSlotOnError(t *testing.T) {
}
result, err := proc.Process(context.Background(), bytes.NewReader(valid),
smallJPEGTransform())
imagerequest.FormatJPEG, smallJPEGTransform())
if err != nil {
t.Fatalf("Process() after the error = %v, want nil", err)
}
@@ -10,8 +10,9 @@ import (
)
// TestImageProcessor_OrigOfUnnamedFormatRefused verifies that orig of a TIFF
// input, which is not a source format, is refused with
// ErrUnsupportedInputFormat before it is decoded, and not saved as JPEG.
// input is refused with ErrUnsupportedInputFormat before it is decoded, and
// not saved as JPEG. TIFF is not a source format, and the test passes the
// input to Process as a JPEG.
func TestImageProcessor_OrigOfUnnamedFormatRefused(t *testing.T) {
t.Parallel()
@@ -31,7 +32,7 @@ func TestImageProcessor_OrigOfUnnamedFormatRefused(t *testing.T) {
MaxInputBytes: 50 << 20,
MaxConcurrentProcessing: 1,
}).Process(
t.Context(), bytes.NewReader(tiff),
t.Context(), bytes.NewReader(tiff), imagerequest.FormatJPEG,
&imagerequest.Transform{Format: imagerequest.FormatOriginal},
)
if result != nil {
@@ -2,6 +2,7 @@ package imageprocessor
import (
"bytes"
"errors"
"io"
"testing"
@@ -21,7 +22,7 @@ func TestImageProcessor_LoadsEachSourceFormat(t *testing.T) {
imagerequest.FormatWebP, imagerequest.FormatAVIF, imagerequest.FormatJXL,
} {
saved, err := proc.Process(t.Context(),
bytes.NewReader(createTestPNG(t, 20, 20)),
bytes.NewReader(createTestPNG(t, 20, 20)), imagerequest.FormatPNG,
&imagerequest.Transform{Format: format})
if err != nil {
t.Fatalf("saving the test image as %s: %v", format, err)
@@ -32,7 +33,7 @@ func TestImageProcessor_LoadsEachSourceFormat(t *testing.T) {
t.Fatalf("reading the %s image: %v", format, err)
}
result, err := proc.Process(t.Context(), bytes.NewReader(source),
result, err := proc.Process(t.Context(), bytes.NewReader(source), format,
&imagerequest.Transform{Format: imagerequest.FormatOriginal})
if err != nil {
t.Errorf("orig of the %s image: %v", format, err)
@@ -42,9 +43,33 @@ func TestImageProcessor_LoadsEachSourceFormat(t *testing.T) {
t.Logf("orig of the %s image: %s", format, result.ContentType)
if result.ContentType != FormatToMIME(format) {
if result.ContentType != format.MIMEType() {
t.Errorf("orig of the %s image is %s, want %s",
format, result.ContentType, FormatToMIME(format))
format, result.ContentType, format.MIMEType())
}
}
}
// TestImageProcessor_NotASourceFormatRefused verifies that a source format
// other than the six, such as orig, auto or none, is refused with
// ErrUnsupportedInputFormat. The input is text, which libvips reads as no
// format.
func TestImageProcessor_NotASourceFormatRefused(t *testing.T) {
t.Parallel()
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
for _, sourceFormat := range []imagerequest.Format{
imagerequest.FormatOriginal, imagerequest.FormatAuto, "",
} {
result, err := proc.Process(t.Context(),
bytes.NewReader([]byte("this is text, not an image")), sourceFormat,
&imagerequest.Transform{Format: imagerequest.FormatJPEG})
t.Logf("source format %q: error = %v", sourceFormat, err)
if result != nil || !errors.Is(err, ErrUnsupportedInputFormat) {
t.Errorf("Process() with source format %q = %v, error %v; want %v",
sourceFormat, result, err, ErrUnsupportedInputFormat)
}
}
}