Files
RenewCMS/test/image_variants_geometry_test.go
2026-10-08 02:33:32 +02:00

324 lines
12 KiB
Go

package test
import (
domain "RenewCMS/internal/domain/image"
"RenewCMS/internal/infrastructure/imaging"
"bytes"
"image/color"
"strconv"
"testing"
)
func variantSettings(width int, height int, fit domain.VariantFit, format domain.VariantFormat) domain.VariantSettings {
settings := domain.VariantSettings{Fit: fit, Format: format}
if width > 0 {
settings.Width = &width
}
if height > 0 {
settings.Height = &height
}
return settings
}
func renderVariant(t *testing.T, source []byte, settings domain.VariantSettings) domain.RenderedVariant {
t.Helper()
rendered, err := imaging.NewVariantRenderer().Render(bytes.NewReader(source), settings)
if err != nil {
t.Fatalf("render failed: %v", err)
}
return rendered
}
func TestVariantOutputGeometry(t *testing.T) {
rows := []struct {
sourceWidth, sourceHeight, width, height int
fit domain.VariantFit
wantWidth, wantHeight int
}{
{4032, 3024, 400, 300, domain.FitCover, 400, 300},
{4032, 3024, 800, 0, domain.FitCover, 800, 600},
{4032, 3024, 0, 600, domain.FitContain, 800, 600},
{4032, 3024, 1000, 1000, domain.FitInside, 1000, 750},
{4032, 3024, 1000, 1000, domain.FitCover, 1000, 1000},
{4032, 3024, 1000, 1000, domain.FitContain, 1000, 1000},
{300, 200, 800, 0, domain.FitCover, 300, 200},
{300, 200, 0, 800, domain.FitInside, 300, 200},
{300, 200, 400, 300, domain.FitCover, 267, 200},
{300, 200, 400, 300, domain.FitContain, 400, 300},
{300, 200, 1200, 1200, domain.FitContain, 1200, 1200},
{300, 200, 1200, 1200, domain.FitInside, 300, 200},
}
for _, row := range rows {
name := strconv.Itoa(row.sourceWidth) + "x" + strconv.Itoa(row.sourceHeight) + " to " + strconv.Itoa(row.width) + "x" + strconv.Itoa(row.height) + " " + string(row.fit)
t.Run(name, func(t *testing.T) {
source := solidPNG(t, row.sourceWidth, row.sourceHeight)
rendered := renderVariant(t, source, variantSettings(row.width, row.height, row.fit, domain.FormatPNG))
width, height := decodedSize(t, rendered.Content)
if width != row.wantWidth || height != row.wantHeight {
t.Errorf("output = %dx%d, want %dx%d", width, height, row.wantWidth, row.wantHeight)
}
})
}
}
func TestVariantFitPixels(t *testing.T) {
t.Run("Cover crops around the centre", func(t *testing.T) {
rendered := renderVariant(t, encodePNG(t, stripedImage(t)), variantSettings(20, 20, domain.FitCover, domain.FormatPNG))
decoded, _ := decodeImage(t, rendered.Content)
if decoded.Bounds().Dx() != 20 || decoded.Bounds().Dy() != 20 {
t.Fatalf("output = %v", decoded.Bounds())
}
for y := 0; y < 20; y++ {
for x := 0; x < 20; x++ {
if got := pixelAt(decoded, x, y); got != variantGreen {
t.Fatalf("pixel %d,%d = %v, want green", x, y, got)
}
}
}
})
t.Run("Contain pads transparent on png", func(t *testing.T) {
source := encodePNG(t, variantSolidImage(40, 20, variantRed))
rendered := renderVariant(t, source, variantSettings(40, 40, domain.FitContain, domain.FormatPNG))
decoded, _ := decodeImage(t, rendered.Content)
if decoded.Bounds().Dx() != 40 || decoded.Bounds().Dy() != 40 {
t.Fatalf("output = %v", decoded.Bounds())
}
for x := 0; x < 40; x++ {
for _, y := range []int{0, 9, 30, 39} {
if got := pixelAt(decoded, x, y); got.A != 0 {
t.Fatalf("pixel %d,%d = %v, want transparent", x, y, got)
}
}
for _, y := range []int{10, 19, 29} {
if got := pixelAt(decoded, x, y); got != variantRed {
t.Fatalf("pixel %d,%d = %v, want red", x, y, got)
}
}
}
})
t.Run("Contain never enlarges a small source and centres it on the box", func(t *testing.T) {
source := encodePNG(t, variantSolidImage(30, 20, variantRed))
rendered := renderVariant(t, source, variantSettings(40, 30, domain.FitContain, domain.FormatPNG))
decoded, _ := decodeImage(t, rendered.Content)
if decoded.Bounds().Dx() != 40 || decoded.Bounds().Dy() != 30 {
t.Fatalf("output = %v", decoded.Bounds())
}
for _, inside := range [][2]int{{5, 5}, {34, 24}, {20, 15}} {
if got := pixelAt(decoded, inside[0], inside[1]); got != variantRed {
t.Errorf("pixel %v = %v, want red", inside, got)
}
}
for _, outside := range [][2]int{{4, 15}, {35, 15}, {20, 4}, {20, 25}} {
if got := pixelAt(decoded, outside[0], outside[1]); got.A != 0 {
t.Errorf("pixel %v = %v, want transparent", outside, got)
}
}
})
t.Run("Contain with a single dimension adds no padding", func(t *testing.T) {
source := encodePNG(t, variantSolidImage(40, 20, variantRed))
rendered := renderVariant(t, source, variantSettings(0, 10, domain.FitContain, domain.FormatPNG))
decoded, _ := decodeImage(t, rendered.Content)
if decoded.Bounds().Dx() != 20 || decoded.Bounds().Dy() != 10 {
t.Fatalf("output = %v", decoded.Bounds())
}
for y := 0; y < 10; y++ {
if got := pixelAt(decoded, 0, y); got.A == 0 {
t.Fatalf("pixel 0,%d is transparent", y)
}
}
})
t.Run("Contain pads transparent on webp", func(t *testing.T) {
source := encodePNG(t, variantSolidImage(40, 20, variantRed))
rendered := renderVariant(t, source, variantSettings(40, 40, domain.FitContain, domain.FormatWebP))
decoded, _ := decodeImage(t, rendered.Content)
if got := pixelAt(decoded, 20, 2); got.A != 0 {
t.Errorf("padded pixel = %v, want transparent", got)
}
})
t.Run("Contain pads white on jpeg", func(t *testing.T) {
source := encodePNG(t, variantSolidImage(40, 20, variantRed))
rendered := renderVariant(t, source, variantSettings(40, 40, domain.FitContain, domain.FormatJPEG))
decoded, _ := decodeImage(t, rendered.Content)
for _, y := range []int{0, 5, 9, 30, 35, 39} {
assertNearColor(t, pixelAt(decoded, 20, y), color.NRGBA{R: 255, G: 255, B: 255, A: 255})
}
})
t.Run("Inside adds no padding", func(t *testing.T) {
source := encodePNG(t, variantSolidImage(40, 20, variantRed))
rendered := renderVariant(t, source, variantSettings(40, 40, domain.FitInside, domain.FormatPNG))
decoded, _ := decodeImage(t, rendered.Content)
if decoded.Bounds().Dx() != 40 || decoded.Bounds().Dy() != 20 {
t.Fatalf("output = %v", decoded.Bounds())
}
for y := 0; y < 20; y++ {
if got := pixelAt(decoded, 3, y); got.A != 255 {
t.Fatalf("pixel 3,%d = %v, want opaque", y, got)
}
}
})
t.Run("A transparent png source flattened to jpeg becomes white", func(t *testing.T) {
source := encodePNG(t, variantSolidImage(10, 10, color.NRGBA{}))
rendered := renderVariant(t, source, variantSettings(10, 10, domain.FitCover, domain.FormatJPEG))
decoded, _ := decodeImage(t, rendered.Content)
for y := 0; y < 10; y++ {
for x := 0; x < 10; x++ {
assertNearColor(t, pixelAt(decoded, x, y), color.NRGBA{R: 255, G: 255, B: 255, A: 255})
}
}
})
}
func assertNearColor(t *testing.T, got color.NRGBA, want color.NRGBA) {
t.Helper()
const tolerance = 8
for _, pair := range [][2]uint8{{got.R, want.R}, {got.G, want.G}, {got.B, want.B}} {
difference := int(pair[0]) - int(pair[1])
if difference > tolerance || difference < -tolerance {
t.Fatalf("color = %v, want near %v", got, want)
}
}
}
func TestVariantOutputFormat(t *testing.T) {
photo := encodePNG(t, noisyImage(64, 64))
rows := []struct {
name string
source []byte
format domain.VariantFormat
contentType string
signature string
}{
{"png original", photo, domain.FormatOriginal, "image/png", "\x89PNG"},
{"jpeg original", encodeJPEG(t, noisyImage(64, 64)), domain.FormatOriginal, "image/jpeg", "\xff\xd8"},
{"png to jpeg", photo, domain.FormatJPEG, "image/jpeg", "\xff\xd8"},
{"jpeg to png", encodeJPEG(t, noisyImage(64, 64)), domain.FormatPNG, "image/png", "\x89PNG"},
{"png to webp", photo, domain.FormatWebP, "image/webp", "RIFF"},
}
for _, row := range rows {
t.Run(row.name, func(t *testing.T) {
rendered := renderVariant(t, row.source, variantSettings(32, 0, domain.FitCover, row.format))
if rendered.ContentType != row.contentType {
t.Errorf("content type = %q, want %q", rendered.ContentType, row.contentType)
}
if !bytes.HasPrefix(rendered.Content, []byte(row.signature)) {
t.Errorf("signature = %q", rendered.Content[:4])
}
})
}
t.Run("A webp output carries the WEBP marker and the right dimensions", func(t *testing.T) {
rendered := renderVariant(t, photo, variantSettings(32, 0, domain.FitCover, domain.FormatWebP))
width, height := webpSize(t, rendered.Content)
if string(rendered.Content[8:12]) != "WEBP" || width != 32 || height != 32 {
t.Errorf("webp = %q %dx%d", rendered.Content[8:12], width, height)
}
})
}
func TestVariantQuality(t *testing.T) {
photo := encodePNG(t, noisyImage(300, 300))
t.Run("Quality changes the size of a lossy output", func(t *testing.T) {
for _, format := range []domain.VariantFormat{domain.FormatJPEG, domain.FormatWebP} {
low, high := 20, 95
lowSettings := variantSettings(300, 0, domain.FitCover, format)
highSettings := lowSettings
lowSettings.Quality, highSettings.Quality = &low, &high
lowSize := len(renderVariant(t, photo, lowSettings).Content)
highSize := len(renderVariant(t, photo, highSettings).Content)
if lowSize >= highSize {
t.Errorf("%s: quality 20 gave %d bytes, quality 95 gave %d", format, lowSize, highSize)
}
}
})
t.Run("A png output ignores quality", func(t *testing.T) {
low, high := 30, 95
settings := variantSettings(100, 0, domain.FitCover, domain.FormatPNG)
first, second := settings, settings
first.Quality, second.Quality = &low, &high
if !bytes.Equal(renderVariant(t, photo, first).Content, renderVariant(t, photo, second).Content) {
t.Error("png outputs differ")
}
})
}
func TestVariantSourceHandling(t *testing.T) {
t.Run("A webp source still decodes and renders to jpeg", func(t *testing.T) {
webpSource := renderVariant(t, solidPNG(t, 40, 40), variantSettings(40, 0, domain.FitCover, domain.FormatWebP)).Content
rendered := renderVariant(t, webpSource, variantSettings(20, 0, domain.FitCover, domain.FormatJPEG))
if rendered.ContentType != "image/jpeg" {
t.Errorf("content type = %q", rendered.ContentType)
}
})
t.Run("A webp source with format original stays webp when an encoder exists", func(t *testing.T) {
webpSource := renderVariant(t, solidPNG(t, 40, 40), variantSettings(40, 0, domain.FitCover, domain.FormatWebP)).Content
rendered := renderVariant(t, webpSource, variantSettings(20, 0, domain.FitCover, domain.FormatOriginal))
if rendered.ContentType != "image/webp" {
t.Errorf("content type = %q", rendered.ContentType)
}
})
t.Run("Without a webp encoder a webp source with format original falls back to png", func(t *testing.T) {
webpSource := renderVariant(t, solidPNG(t, 40, 40), variantSettings(40, 0, domain.FitCover, domain.FormatWebP)).Content
renderer := imaging.NewVariantRendererEncoding(domain.FormatOriginal, domain.FormatJPEG, domain.FormatPNG)
rendered, err := renderer.Render(bytes.NewReader(webpSource), variantSettings(20, 0, domain.FitCover, domain.FormatOriginal))
if err != nil || rendered.ContentType != "image/png" {
t.Errorf("rendered = %q, err = %v", rendered.ContentType, err)
}
})
t.Run("The renderer reports the formats it can encode", func(t *testing.T) {
all := imaging.NewVariantRenderer().EncodableFormats()
without := imaging.NewVariantRendererEncoding(domain.FormatOriginal, domain.FormatJPEG, domain.FormatPNG).EncodableFormats()
if len(all) != 4 || len(without) != 3 {
t.Errorf("formats = %v and %v", all, without)
}
})
t.Run("A one-frame gif renders like any raster source", func(t *testing.T) {
rendered := renderVariant(t, stillGIF(t), variantSettings(4, 0, domain.FitCover, domain.FormatPNG))
if rendered.ContentType != "image/png" {
t.Errorf("content type = %q", rendered.ContentType)
}
})
t.Run("Bytes that are not an image fail to render", func(t *testing.T) {
_, err := imaging.NewVariantRenderer().Render(bytes.NewReader([]byte("not an image")), variantSettings(4, 0, domain.FitCover, domain.FormatPNG))
if err == nil {
t.Error("expected an error")
}
})
}