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:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user