Files
pixa/internal/imgcache/processor_test.go
sneak 32f9166ece Add failing test for AVIF encoding
Test verifies that images can be encoded to AVIF format.
Currently fails because AVIF encoding is not implemented.
Removes the rejection test for AVIF output format.
2026-01-08 13:11:12 -08:00

501 lines
11 KiB
Go

package imgcache
import (
"bytes"
"context"
"image"
"image/color"
"image/jpeg"
"image/png"
"io"
"os"
"testing"
)
// createTestJPEG creates a simple test JPEG image.
func createTestJPEG(t *testing.T, width, height int) []byte {
t.Helper()
img := image.NewRGBA(image.Rect(0, 0, width, height))
// Fill with a gradient
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
img.Set(x, y, color.RGBA{
R: uint8(x * 255 / width),
G: uint8(y * 255 / height),
B: 128,
A: 255,
})
}
}
var buf bytes.Buffer
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90}); err != nil {
t.Fatalf("failed to encode test JPEG: %v", err)
}
return buf.Bytes()
}
// createTestPNG creates a simple test PNG image.
func createTestPNG(t *testing.T, width, height int) []byte {
t.Helper()
img := image.NewRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
img.Set(x, y, color.RGBA{
R: uint8(x * 255 / width),
G: uint8(y * 255 / height),
B: 128,
A: 255,
})
}
}
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
t.Fatalf("failed to encode test PNG: %v", err)
}
return buf.Bytes()
}
func TestImageProcessor_ResizeJPEG(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
input := createTestJPEG(t, 800, 600)
req := &ImageRequest{
Size: Size{Width: 400, Height: 300},
Format: FormatJPEG,
Quality: 85,
FitMode: FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
defer result.Content.Close()
if result.Width != 400 {
t.Errorf("Process() width = %d, want 400", result.Width)
}
if result.Height != 300 {
t.Errorf("Process() height = %d, want 300", result.Height)
}
if result.ContentLength == 0 {
t.Error("Process() returned zero content length")
}
// Verify it's valid JPEG by reading the content
data, err := io.ReadAll(result.Content)
if err != nil {
t.Fatalf("failed to read result: %v", err)
}
mime, err := DetectFormat(data)
if err != nil {
t.Fatalf("DetectFormat() error = %v", err)
}
if mime != MIMETypeJPEG {
t.Errorf("Output format = %v, want %v", mime, MIMETypeJPEG)
}
}
func TestImageProcessor_ConvertToPNG(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
input := createTestJPEG(t, 200, 150)
req := &ImageRequest{
Size: Size{Width: 200, Height: 150},
Format: FormatPNG,
FitMode: FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
defer result.Content.Close()
data, err := io.ReadAll(result.Content)
if err != nil {
t.Fatalf("failed to read result: %v", err)
}
mime, err := DetectFormat(data)
if err != nil {
t.Fatalf("DetectFormat() error = %v", err)
}
if mime != MIMETypePNG {
t.Errorf("Output format = %v, want %v", mime, MIMETypePNG)
}
}
func TestImageProcessor_OriginalSize(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
input := createTestJPEG(t, 640, 480)
req := &ImageRequest{
Size: Size{Width: 0, Height: 0}, // Original size
Format: FormatJPEG,
Quality: 85,
FitMode: FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
defer result.Content.Close()
if result.Width != 640 {
t.Errorf("Process() width = %d, want 640", result.Width)
}
if result.Height != 480 {
t.Errorf("Process() height = %d, want 480", result.Height)
}
}
func TestImageProcessor_FitContain(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
// 800x400 image (2:1 aspect) into 400x400 box with contain
// Should result in 400x200 (maintaining aspect ratio)
input := createTestJPEG(t, 800, 400)
req := &ImageRequest{
Size: Size{Width: 400, Height: 400},
Format: FormatJPEG,
Quality: 85,
FitMode: FitContain,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
defer result.Content.Close()
// With contain, the image should fit within the box
if result.Width > 400 || result.Height > 400 {
t.Errorf("Process() size %dx%d exceeds 400x400 box", result.Width, result.Height)
}
}
func TestImageProcessor_ProportionalScale_WidthOnly(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
// 800x600 image, request width=400 height=0
// Should scale proportionally to 400x300
input := createTestJPEG(t, 800, 600)
req := &ImageRequest{
Size: Size{Width: 400, Height: 0},
Format: FormatJPEG,
Quality: 85,
FitMode: FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
defer result.Content.Close()
if result.Width != 400 {
t.Errorf("Process() width = %d, want 400", result.Width)
}
if result.Height != 300 {
t.Errorf("Process() height = %d, want 300", result.Height)
}
}
func TestImageProcessor_ProportionalScale_HeightOnly(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
// 800x600 image, request width=0 height=300
// Should scale proportionally to 400x300
input := createTestJPEG(t, 800, 600)
req := &ImageRequest{
Size: Size{Width: 0, Height: 300},
Format: FormatJPEG,
Quality: 85,
FitMode: FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
defer result.Content.Close()
if result.Width != 400 {
t.Errorf("Process() width = %d, want 400", result.Width)
}
if result.Height != 300 {
t.Errorf("Process() height = %d, want 300", result.Height)
}
}
func TestImageProcessor_ProcessPNG(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
input := createTestPNG(t, 400, 300)
req := &ImageRequest{
Size: Size{Width: 200, Height: 150},
Format: FormatPNG,
FitMode: FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
defer result.Content.Close()
if result.Width != 200 {
t.Errorf("Process() width = %d, want 200", result.Width)
}
if result.Height != 150 {
t.Errorf("Process() height = %d, want 150", result.Height)
}
}
func TestImageProcessor_ImplementsInterface(t *testing.T) {
// Verify ImageProcessor implements Processor interface
var _ Processor = (*ImageProcessor)(nil)
}
func TestImageProcessor_SupportedFormats(t *testing.T) {
proc := NewImageProcessor()
inputFormats := proc.SupportedInputFormats()
if len(inputFormats) == 0 {
t.Error("SupportedInputFormats() returned empty slice")
}
outputFormats := proc.SupportedOutputFormats()
if len(outputFormats) == 0 {
t.Error("SupportedOutputFormats() returned empty slice")
}
}
func TestImageProcessor_RejectsOversizedInput(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
// Create an image that exceeds MaxInputDimension (e.g., 10000x100)
// This should be rejected before processing to prevent DoS
input := createTestJPEG(t, 10000, 100)
req := &ImageRequest{
Size: Size{Width: 100, Height: 100},
Format: FormatJPEG,
Quality: 85,
FitMode: FitCover,
}
_, err := proc.Process(ctx, bytes.NewReader(input), req)
if err == nil {
t.Error("Process() should reject oversized input images")
}
if err != ErrInputTooLarge {
t.Errorf("Process() error = %v, want ErrInputTooLarge", err)
}
}
func TestImageProcessor_RejectsOversizedInputHeight(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
// Create an image with oversized height
input := createTestJPEG(t, 100, 10000)
req := &ImageRequest{
Size: Size{Width: 100, Height: 100},
Format: FormatJPEG,
Quality: 85,
FitMode: FitCover,
}
_, err := proc.Process(ctx, bytes.NewReader(input), req)
if err == nil {
t.Error("Process() should reject oversized input images")
}
if err != ErrInputTooLarge {
t.Errorf("Process() error = %v, want ErrInputTooLarge", err)
}
}
func TestImageProcessor_AcceptsMaxDimensionInput(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
// Create an image at exactly MaxInputDimension - should be accepted
// Using smaller dimensions to keep test fast
input := createTestJPEG(t, MaxInputDimension, 100)
req := &ImageRequest{
Size: Size{Width: 100, Height: 100},
Format: FormatJPEG,
Quality: 85,
FitMode: FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
if err != nil {
t.Fatalf("Process() should accept images at MaxInputDimension, got error: %v", err)
}
defer result.Content.Close()
}
func TestImageProcessor_EncodeWebP(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
input := createTestJPEG(t, 200, 150)
req := &ImageRequest{
Size: Size{Width: 100, Height: 75},
Format: FormatWebP,
Quality: 80,
FitMode: FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
if err != nil {
t.Fatalf("Process() error = %v, want nil", err)
}
defer result.Content.Close()
// Verify output is valid WebP
data, err := io.ReadAll(result.Content)
if err != nil {
t.Fatalf("failed to read result: %v", err)
}
mime, err := DetectFormat(data)
if err != nil {
t.Fatalf("DetectFormat() error = %v", err)
}
if mime != MIMETypeWebP {
t.Errorf("Output format = %v, want %v", mime, MIMETypeWebP)
}
// Verify dimensions
if result.Width != 100 {
t.Errorf("Width = %d, want 100", result.Width)
}
if result.Height != 75 {
t.Errorf("Height = %d, want 75", result.Height)
}
}
func TestImageProcessor_DecodeAVIF(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
// Load test AVIF file
input, err := os.ReadFile("testdata/red.avif")
if err != nil {
t.Fatalf("failed to read test AVIF: %v", err)
}
// Request resize and convert to JPEG
req := &ImageRequest{
Size: Size{Width: 2, Height: 2},
Format: FormatJPEG,
Quality: 85,
FitMode: FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
if err != nil {
t.Fatalf("Process() error = %v, want nil (AVIF decoding should work)", err)
}
defer result.Content.Close()
// Verify output is valid JPEG
data, err := io.ReadAll(result.Content)
if err != nil {
t.Fatalf("failed to read result: %v", err)
}
mime, err := DetectFormat(data)
if err != nil {
t.Fatalf("DetectFormat() error = %v", err)
}
if mime != MIMETypeJPEG {
t.Errorf("Output format = %v, want %v", mime, MIMETypeJPEG)
}
}
func TestImageProcessor_EncodeAVIF(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
input := createTestJPEG(t, 200, 150)
req := &ImageRequest{
Size: Size{Width: 100, Height: 75},
Format: FormatAVIF,
Quality: 85,
FitMode: FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
if err != nil {
t.Fatalf("Process() error = %v, want nil (AVIF encoding should work)", err)
}
defer result.Content.Close()
// Verify output is valid AVIF
data, err := io.ReadAll(result.Content)
if err != nil {
t.Fatalf("failed to read result: %v", err)
}
mime, err := DetectFormat(data)
if err != nil {
t.Fatalf("DetectFormat() error = %v", err)
}
if mime != MIMETypeAVIF {
t.Errorf("Output format = %v, want %v", mime, MIMETypeAVIF)
}
// Verify dimensions
if result.Width != 100 {
t.Errorf("Width = %d, want 100", result.Width)
}
if result.Height != 75 {
t.Errorf("Height = %d, want 75", result.Height)
}
}