One image request type instead of five (closes #307)
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 CBOR field names, so tokens made before still decode. 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
This commit is contained in:
2026-10-21 12:27:29 +00:00
parent 385c0ca76f
commit fece3b69c9
68 changed files with 2217 additions and 1978 deletions
@@ -15,6 +15,7 @@ import (
"testing"
"github.com/davidbyttow/govips/v2/vips"
"sneak.berlin/go/pixa/internal/imagerequest"
)
func TestMain(m *testing.M) {
@@ -87,7 +88,7 @@ func isAVIF(data []byte) bool {
brand := string(data[8:12])
return brand == string(FormatAVIF) || brand == "avis"
return brand == string(imagerequest.FormatAVIF) || brand == "avis"
}
// detectMIME is a minimal magic-byte detector for test assertions.
@@ -123,14 +124,14 @@ func TestImageProcessor_ResizeJPEG(t *testing.T) {
input := createTestJPEG(t, 800, 600)
req := &Request{
Size: Size{Width: 400, Height: 300},
Format: FormatJPEG,
transform := &imagerequest.Transform{
Size: imagerequest.Size{Width: 400, Height: 300},
Format: imagerequest.FormatJPEG,
Quality: 85,
FitMode: FitCover,
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
@@ -169,13 +170,13 @@ func TestImageProcessor_ConvertToPNG(t *testing.T) {
input := createTestJPEG(t, 200, 150)
req := &Request{
Size: Size{Width: 200, Height: 150},
Format: FormatPNG,
FitMode: FitCover,
transform := &imagerequest.Transform{
Size: imagerequest.Size{Width: 200, Height: 150},
Format: imagerequest.FormatPNG,
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
@@ -196,7 +197,7 @@ func TestImageProcessor_ConvertToPNG(t *testing.T) {
// processAndCheckSize processes a test JPEG of the given input dimensions
// with the requested size and asserts the resulting dimensions.
func processAndCheckSize(
t *testing.T, inputW, inputH int, size Size, wantW, wantH int,
t *testing.T, inputW, inputH int, size imagerequest.Size, wantW, wantH int,
) {
t.Helper()
@@ -205,14 +206,14 @@ func processAndCheckSize(
input := createTestJPEG(t, inputW, inputH)
req := &Request{
transform := &imagerequest.Transform{
Size: size,
Format: FormatJPEG,
Format: imagerequest.FormatJPEG,
Quality: 85,
FitMode: FitCover,
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
@@ -232,7 +233,7 @@ func TestImageProcessor_OriginalSize(t *testing.T) {
t.Parallel()
// Width and height 0: keep original size
processAndCheckSize(t, 640, 480, Size{Width: 0, Height: 0}, 640, 480)
processAndCheckSize(t, 640, 480, imagerequest.Size{Width: 0, Height: 0}, 640, 480)
}
func TestImageProcessor_FitContain(t *testing.T) {
@@ -245,14 +246,14 @@ func TestImageProcessor_FitContain(t *testing.T) {
// Should result in 400x200 (maintaining aspect ratio)
input := createTestJPEG(t, 800, 400)
req := &Request{
Size: Size{Width: 400, Height: 400},
Format: FormatJPEG,
transform := &imagerequest.Transform{
Size: imagerequest.Size{Width: 400, Height: 400},
Format: imagerequest.FormatJPEG,
Quality: 85,
FitMode: FitContain,
FitMode: imagerequest.FitContain,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
@@ -270,7 +271,7 @@ func TestImageProcessor_ProportionalScale_WidthOnly(t *testing.T) {
// 800x600 image, request width=400 height=0
// Should scale proportionally to 400x300
processAndCheckSize(t, 800, 600, Size{Width: 400, Height: 0}, 400, 300)
processAndCheckSize(t, 800, 600, imagerequest.Size{Width: 400, Height: 0}, 400, 300)
}
func TestImageProcessor_ProportionalScale_HeightOnly(t *testing.T) {
@@ -278,7 +279,7 @@ func TestImageProcessor_ProportionalScale_HeightOnly(t *testing.T) {
// 800x600 image, request width=0 height=300
// Should scale proportionally to 400x300
processAndCheckSize(t, 800, 600, Size{Width: 0, Height: 300}, 400, 300)
processAndCheckSize(t, 800, 600, imagerequest.Size{Width: 0, Height: 300}, 400, 300)
}
func TestImageProcessor_ProcessPNG(t *testing.T) {
@@ -289,13 +290,13 @@ func TestImageProcessor_ProcessPNG(t *testing.T) {
input := createTestPNG(t, 400, 300)
req := &Request{
Size: Size{Width: 200, Height: 150},
Format: FormatPNG,
FitMode: FitCover,
transform := &imagerequest.Transform{
Size: imagerequest.Size{Width: 200, Height: 150},
Format: imagerequest.FormatPNG,
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
@@ -349,14 +350,14 @@ func TestImageProcessor_RejectsOversizedInput(t *testing.T) {
ctx := context.Background()
input := createTestJPEG(t, tt.width, tt.height)
req := &Request{
Size: Size{Width: 100, Height: 100},
Format: FormatJPEG,
transform := &imagerequest.Transform{
Size: imagerequest.Size{Width: 100, Height: 100},
Format: imagerequest.FormatJPEG,
Quality: 85,
FitMode: FitCover,
FitMode: imagerequest.FitCover,
}
_, err := proc.Process(ctx, bytes.NewReader(input), req)
_, err := proc.Process(ctx, bytes.NewReader(input), transform)
if err == nil {
t.Error("Process() should reject oversized input images")
}
@@ -377,14 +378,14 @@ func TestImageProcessor_AcceptsMaxDimensionInput(t *testing.T) {
// Create an image at exactly MaxInputDimension - should be accepted
input := createTestJPEG(t, MaxInputDimension, 100)
req := &Request{
Size: Size{Width: 100, Height: 100},
Format: FormatJPEG,
transform := &imagerequest.Transform{
Size: imagerequest.Size{Width: 100, Height: 100},
Format: imagerequest.FormatJPEG,
Quality: 85,
FitMode: FitCover,
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
if err != nil {
t.Fatalf(
"Process() should accept images at MaxInputDimension, got error: %v",
@@ -397,7 +398,9 @@ func TestImageProcessor_AcceptsMaxDimensionInput(t *testing.T) {
// encodeAndCheck processes a 200x150 test JPEG into a 100x75 output of the
// given format and asserts the output MIME type and dimensions.
func encodeAndCheck(t *testing.T, format Format, quality int, wantMIME string) {
func encodeAndCheck(
t *testing.T, format imagerequest.Format, quality int, wantMIME string,
) {
t.Helper()
proc := New(Params{})
@@ -405,14 +408,14 @@ func encodeAndCheck(t *testing.T, format Format, quality int, wantMIME string) {
input := createTestJPEG(t, 200, 150)
req := &Request{
Size: Size{Width: 100, Height: 75},
transform := &imagerequest.Transform{
Size: imagerequest.Size{Width: 100, Height: 75},
Format: format,
Quality: quality,
FitMode: FitCover,
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
if err != nil {
t.Fatalf("Process() error = %v, want nil", err)
}
@@ -443,7 +446,7 @@ func encodeAndCheck(t *testing.T, format Format, quality int, wantMIME string) {
func TestImageProcessor_EncodeWebP(t *testing.T) {
t.Parallel()
encodeAndCheck(t, FormatWebP, 80, mimeWebP)
encodeAndCheck(t, imagerequest.FormatWebP, 80, mimeWebP)
}
func TestImageProcessor_DecodeAVIF(t *testing.T) {
@@ -459,14 +462,14 @@ func TestImageProcessor_DecodeAVIF(t *testing.T) {
}
// Request resize and convert to JPEG
req := &Request{
Size: Size{Width: 2, Height: 2},
Format: FormatJPEG,
transform := &imagerequest.Transform{
Size: imagerequest.Size{Width: 2, Height: 2},
Format: imagerequest.FormatJPEG,
Quality: 85,
FitMode: FitCover,
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
if err != nil {
t.Fatalf("Process() error = %v, want nil (AVIF decoding should work)", err)
}
@@ -500,14 +503,14 @@ func TestImageProcessor_RejectsOversizedInputData(t *testing.T) {
t.Fatalf("test JPEG must exceed %d bytes, got %d", limit, len(input))
}
req := &Request{
Size: Size{Width: 100, Height: 75},
Format: FormatJPEG,
transform := &imagerequest.Transform{
Size: imagerequest.Size{Width: 100, Height: 75},
Format: imagerequest.FormatJPEG,
Quality: 85,
FitMode: FitCover,
FitMode: imagerequest.FitCover,
}
_, err := proc.Process(ctx, bytes.NewReader(input), req)
_, err := proc.Process(ctx, bytes.NewReader(input), transform)
if err == nil {
t.Fatal("Process() should reject input exceeding maxInputBytes")
}
@@ -527,14 +530,14 @@ func TestImageProcessor_AcceptsInputWithinLimit(t *testing.T) {
proc := New(Params{MaxInputBytes: limit})
ctx := context.Background()
req := &Request{
Size: Size{Width: 10, Height: 10},
Format: FormatJPEG,
transform := &imagerequest.Transform{
Size: imagerequest.Size{Width: 10, Height: 10},
Format: imagerequest.FormatJPEG,
Quality: 85,
FitMode: FitCover,
FitMode: imagerequest.FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
if err != nil {
t.Fatalf("Process() error = %v, want nil", err)
}
@@ -561,16 +564,18 @@ func TestImageProcessor_DefaultMaxInputBytes(t *testing.T) {
func TestImageProcessor_EncodeAVIF(t *testing.T) {
t.Parallel()
encodeAndCheck(t, FormatAVIF, 85, mimeAVIF)
encodeAndCheck(t, imagerequest.FormatAVIF, 85, mimeAVIF)
}
// processAndDecode runs input 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, req *Request) *vips.ImageRef {
func processAndDecode(
t *testing.T, input []byte, transform *imagerequest.Transform,
) *vips.ImageRef {
t.Helper()
result, err := New(Params{}).Process(
context.Background(), bytes.NewReader(input), req,
context.Background(), bytes.NewReader(input), transform,
)
if err != nil {
t.Fatalf("Process() error = %v", err)
@@ -614,15 +619,17 @@ func TestImageProcessor_StripsEXIF(t *testing.T) {
t.Fatal("testdata/gps-exif.jpg has no GPS latitude")
}
formats := []Format{
FormatJPEG, FormatPNG, FormatWebP, FormatAVIF, FormatGIF, FormatOriginal,
formats := []imagerequest.Format{
imagerequest.FormatJPEG, imagerequest.FormatPNG, imagerequest.FormatWebP,
imagerequest.FormatAVIF, imagerequest.FormatGIF, imagerequest.FormatOriginal,
}
for _, format := range formats {
t.Run(string(format), func(t *testing.T) {
t.Parallel()
output := processAndDecode(t, input, &Request{Format: format})
output := processAndDecode(t, input,
&imagerequest.Transform{Format: format})
if output.HasExif() {
t.Errorf("output has EXIF: %v", output.GetExif())
@@ -644,21 +651,21 @@ func TestImageProcessor_AppliesEXIFOrientation(t *testing.T) {
tests := []struct {
name string
size Size
size imagerequest.Size
wantW int
wantH int
}{
{name: "original size", size: Size{}, wantW: 8, wantH: 16},
{name: "width only", size: Size{Width: 4}, wantW: 4, wantH: 8},
{name: "original size", size: imagerequest.Size{}, wantW: 8, wantH: 16},
{name: "width only", size: imagerequest.Size{Width: 4}, wantW: 4, wantH: 8},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
output := processAndDecode(t, input, &Request{
output := processAndDecode(t, input, &imagerequest.Transform{
Size: tt.size,
Format: FormatPNG,
Format: imagerequest.FormatPNG,
})
if output.Width() != tt.wantW || output.Height() != tt.wantH {
@@ -694,7 +701,8 @@ func TestImageProcessor_ConvertsWideGamutToSRGB(t *testing.T) {
t.Fatalf("failed to read test JPEG: %v", err)
}
output := processAndDecode(t, input, &Request{Format: FormatPNG})
output := processAndDecode(t, input,
&imagerequest.Transform{Format: imagerequest.FormatPNG})
if output.HasICCProfile() {
t.Error("output has an ICC profile")