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
183 lines
4.7 KiB
Go
183 lines
4.7 KiB
Go
package imageprocessor
|
|
|
|
import (
|
|
"bytes"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"testing"
|
|
|
|
"github.com/davidbyttow/govips/v2/vips"
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
)
|
|
|
|
// processedSize runs input, an image in sourceFormat, through Process and
|
|
// returns the output's size in bytes.
|
|
func processedSize(
|
|
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), sourceFormat, transform)
|
|
if err != nil {
|
|
t.Fatalf("Process() error = %v", err)
|
|
}
|
|
|
|
_ = result.Content.Close()
|
|
|
|
return result.ContentLength
|
|
}
|
|
|
|
// encodePNG encodes img as PNG with Go's encoder.
|
|
func encodePNG(t *testing.T, img image.Image) []byte {
|
|
t.Helper()
|
|
|
|
var buf bytes.Buffer
|
|
|
|
err := png.Encode(&buf, img)
|
|
if err != nil {
|
|
t.Fatalf("failed to encode test PNG: %v", err)
|
|
}
|
|
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// decode decodes input with vips, as Process does.
|
|
func decode(t *testing.T, input []byte) *vips.ImageRef {
|
|
t.Helper()
|
|
|
|
img, err := vips.NewImageFromBuffer(input)
|
|
if err != nil {
|
|
t.Fatalf("failed to decode input: %v", err)
|
|
}
|
|
|
|
t.Cleanup(img.Close)
|
|
|
|
return img
|
|
}
|
|
|
|
// TestImageProcessor_PNGIsCompressed verifies that a PNG output is
|
|
// compressed: a flat 200x150 image, 90,000 bytes of raw pixels, comes out at
|
|
// a small fraction of that.
|
|
func TestImageProcessor_PNGIsCompressed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const width, height = 200, 150
|
|
|
|
flat := image.NewRGBA(image.Rect(0, 0, width, height))
|
|
for y := range height {
|
|
for x := range width {
|
|
flat.Set(x, y, color.RGBA{R: 40, G: 120, B: 200, A: 255})
|
|
}
|
|
}
|
|
|
|
size := processedSize(t, encodePNG(t, flat), imagerequest.FormatPNG,
|
|
&imagerequest.Transform{Format: imagerequest.FormatPNG})
|
|
|
|
const rawBytes = width * height * 3
|
|
if size > rawBytes/10 {
|
|
t.Errorf("PNG output is %d bytes, want under a tenth of its %d raw",
|
|
size, rawBytes)
|
|
}
|
|
}
|
|
|
|
// TestImageProcessor_WebPAtEffort2 verifies that WebP is saved at effort 2,
|
|
// not libvips' default of 4: the output is the size of the same image saved
|
|
// at effort 2.
|
|
func TestImageProcessor_WebPAtEffort2(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
input := createTestJPEG(t, 200, 150)
|
|
|
|
size := processedSize(t, input, imagerequest.FormatJPEG,
|
|
&imagerequest.Transform{Format: imagerequest.FormatWebP, Quality: 85})
|
|
|
|
want, _, err := decode(t, input).ExportWebp(&vips.WebpExportParams{
|
|
StripMetadata: true,
|
|
Quality: 85,
|
|
ReductionEffort: 2,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ExportWebp() error = %v", err)
|
|
}
|
|
|
|
if size != int64(len(want)) {
|
|
t.Errorf("WebP output is %d bytes, want %d, the size at effort 2",
|
|
size, len(want))
|
|
}
|
|
}
|
|
|
|
// TestImageProcessor_AVIFAtEffort1 verifies that AVIF is saved at effort 1
|
|
// with 8 bits per sample: the output is the size of the same image saved
|
|
// with those settings. The source is 16-bit, which libvips saves with 12
|
|
// bits when it is not given a bit depth, and 640x480: on a small image,
|
|
// such as 64x48, efforts 1 and 2 give the same output.
|
|
func TestImageProcessor_AVIFAtEffort1(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const width, height = 640, 480
|
|
|
|
source := image.NewRGBA64(image.Rect(0, 0, width, height))
|
|
for y := range height {
|
|
for x := range width {
|
|
source.Set(x, y, color.RGBA64{
|
|
R: uint16((x * 65535 / width) & 0xffff),
|
|
G: uint16((y * 65535 / height) & 0xffff),
|
|
B: 32768,
|
|
A: 65535,
|
|
})
|
|
}
|
|
}
|
|
|
|
input := encodePNG(t, source)
|
|
|
|
size := processedSize(t, input, imagerequest.FormatPNG,
|
|
&imagerequest.Transform{Format: imagerequest.FormatAVIF, Quality: 85})
|
|
|
|
want, _, err := decode(t, input).ExportAvif(&vips.AvifExportParams{
|
|
StripMetadata: true,
|
|
Quality: 85,
|
|
Effort: 1,
|
|
Bitdepth: 8,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ExportAvif() error = %v", err)
|
|
}
|
|
|
|
if size != int64(len(want)) {
|
|
t.Errorf("AVIF output is %d bytes, want %d, the size at effort 1 "+
|
|
"and 8 bits", size, len(want))
|
|
}
|
|
}
|
|
|
|
// TestImageProcessor_JPEGXLAtEffort4 verifies that JPEG XL is saved at
|
|
// effort 4, not libvips' default of 7: the output is the size of the same
|
|
// image saved at effort 4, with the distance exportJXL gives quality 85.
|
|
func TestImageProcessor_JPEGXLAtEffort4(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
input := createTestJPEG(t, 200, 150)
|
|
|
|
size := processedSize(t, input, imagerequest.FormatJPEG,
|
|
&imagerequest.Transform{Format: imagerequest.FormatJXL, Quality: 85})
|
|
|
|
params := vips.NewJxlExportParams()
|
|
params.Quality = 85
|
|
params.Distance = jxlDistance(85)
|
|
params.Effort = 4
|
|
|
|
want, _, err := decode(t, input).ExportJxl(params)
|
|
if err != nil {
|
|
t.Fatalf("ExportJxl() error = %v", err)
|
|
}
|
|
|
|
if size != int64(len(want)) {
|
|
t.Errorf("JPEG XL output is %d bytes, want %d, the size at effort 4",
|
|
size, len(want))
|
|
}
|
|
}
|