check / check (push) Canceled after 0s
The new package internal/imagerequest holds the request type, its two parts Source and Transform, the Format and Fit types, the dimension and fit checks, the /v1/image/ path parsing and the variant key; it imports no other package of pixa. imgcache.ParsedURL, imgcache.ImageRequest, signature.Request, imageprocessor.Request, the second Size, format and fit types and the conversions between them are gone. The image processor takes a Transform only. encurl keeps a private struct with the token's short CBOR field names. VariantKey moves with the function that makes it. Tests follow the moved and renamed code; the tests of the removed conversions go with them. Model: opus-5-5
179 lines
4.5 KiB
Go
179 lines
4.5 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 through Process and returns the output's size in
|
|
// bytes.
|
|
func processedSize(
|
|
t *testing.T, input []byte, transform *imagerequest.Transform,
|
|
) int64 {
|
|
t.Helper()
|
|
|
|
result, err := New(Params{}).Process(t.Context(), bytes.NewReader(input), 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.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.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.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.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))
|
|
}
|
|
}
|