check / check (push) Successful in 13s
Every output is exported with govips' StripMetadata, so it carries no EXIF (GPS, serial numbers, embedded thumbnails), XMP, IPTC or ICC profile, the orig format included: it is always re-encoded, and pixa never serves the source bytes. The image is turned upright with AutoRotate right after decoding, so dropping the orientation tag does not leave it rotated, and a requested size applies to the upright image. An image with an ICC profile is converted to sRGB before export. No setting turns this off. README.md documents it. Model: opus-5-5
715 lines
16 KiB
Go
715 lines
16 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"
|
||
)
|
||
|
||
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(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{})
|
||
ctx := context.Background()
|
||
|
||
input := createTestJPEG(t, 800, 600)
|
||
|
||
req := &Request{
|
||
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 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{})
|
||
ctx := context.Background()
|
||
|
||
input := createTestJPEG(t, 200, 150)
|
||
|
||
req := &Request{
|
||
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 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 Size, wantW, wantH int,
|
||
) {
|
||
t.Helper()
|
||
|
||
proc := New(Params{})
|
||
ctx := context.Background()
|
||
|
||
input := createTestJPEG(t, inputW, inputH)
|
||
|
||
req := &Request{
|
||
Size: 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 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, Size{Width: 0, Height: 0}, 640, 480)
|
||
}
|
||
|
||
func TestImageProcessor_FitContain(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
proc := New(Params{})
|
||
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 := &Request{
|
||
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 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, 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, Size{Width: 0, Height: 300}, 400, 300)
|
||
}
|
||
|
||
func TestImageProcessor_ProcessPNG(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
proc := New(Params{})
|
||
ctx := context.Background()
|
||
|
||
input := createTestPNG(t, 400, 300)
|
||
|
||
req := &Request{
|
||
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 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_SupportedFormats(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
proc := New(Params{})
|
||
|
||
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) {
|
||
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{})
|
||
ctx := context.Background()
|
||
input := createTestJPEG(t, tt.width, tt.height)
|
||
|
||
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 !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()
|
||
|
||
// Create an image at exactly MaxInputDimension - should be accepted
|
||
input := createTestJPEG(t, MaxInputDimension, 100)
|
||
|
||
req := &Request{
|
||
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 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 Format, quality int, wantMIME string) {
|
||
t.Helper()
|
||
|
||
proc := New(Params{})
|
||
ctx := context.Background()
|
||
|
||
input := createTestJPEG(t, 200, 150)
|
||
|
||
req := &Request{
|
||
Size: Size{Width: 100, Height: 75},
|
||
Format: format,
|
||
Quality: quality,
|
||
FitMode: FitCover,
|
||
}
|
||
|
||
result, err := proc.Process(ctx, bytes.NewReader(input), req)
|
||
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, FormatWebP, 80, mimeWebP)
|
||
}
|
||
|
||
func TestImageProcessor_DecodeAVIF(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
proc := New(Params{})
|
||
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 := &Request{
|
||
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 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})
|
||
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))
|
||
}
|
||
|
||
req := &Request{
|
||
Size: Size{Width: 100, Height: 75},
|
||
Format: FormatJPEG,
|
||
Quality: 85,
|
||
FitMode: FitCover,
|
||
}
|
||
|
||
_, err := proc.Process(ctx, bytes.NewReader(input), req)
|
||
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})
|
||
ctx := context.Background()
|
||
|
||
req := &Request{
|
||
Size: Size{Width: 10, Height: 10},
|
||
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", err)
|
||
}
|
||
|
||
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 {
|
||
t.Errorf("maxInputBytes = %d, want %d", proc.maxInputBytes, DefaultMaxInputBytes)
|
||
}
|
||
|
||
// Passing negative should also use the default
|
||
proc = New(Params{MaxInputBytes: -1})
|
||
if proc.maxInputBytes != DefaultMaxInputBytes {
|
||
t.Errorf("maxInputBytes = %d, want %d", proc.maxInputBytes, DefaultMaxInputBytes)
|
||
}
|
||
}
|
||
|
||
func TestImageProcessor_EncodeAVIF(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
encodeAndCheck(t, 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 {
|
||
t.Helper()
|
||
|
||
result, err := New(Params{}).Process(
|
||
context.Background(), bytes.NewReader(input), req,
|
||
)
|
||
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 := []Format{
|
||
FormatJPEG, FormatPNG, FormatWebP, FormatAVIF, FormatGIF, FormatOriginal,
|
||
}
|
||
|
||
for _, format := range formats {
|
||
t.Run(string(format), func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
output := processAndDecode(t, input, &Request{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 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},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
output := processAndDecode(t, input, &Request{
|
||
Size: tt.size,
|
||
Format: 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, &Request{Format: 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)
|
||
}
|
||
}
|
||
}
|