chore: update golangci-lint to v2.12.2 with canonical config
All checks were successful
check / check (push) Successful in 2m3s
All checks were successful
check / check (push) Successful in 2m3s
Replace .golangci.yml with the canonical v2-schema config (default: all minus six disabled linters, lll 88, tests included) and bump every golangci-lint pin to v2.12.2: - Dockerfile: golangci/golangci-lint:v2.12.2-alpine (hash-pinned) - script/bootstrap: GOLANGCI_LINT_VERSION 2.12.2 with new linux-amd64/arm64 release-archive sha256 pins Fix all 747 findings the stricter config surfaces, with no behavior changes: t.Parallel() throughout the test suite, static sentinel errors and errors.Is comparisons, checked error returns, context propagation (contextcheck/noctx), 88-column wrapping, extracted constants and helpers for goconst/dupl/funlen/cyclop, exhaustive switch cases replicating existing defaults, and white-box test files renamed to *_internal_test.go for testpackage. Three nolint:tagliatelle directives preserve the existing snake_case JSON wire and on-disk metadata formats.
This commit is contained in:
@@ -13,7 +13,9 @@ import (
|
||||
)
|
||||
|
||||
// vipsOnce ensures vips is initialized exactly once.
|
||||
var vipsOnce sync.Once //nolint:gochecknoglobals // package-level sync.Once for one-time vips init
|
||||
//
|
||||
//nolint:gochecknoglobals // package-level sync.Once for one-time vips init
|
||||
var vipsOnce sync.Once
|
||||
|
||||
// initVips initializes libvips with quiet logging.
|
||||
func initVips() {
|
||||
@@ -96,10 +98,12 @@ const DefaultMaxInputBytes = 50 << 20
|
||||
// ErrInputTooLarge is returned when input image dimensions exceed MaxInputDimension.
|
||||
var ErrInputTooLarge = errors.New("input image dimensions exceed maximum")
|
||||
|
||||
// ErrInputDataTooLarge is returned when the raw input data exceeds the configured byte limit.
|
||||
// ErrInputDataTooLarge is returned when the raw input data exceeds the
|
||||
// configured byte limit.
|
||||
var ErrInputDataTooLarge = errors.New("input data exceeds maximum allowed size")
|
||||
|
||||
// ErrUnsupportedOutputFormat is returned when the requested output format is not supported.
|
||||
// ErrUnsupportedOutputFormat is returned when the requested output format is
|
||||
// not supported.
|
||||
var ErrUnsupportedOutputFormat = errors.New("unsupported output format")
|
||||
|
||||
// ImageProcessor implements image transformation using libvips via govips.
|
||||
@@ -170,25 +174,12 @@ func (p *ImageProcessor) Process(
|
||||
}
|
||||
|
||||
// Determine target dimensions
|
||||
targetWidth := req.Size.Width
|
||||
targetHeight := req.Size.Height
|
||||
|
||||
// Handle dimension calculation
|
||||
if targetWidth == 0 && targetHeight == 0 {
|
||||
// Both are 0: keep original size
|
||||
targetWidth = origWidth
|
||||
targetHeight = origHeight
|
||||
} else if targetWidth == 0 {
|
||||
// Only height specified: calculate width proportionally
|
||||
targetWidth = origWidth * targetHeight / origHeight
|
||||
} else if targetHeight == 0 {
|
||||
// Only width specified: calculate height proportionally
|
||||
targetHeight = origHeight * targetWidth / origWidth
|
||||
}
|
||||
targetWidth, targetHeight := targetDimensions(req.Size, origWidth, origHeight)
|
||||
|
||||
// Resize if needed
|
||||
if targetWidth != origWidth || targetHeight != origHeight {
|
||||
if err := p.resize(img, targetWidth, targetHeight, req.FitMode); err != nil {
|
||||
err := p.resize(img, targetWidth, targetHeight, req.FitMode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resize: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -217,14 +208,42 @@ func (p *ImageProcessor) Process(
|
||||
}, nil
|
||||
}
|
||||
|
||||
// targetDimensions calculates the output dimensions for a requested size,
|
||||
// scaling proportionally when only one dimension is given and keeping the
|
||||
// original dimensions when both are zero.
|
||||
func targetDimensions(size Size, origWidth, origHeight int) (int, int) {
|
||||
switch {
|
||||
case size.Width == 0 && size.Height == 0:
|
||||
// Both are 0: keep original size
|
||||
return origWidth, origHeight
|
||||
case size.Width == 0:
|
||||
// Only height specified: calculate width proportionally
|
||||
return origWidth * size.Height / origHeight, size.Height
|
||||
case size.Height == 0:
|
||||
// Only width specified: calculate height proportionally
|
||||
return size.Width, origHeight * size.Width / origWidth
|
||||
default:
|
||||
return size.Width, size.Height
|
||||
}
|
||||
}
|
||||
|
||||
// MIME types for the supported image formats.
|
||||
const (
|
||||
mimeJPEG = "image/jpeg"
|
||||
mimePNG = "image/png"
|
||||
mimeGIF = "image/gif"
|
||||
mimeWebP = "image/webp"
|
||||
mimeAVIF = "image/avif"
|
||||
)
|
||||
|
||||
// SupportedInputFormats returns MIME types this processor can read.
|
||||
func (p *ImageProcessor) SupportedInputFormats() []string {
|
||||
return []string{
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/avif",
|
||||
mimeJPEG,
|
||||
mimePNG,
|
||||
mimeGIF,
|
||||
mimeWebP,
|
||||
mimeAVIF,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,15 +262,17 @@ func (p *ImageProcessor) SupportedOutputFormats() []Format {
|
||||
func FormatToMIME(format Format) string {
|
||||
switch format {
|
||||
case FormatJPEG:
|
||||
return "image/jpeg"
|
||||
return mimeJPEG
|
||||
case FormatPNG:
|
||||
return "image/png"
|
||||
return mimePNG
|
||||
case FormatWebP:
|
||||
return "image/webp"
|
||||
return mimeWebP
|
||||
case FormatGIF:
|
||||
return "image/gif"
|
||||
return mimeGIF
|
||||
case FormatAVIF:
|
||||
return "image/avif"
|
||||
return mimeAVIF
|
||||
case FormatOriginal:
|
||||
return "application/octet-stream"
|
||||
default:
|
||||
return "application/octet-stream"
|
||||
}
|
||||
@@ -270,14 +291,20 @@ func (p *ImageProcessor) detectFormat(img *vips.ImageRef) string {
|
||||
case vips.ImageTypeWEBP:
|
||||
return "webp"
|
||||
case vips.ImageTypeAVIF, vips.ImageTypeHEIF:
|
||||
return "avif"
|
||||
return string(FormatAVIF)
|
||||
case vips.ImageTypeUnknown, vips.ImageTypeMagick, vips.ImageTypePDF,
|
||||
vips.ImageTypeSVG, vips.ImageTypeTIFF, vips.ImageTypeBMP,
|
||||
vips.ImageTypeJP2K, vips.ImageTypeJXL:
|
||||
return "unknown"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// resize resizes the image according to the fit mode.
|
||||
func (p *ImageProcessor) resize(img *vips.ImageRef, width, height int, fit FitMode) error {
|
||||
func (p *ImageProcessor) resize(
|
||||
img *vips.ImageRef, width, height int, fit FitMode,
|
||||
) error {
|
||||
switch fit {
|
||||
case FitCover, "":
|
||||
// Resize and crop to fill exact dimensions (default)
|
||||
@@ -303,6 +330,7 @@ func (p *ImageProcessor) resize(img *vips.ImageRef, width, height int, fit FitMo
|
||||
if img.Width() <= width && img.Height() <= height {
|
||||
return nil // Already fits
|
||||
}
|
||||
|
||||
imgW, imgH := img.Width(), img.Height()
|
||||
scaleW := float64(width) / float64(imgW)
|
||||
scaleH := float64(height) / float64(imgH)
|
||||
@@ -331,7 +359,9 @@ func (p *ImageProcessor) resize(img *vips.ImageRef, width, height int, fit FitMo
|
||||
const defaultQuality = 85
|
||||
|
||||
// encode encodes an image to the specified format.
|
||||
func (p *ImageProcessor) encode(img *vips.ImageRef, format Format, quality int) ([]byte, error) {
|
||||
func (p *ImageProcessor) encode(
|
||||
img *vips.ImageRef, format Format, quality int,
|
||||
) ([]byte, error) {
|
||||
if quality <= 0 {
|
||||
quality = defaultQuality
|
||||
}
|
||||
@@ -367,8 +397,11 @@ func (p *ImageProcessor) encode(img *vips.ImageRef, format Format, quality int)
|
||||
Quality: quality,
|
||||
}
|
||||
|
||||
case FormatOriginal:
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnsupportedOutputFormat, format)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported output format: %s", format)
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnsupportedOutputFormat, format)
|
||||
}
|
||||
|
||||
output, _, err := img.Export(¶ms)
|
||||
@@ -390,7 +423,7 @@ func (p *ImageProcessor) formatFromString(format string) Format {
|
||||
return FormatGIF
|
||||
case "webp":
|
||||
return FormatWebP
|
||||
case "avif":
|
||||
case string(FormatAVIF):
|
||||
return FormatAVIF
|
||||
default:
|
||||
return FormatJPEG
|
||||
|
||||
@@ -3,6 +3,7 @@ package imageprocessor
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
@@ -16,7 +17,9 @@ import (
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
initVips()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
vips.Shutdown()
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -27,11 +30,11 @@ func createTestJPEG(t *testing.T, width, height int) []byte {
|
||||
|
||||
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++ {
|
||||
for y := range height {
|
||||
for x := range width {
|
||||
img.Set(x, y, color.RGBA{
|
||||
R: uint8(x * 255 / width),
|
||||
G: uint8(y * 255 / height),
|
||||
R: uint8((x * 255 / width) & 0xff),
|
||||
G: uint8((y * 255 / height) & 0xff),
|
||||
B: 128,
|
||||
A: 255,
|
||||
})
|
||||
@@ -39,7 +42,9 @@ func createTestJPEG(t *testing.T, width, height int) []byte {
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90}); err != nil {
|
||||
|
||||
err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode test JPEG: %v", err)
|
||||
}
|
||||
|
||||
@@ -51,11 +56,11 @@ 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++ {
|
||||
for y := range height {
|
||||
for x := range width {
|
||||
img.Set(x, y, color.RGBA{
|
||||
R: uint8(x * 255 / width),
|
||||
G: uint8(y * 255 / height),
|
||||
R: uint8((x * 255 / width) & 0xff),
|
||||
G: uint8((y * 255 / height) & 0xff),
|
||||
B: 128,
|
||||
A: 255,
|
||||
})
|
||||
@@ -63,37 +68,54 @@ func createTestPNG(t *testing.T, width, height int) []byte {
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
|
||||
err := png.Encode(&buf, img)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode test PNG: %v", err)
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// isAVIF reports whether data starts with an AVIF ftyp box.
|
||||
func isAVIF(data []byte) bool {
|
||||
if len(data) < 12 || string(data[4:8]) != "ftyp" {
|
||||
return false
|
||||
}
|
||||
|
||||
brand := string(data[8:12])
|
||||
|
||||
return brand == string(FormatAVIF) || brand == "avis"
|
||||
}
|
||||
|
||||
// detectMIME is a minimal magic-byte detector for test assertions.
|
||||
func detectMIME(data []byte) string {
|
||||
if len(data) >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
|
||||
return "image/jpeg"
|
||||
return mimeJPEG
|
||||
}
|
||||
|
||||
if len(data) >= 8 && string(data[:8]) == "\x89PNG\r\n\x1a\n" {
|
||||
return "image/png"
|
||||
return mimePNG
|
||||
}
|
||||
|
||||
if len(data) >= 4 && string(data[:4]) == "GIF8" {
|
||||
return "image/gif"
|
||||
return mimeGIF
|
||||
}
|
||||
|
||||
if len(data) >= 12 && string(data[:4]) == "RIFF" && string(data[8:12]) == "WEBP" {
|
||||
return "image/webp"
|
||||
return mimeWebP
|
||||
}
|
||||
if len(data) >= 12 && string(data[4:8]) == "ftyp" {
|
||||
brand := string(data[8:12])
|
||||
if brand == "avif" || brand == "avis" {
|
||||
return "image/avif"
|
||||
}
|
||||
|
||||
if isAVIF(data) {
|
||||
return mimeAVIF
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestImageProcessor_ResizeJPEG(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
proc := New(Params{})
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -110,7 +132,8 @@ func TestImageProcessor_ResizeJPEG(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Process() error = %v", err)
|
||||
}
|
||||
defer result.Content.Close()
|
||||
|
||||
defer func() { _ = result.Content.Close() }()
|
||||
|
||||
if result.Width != 400 {
|
||||
t.Errorf("Process() width = %d, want 400", result.Width)
|
||||
@@ -131,12 +154,14 @@ func TestImageProcessor_ResizeJPEG(t *testing.T) {
|
||||
}
|
||||
|
||||
mime := detectMIME(data)
|
||||
if mime != "image/jpeg" {
|
||||
if mime != mimeJPEG {
|
||||
t.Errorf("Output format = %v, want image/jpeg", mime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageProcessor_ConvertToPNG(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
proc := New(Params{})
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -152,7 +177,8 @@ func TestImageProcessor_ConvertToPNG(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Process() error = %v", err)
|
||||
}
|
||||
defer result.Content.Close()
|
||||
|
||||
defer func() { _ = result.Content.Close() }()
|
||||
|
||||
data, err := io.ReadAll(result.Content)
|
||||
if err != nil {
|
||||
@@ -160,19 +186,25 @@ func TestImageProcessor_ConvertToPNG(t *testing.T) {
|
||||
}
|
||||
|
||||
mime := detectMIME(data)
|
||||
if mime != "image/png" {
|
||||
if mime != mimePNG {
|
||||
t.Errorf("Output format = %v, want image/png", mime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageProcessor_OriginalSize(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.Helper()
|
||||
|
||||
proc := New(Params{})
|
||||
ctx := context.Background()
|
||||
|
||||
input := createTestJPEG(t, 640, 480)
|
||||
input := createTestJPEG(t, inputW, inputH)
|
||||
|
||||
req := &Request{
|
||||
Size: Size{Width: 0, Height: 0}, // Original size
|
||||
Size: size,
|
||||
Format: FormatJPEG,
|
||||
Quality: 85,
|
||||
FitMode: FitCover,
|
||||
@@ -182,18 +214,28 @@ func TestImageProcessor_OriginalSize(t *testing.T) {
|
||||
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)
|
||||
defer func() { _ = result.Content.Close() }()
|
||||
|
||||
if result.Width != wantW {
|
||||
t.Errorf("Process() width = %d, want %d", result.Width, wantW)
|
||||
}
|
||||
|
||||
if result.Height != 480 {
|
||||
t.Errorf("Process() height = %d, want 480", result.Height)
|
||||
if result.Height != wantH {
|
||||
t.Errorf("Process() height = %d, want %d", result.Height, wantH)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func TestImageProcessor_FitContain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
proc := New(Params{})
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -212,7 +254,8 @@ func TestImageProcessor_FitContain(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Process() error = %v", err)
|
||||
}
|
||||
defer result.Content.Close()
|
||||
|
||||
defer func() { _ = result.Content.Close() }()
|
||||
|
||||
// With contain, the image should fit within the box
|
||||
if result.Width > 400 || result.Height > 400 {
|
||||
@@ -221,66 +264,24 @@ func TestImageProcessor_FitContain(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImageProcessor_ProportionalScale_WidthOnly(t *testing.T) {
|
||||
proc := New(Params{})
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
|
||||
// 800x600 image, request width=400 height=0
|
||||
// Should scale proportionally to 400x300
|
||||
input := createTestJPEG(t, 800, 600)
|
||||
|
||||
req := &Request{
|
||||
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)
|
||||
}
|
||||
processAndCheckSize(t, 800, 600, Size{Width: 400, Height: 0}, 400, 300)
|
||||
}
|
||||
|
||||
func TestImageProcessor_ProportionalScale_HeightOnly(t *testing.T) {
|
||||
proc := New(Params{})
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
|
||||
// 800x600 image, request width=0 height=300
|
||||
// Should scale proportionally to 400x300
|
||||
input := createTestJPEG(t, 800, 600)
|
||||
|
||||
req := &Request{
|
||||
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)
|
||||
}
|
||||
processAndCheckSize(t, 800, 600, Size{Width: 0, Height: 300}, 400, 300)
|
||||
}
|
||||
|
||||
func TestImageProcessor_ProcessPNG(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
proc := New(Params{})
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -296,7 +297,8 @@ func TestImageProcessor_ProcessPNG(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Process() error = %v", err)
|
||||
}
|
||||
defer result.Content.Close()
|
||||
|
||||
defer func() { _ = result.Content.Close() }()
|
||||
|
||||
if result.Width != 200 {
|
||||
t.Errorf("Process() width = %d, want 200", result.Width)
|
||||
@@ -308,6 +310,8 @@ func TestImageProcessor_ProcessPNG(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImageProcessor_SupportedFormats(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
proc := New(Params{})
|
||||
|
||||
inputFormats := proc.SupportedInputFormats()
|
||||
@@ -322,55 +326,49 @@ func TestImageProcessor_SupportedFormats(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImageProcessor_RejectsOversizedInput(t *testing.T) {
|
||||
proc := New(Params{})
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
|
||||
// Create an image that exceeds MaxInputDimension (e.g., 10000x100)
|
||||
// This should be rejected before processing to prevent DoS
|
||||
input := createTestJPEG(t, 10000, 100)
|
||||
|
||||
req := &Request{
|
||||
Size: Size{Width: 100, Height: 100},
|
||||
Format: FormatJPEG,
|
||||
Quality: 85,
|
||||
FitMode: FitCover,
|
||||
// Images exceeding MaxInputDimension in either dimension must be
|
||||
// rejected before processing to prevent DoS.
|
||||
tests := []struct {
|
||||
name string
|
||||
width int
|
||||
height int
|
||||
}{
|
||||
{name: "oversized width", width: 10000, height: 100},
|
||||
{name: "oversized height", width: 100, height: 10000},
|
||||
}
|
||||
|
||||
_, err := proc.Process(ctx, bytes.NewReader(input), req)
|
||||
if err == nil {
|
||||
t.Error("Process() should reject oversized input images")
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if err != ErrInputTooLarge {
|
||||
t.Errorf("Process() error = %v, want ErrInputTooLarge", err)
|
||||
}
|
||||
}
|
||||
proc := New(Params{})
|
||||
ctx := context.Background()
|
||||
input := createTestJPEG(t, tt.width, tt.height)
|
||||
|
||||
func TestImageProcessor_RejectsOversizedInputHeight(t *testing.T) {
|
||||
proc := New(Params{})
|
||||
ctx := context.Background()
|
||||
req := &Request{
|
||||
Size: Size{Width: 100, Height: 100},
|
||||
Format: FormatJPEG,
|
||||
Quality: 85,
|
||||
FitMode: FitCover,
|
||||
}
|
||||
|
||||
// Create an image with oversized height
|
||||
input := createTestJPEG(t, 100, 10000)
|
||||
_, err := proc.Process(ctx, bytes.NewReader(input), req)
|
||||
if err == nil {
|
||||
t.Error("Process() should reject oversized input images")
|
||||
}
|
||||
|
||||
req := &Request{
|
||||
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)
|
||||
if !errors.Is(err, ErrInputTooLarge) {
|
||||
t.Errorf("Process() error = %v, want ErrInputTooLarge", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageProcessor_AcceptsMaxDimensionInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
proc := New(Params{})
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -386,12 +384,20 @@ func TestImageProcessor_AcceptsMaxDimensionInput(t *testing.T) {
|
||||
|
||||
result, err := proc.Process(ctx, bytes.NewReader(input), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Process() should accept images at MaxInputDimension, got error: %v", err)
|
||||
t.Fatalf(
|
||||
"Process() should accept images at MaxInputDimension, got error: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
defer result.Content.Close()
|
||||
|
||||
defer func() { _ = result.Content.Close() }()
|
||||
}
|
||||
|
||||
func TestImageProcessor_EncodeWebP(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) {
|
||||
t.Helper()
|
||||
|
||||
proc := New(Params{})
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -399,8 +405,8 @@ func TestImageProcessor_EncodeWebP(t *testing.T) {
|
||||
|
||||
req := &Request{
|
||||
Size: Size{Width: 100, Height: 75},
|
||||
Format: FormatWebP,
|
||||
Quality: 80,
|
||||
Format: format,
|
||||
Quality: quality,
|
||||
FitMode: FitCover,
|
||||
}
|
||||
|
||||
@@ -408,29 +414,39 @@ func TestImageProcessor_EncodeWebP(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Process() error = %v, want nil", err)
|
||||
}
|
||||
defer result.Content.Close()
|
||||
|
||||
// Verify output is valid WebP
|
||||
defer func() { _ = result.Content.Close() }()
|
||||
|
||||
// Verify output format
|
||||
data, err := io.ReadAll(result.Content)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read result: %v", err)
|
||||
}
|
||||
|
||||
mime := detectMIME(data)
|
||||
if mime != "image/webp" {
|
||||
t.Errorf("Output format = %v, want image/webp", mime)
|
||||
if mime != wantMIME {
|
||||
t.Errorf("Output format = %v, want %v", mime, wantMIME)
|
||||
}
|
||||
|
||||
// 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_EncodeWebP(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodeAndCheck(t, FormatWebP, 80, mimeWebP)
|
||||
}
|
||||
|
||||
func TestImageProcessor_DecodeAVIF(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
proc := New(Params{})
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -452,7 +468,8 @@ func TestImageProcessor_DecodeAVIF(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Process() error = %v, want nil (AVIF decoding should work)", err)
|
||||
}
|
||||
defer result.Content.Close()
|
||||
|
||||
defer func() { _ = result.Content.Close() }()
|
||||
|
||||
// Verify output is valid JPEG
|
||||
data, err := io.ReadAll(result.Content)
|
||||
@@ -461,14 +478,17 @@ func TestImageProcessor_DecodeAVIF(t *testing.T) {
|
||||
}
|
||||
|
||||
mime := detectMIME(data)
|
||||
if mime != "image/jpeg" {
|
||||
if mime != mimeJPEG {
|
||||
t.Errorf("Output format = %v, want image/jpeg", mime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageProcessor_RejectsOversizedInputData(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a processor with a very small byte limit
|
||||
const limit = 1024
|
||||
|
||||
proc := New(Params{MaxInputBytes: limit})
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -490,12 +510,14 @@ func TestImageProcessor_RejectsOversizedInputData(t *testing.T) {
|
||||
t.Fatal("Process() should reject input exceeding maxInputBytes")
|
||||
}
|
||||
|
||||
if err != ErrInputDataTooLarge {
|
||||
if !errors.Is(err, ErrInputDataTooLarge) {
|
||||
t.Errorf("Process() error = %v, want ErrInputDataTooLarge", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageProcessor_AcceptsInputWithinLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a small image and set limit well above its size
|
||||
input := createTestJPEG(t, 10, 10)
|
||||
limit := int64(len(input)) * 10 // 10× headroom
|
||||
@@ -514,10 +536,13 @@ func TestImageProcessor_AcceptsInputWithinLimit(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Process() error = %v, want nil", err)
|
||||
}
|
||||
defer result.Content.Close()
|
||||
|
||||
defer func() { _ = result.Content.Close() }()
|
||||
}
|
||||
|
||||
func TestImageProcessor_DefaultMaxInputBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Passing 0 should use the default
|
||||
proc := New(Params{})
|
||||
if proc.maxInputBytes != DefaultMaxInputBytes {
|
||||
@@ -532,40 +557,7 @@ func TestImageProcessor_DefaultMaxInputBytes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImageProcessor_EncodeAVIF(t *testing.T) {
|
||||
proc := New(Params{})
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
|
||||
input := createTestJPEG(t, 200, 150)
|
||||
|
||||
req := &Request{
|
||||
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 := detectMIME(data)
|
||||
if mime != "image/avif" {
|
||||
t.Errorf("Output format = %v, want image/avif", mime)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
encodeAndCheck(t, FormatAVIF, 85, mimeAVIF)
|
||||
}
|
||||
Reference in New Issue
Block a user