Add pure Go image processor with resize and format conversion
Implements the Processor interface using disintegration/imaging library. Supports JPEG, PNG, GIF, WebP decoding and JPEG, PNG, GIF encoding. Includes all fit modes: cover, contain, fill, inside, outside.
This commit is contained in:
242
internal/imgcache/processor_test.go
Normal file
242
internal/imgcache/processor_test.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package imgcache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"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_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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user