324 lines
12 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|