check / check (push) Waiting to run
The new package internal/providers builds the cache, the fetcher, the processor, the image service, the session manager and the encrypted URL generator from the config. pixad's fx graph provides them with providers.Constructors(), and handlers.New receives them built. The server tests build the same graph, with only the fetcher's dial function replaced. database.New opens the database, so these parts can read it while fx builds them; the cache's stop hook still runs before the database's. Each setting's default is declared in internal/config, and internal/providers works out the default cache_max_bytes. The cache's MaxBytes is the one value for the disk cache limit, and 0 turns the disk cache off. The fetcher and the processor take explicit values. Model: opus-5-5
694 lines
17 KiB
Go
694 lines
17 KiB
Go
package imageprocessor
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"errors"
|
||
"image"
|
||
"image/color"
|
||
"image/jpeg"
|
||
"image/png"
|
||
"io"
|
||
"math"
|
||
"os"
|
||
"slices"
|
||
"testing"
|
||
|
||
"github.com/davidbyttow/govips/v2/vips"
|
||
"sneak.berlin/go/pixa/internal/imagerequest"
|
||
)
|
||
|
||
func TestMain(m *testing.M) {
|
||
initVips()
|
||
|
||
code := m.Run()
|
||
|
||
vips.Shutdown()
|
||
os.Exit(code)
|
||
}
|
||
|
||
// 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 := range height {
|
||
for x := range width {
|
||
img.Set(x, y, color.RGBA{
|
||
R: uint8((x * 255 / width) & 0xff),
|
||
G: uint8((y * 255 / height) & 0xff),
|
||
B: 128,
|
||
A: 255,
|
||
})
|
||
}
|
||
}
|
||
|
||
var buf bytes.Buffer
|
||
|
||
err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90})
|
||
if 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 := range height {
|
||
for x := range width {
|
||
img.Set(x, y, color.RGBA{
|
||
R: uint8((x * 255 / width) & 0xff),
|
||
G: uint8((y * 255 / height) & 0xff),
|
||
B: 128,
|
||
A: 255,
|
||
})
|
||
}
|
||
}
|
||
|
||
var buf bytes.Buffer
|
||
|
||
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(imagerequest.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 mimeJPEG
|
||
}
|
||
|
||
if len(data) >= 8 && string(data[:8]) == "\x89PNG\r\n\x1a\n" {
|
||
return mimePNG
|
||
}
|
||
|
||
if len(data) >= 4 && string(data[:4]) == "GIF8" {
|
||
return mimeGIF
|
||
}
|
||
|
||
if len(data) >= 12 && string(data[:4]) == "RIFF" && string(data[8:12]) == "WEBP" {
|
||
return mimeWebP
|
||
}
|
||
|
||
if isAVIF(data) {
|
||
return mimeAVIF
|
||
}
|
||
|
||
return ""
|
||
}
|
||
|
||
func TestImageProcessor_ResizeJPEG(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
|
||
ctx := context.Background()
|
||
|
||
input := createTestJPEG(t, 800, 600)
|
||
|
||
transform := &imagerequest.Transform{
|
||
Size: imagerequest.Size{Width: 400, Height: 300},
|
||
Format: imagerequest.FormatJPEG,
|
||
Quality: 85,
|
||
FitMode: imagerequest.FitCover,
|
||
}
|
||
|
||
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
|
||
if err != nil {
|
||
t.Fatalf("Process() error = %v", err)
|
||
}
|
||
|
||
defer func() { _ = 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 := detectMIME(data)
|
||
if mime != mimeJPEG {
|
||
t.Errorf("Output format = %v, want image/jpeg", mime)
|
||
}
|
||
}
|
||
|
||
func TestImageProcessor_ConvertToPNG(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
|
||
ctx := context.Background()
|
||
|
||
input := createTestJPEG(t, 200, 150)
|
||
|
||
transform := &imagerequest.Transform{
|
||
Size: imagerequest.Size{Width: 200, Height: 150},
|
||
Format: imagerequest.FormatPNG,
|
||
FitMode: imagerequest.FitCover,
|
||
}
|
||
|
||
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
|
||
if err != nil {
|
||
t.Fatalf("Process() error = %v", err)
|
||
}
|
||
|
||
defer func() { _ = result.Content.Close() }()
|
||
|
||
data, err := io.ReadAll(result.Content)
|
||
if err != nil {
|
||
t.Fatalf("failed to read result: %v", err)
|
||
}
|
||
|
||
mime := detectMIME(data)
|
||
if mime != mimePNG {
|
||
t.Errorf("Output format = %v, want image/png", mime)
|
||
}
|
||
}
|
||
|
||
// 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 imagerequest.Size, wantW, wantH int,
|
||
) {
|
||
t.Helper()
|
||
|
||
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
|
||
ctx := context.Background()
|
||
|
||
input := createTestJPEG(t, inputW, inputH)
|
||
|
||
transform := &imagerequest.Transform{
|
||
Size: size,
|
||
Format: imagerequest.FormatJPEG,
|
||
Quality: 85,
|
||
FitMode: imagerequest.FitCover,
|
||
}
|
||
|
||
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
|
||
if err != nil {
|
||
t.Fatalf("Process() error = %v", err)
|
||
}
|
||
|
||
defer func() { _ = result.Content.Close() }()
|
||
|
||
if result.Width != wantW {
|
||
t.Errorf("Process() width = %d, want %d", result.Width, wantW)
|
||
}
|
||
|
||
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, imagerequest.Size{Width: 0, Height: 0}, 640, 480)
|
||
}
|
||
|
||
func TestImageProcessor_FitContain(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
|
||
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)
|
||
|
||
transform := &imagerequest.Transform{
|
||
Size: imagerequest.Size{Width: 400, Height: 400},
|
||
Format: imagerequest.FormatJPEG,
|
||
Quality: 85,
|
||
FitMode: imagerequest.FitContain,
|
||
}
|
||
|
||
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
|
||
if err != nil {
|
||
t.Fatalf("Process() error = %v", err)
|
||
}
|
||
|
||
defer func() { _ = 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) {
|
||
t.Parallel()
|
||
|
||
// 800x600 image, request width=400 height=0
|
||
// Should scale proportionally to 400x300
|
||
processAndCheckSize(t, 800, 600, imagerequest.Size{Width: 400, Height: 0}, 400, 300)
|
||
}
|
||
|
||
func TestImageProcessor_ProportionalScale_HeightOnly(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
// 800x600 image, request width=0 height=300
|
||
// Should scale proportionally to 400x300
|
||
processAndCheckSize(t, 800, 600, imagerequest.Size{Width: 0, Height: 300}, 400, 300)
|
||
}
|
||
|
||
func TestImageProcessor_ProcessPNG(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
|
||
ctx := context.Background()
|
||
|
||
input := createTestPNG(t, 400, 300)
|
||
|
||
transform := &imagerequest.Transform{
|
||
Size: imagerequest.Size{Width: 200, Height: 150},
|
||
Format: imagerequest.FormatPNG,
|
||
FitMode: imagerequest.FitCover,
|
||
}
|
||
|
||
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
|
||
if err != nil {
|
||
t.Fatalf("Process() error = %v", err)
|
||
}
|
||
|
||
defer func() { _ = 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_RejectsOversizedInput(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
// 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},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
|
||
ctx := context.Background()
|
||
input := createTestJPEG(t, tt.width, tt.height)
|
||
|
||
transform := &imagerequest.Transform{
|
||
Size: imagerequest.Size{Width: 100, Height: 100},
|
||
Format: imagerequest.FormatJPEG,
|
||
Quality: 85,
|
||
FitMode: imagerequest.FitCover,
|
||
}
|
||
|
||
_, err := proc.Process(ctx, bytes.NewReader(input), transform)
|
||
if err == nil {
|
||
t.Error("Process() should reject oversized input images")
|
||
}
|
||
|
||
if !errors.Is(err, ErrInputTooLarge) {
|
||
t.Errorf("Process() error = %v, want ErrInputTooLarge", err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestImageProcessor_AcceptsMaxDimensionInput(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
|
||
ctx := context.Background()
|
||
|
||
// Create an image at exactly MaxInputDimension - should be accepted
|
||
input := createTestJPEG(t, MaxInputDimension, 100)
|
||
|
||
transform := &imagerequest.Transform{
|
||
Size: imagerequest.Size{Width: 100, Height: 100},
|
||
Format: imagerequest.FormatJPEG,
|
||
Quality: 85,
|
||
FitMode: imagerequest.FitCover,
|
||
}
|
||
|
||
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
|
||
if err != nil {
|
||
t.Fatalf(
|
||
"Process() should accept images at MaxInputDimension, got error: %v",
|
||
err,
|
||
)
|
||
}
|
||
|
||
defer func() { _ = result.Content.Close() }()
|
||
}
|
||
|
||
// 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 imagerequest.Format, quality int, wantMIME string,
|
||
) {
|
||
t.Helper()
|
||
|
||
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
|
||
ctx := context.Background()
|
||
|
||
input := createTestJPEG(t, 200, 150)
|
||
|
||
transform := &imagerequest.Transform{
|
||
Size: imagerequest.Size{Width: 100, Height: 75},
|
||
Format: format,
|
||
Quality: quality,
|
||
FitMode: imagerequest.FitCover,
|
||
}
|
||
|
||
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
|
||
if err != nil {
|
||
t.Fatalf("Process() error = %v, want nil", err)
|
||
}
|
||
|
||
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 != 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, imagerequest.FormatWebP, 80, mimeWebP)
|
||
}
|
||
|
||
func TestImageProcessor_DecodeAVIF(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
proc := New(Params{MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 1})
|
||
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
|
||
transform := &imagerequest.Transform{
|
||
Size: imagerequest.Size{Width: 2, Height: 2},
|
||
Format: imagerequest.FormatJPEG,
|
||
Quality: 85,
|
||
FitMode: imagerequest.FitCover,
|
||
}
|
||
|
||
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
|
||
if err != nil {
|
||
t.Fatalf("Process() error = %v, want nil (AVIF decoding should work)", err)
|
||
}
|
||
|
||
defer func() { _ = 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 := detectMIME(data)
|
||
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, MaxConcurrentProcessing: 1})
|
||
ctx := context.Background()
|
||
|
||
// Create a valid JPEG that exceeds the byte limit
|
||
input := createTestJPEG(t, 800, 600) // will be well over 1 KiB
|
||
if int64(len(input)) <= limit {
|
||
t.Fatalf("test JPEG must exceed %d bytes, got %d", limit, len(input))
|
||
}
|
||
|
||
transform := &imagerequest.Transform{
|
||
Size: imagerequest.Size{Width: 100, Height: 75},
|
||
Format: imagerequest.FormatJPEG,
|
||
Quality: 85,
|
||
FitMode: imagerequest.FitCover,
|
||
}
|
||
|
||
_, err := proc.Process(ctx, bytes.NewReader(input), transform)
|
||
if err == nil {
|
||
t.Fatal("Process() should reject input exceeding maxInputBytes")
|
||
}
|
||
|
||
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
|
||
|
||
proc := New(Params{MaxInputBytes: limit, MaxConcurrentProcessing: 1})
|
||
ctx := context.Background()
|
||
|
||
transform := &imagerequest.Transform{
|
||
Size: imagerequest.Size{Width: 10, Height: 10},
|
||
Format: imagerequest.FormatJPEG,
|
||
Quality: 85,
|
||
FitMode: imagerequest.FitCover,
|
||
}
|
||
|
||
result, err := proc.Process(ctx, bytes.NewReader(input), transform)
|
||
if err != nil {
|
||
t.Fatalf("Process() error = %v, want nil", err)
|
||
}
|
||
|
||
defer func() { _ = result.Content.Close() }()
|
||
}
|
||
|
||
func TestImageProcessor_EncodeAVIF(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
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, transform *imagerequest.Transform,
|
||
) *vips.ImageRef {
|
||
t.Helper()
|
||
|
||
result, err := New(Params{
|
||
MaxInputBytes: 50 << 20,
|
||
MaxConcurrentProcessing: 1,
|
||
}).Process(
|
||
context.Background(), bytes.NewReader(input), transform,
|
||
)
|
||
if err != nil {
|
||
t.Fatalf("Process() error = %v", err)
|
||
}
|
||
|
||
defer func() { _ = result.Content.Close() }()
|
||
|
||
data, err := io.ReadAll(result.Content)
|
||
if err != nil {
|
||
t.Fatalf("failed to read result: %v", err)
|
||
}
|
||
|
||
output, err := vips.NewImageFromBuffer(data)
|
||
if err != nil {
|
||
t.Fatalf("failed to decode output: %v", err)
|
||
}
|
||
|
||
t.Cleanup(output.Close)
|
||
|
||
return output
|
||
}
|
||
|
||
func TestImageProcessor_StripsEXIF(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
// gps-exif.jpg carries GPS coordinates, a camera make, model and serial
|
||
// number, and a capture time.
|
||
input, err := os.ReadFile("testdata/gps-exif.jpg")
|
||
if err != nil {
|
||
t.Fatalf("failed to read test JPEG: %v", err)
|
||
}
|
||
|
||
fixture, err := vips.NewImageFromBuffer(input)
|
||
if err != nil {
|
||
t.Fatalf("failed to decode test JPEG: %v", err)
|
||
}
|
||
|
||
t.Cleanup(fixture.Close)
|
||
|
||
if !slices.Contains(fixture.GetFields(), "exif-ifd3-GPSLatitude") {
|
||
t.Fatal("testdata/gps-exif.jpg has no GPS latitude")
|
||
}
|
||
|
||
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,
|
||
&imagerequest.Transform{Format: format})
|
||
|
||
if output.HasExif() {
|
||
t.Errorf("output has EXIF: %v", output.GetExif())
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestImageProcessor_AppliesEXIFOrientation(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
// orientation-6.jpg is stored 16x8, red on the left and blue on the
|
||
// right, with EXIF orientation 6 (turn 90 degrees clockwise to view).
|
||
// Upright it is 8x16, red on top and blue below.
|
||
input, err := os.ReadFile("testdata/orientation-6.jpg")
|
||
if err != nil {
|
||
t.Fatalf("failed to read test JPEG: %v", err)
|
||
}
|
||
|
||
tests := []struct {
|
||
name string
|
||
size imagerequest.Size
|
||
wantW int
|
||
wantH int
|
||
}{
|
||
{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, &imagerequest.Transform{
|
||
Size: tt.size,
|
||
Format: imagerequest.FormatPNG,
|
||
})
|
||
|
||
if output.Width() != tt.wantW || output.Height() != tt.wantH {
|
||
t.Fatalf("output is %dx%d, want %dx%d",
|
||
output.Width(), output.Height(), tt.wantW, tt.wantH)
|
||
}
|
||
|
||
top, err := output.GetPoint(tt.wantW/2, 0)
|
||
if err != nil {
|
||
t.Fatalf("GetPoint() error = %v", err)
|
||
}
|
||
|
||
bottom, err := output.GetPoint(tt.wantW/2, tt.wantH-1)
|
||
if err != nil {
|
||
t.Fatalf("GetPoint() error = %v", err)
|
||
}
|
||
|
||
if top[0] <= top[2] || bottom[2] <= bottom[0] {
|
||
t.Errorf("top pixel = %v, bottom pixel = %v, want red above blue",
|
||
top, bottom)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestImageProcessor_ConvertsWideGamutToSRGB(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
// display-p3.jpg is a flat 8x8 image with the Display P3 profile
|
||
// embedded, filled with Display P3 (234, 51, 35), which is sRGB red.
|
||
input, err := os.ReadFile("testdata/display-p3.jpg")
|
||
if err != nil {
|
||
t.Fatalf("failed to read test JPEG: %v", err)
|
||
}
|
||
|
||
output := processAndDecode(t, input,
|
||
&imagerequest.Transform{Format: imagerequest.FormatPNG})
|
||
|
||
if output.HasICCProfile() {
|
||
t.Error("output has an ICC profile")
|
||
}
|
||
|
||
pixel, err := output.GetPoint(4, 4)
|
||
if err != nil {
|
||
t.Fatalf("GetPoint() error = %v", err)
|
||
}
|
||
|
||
want := []float64{255, 0, 0}
|
||
for i := range want {
|
||
if math.Abs(pixel[i]-want[i]) > 5 {
|
||
t.Fatalf("pixel = %v, want within 5 of %v", pixel, want)
|
||
}
|
||
}
|
||
}
|