1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
|
package main
import (
"bytes"
"image"
"image/jpeg"
"image/png"
"os/exec"
"testing"
)
// requireCWebP skips a test when the encoder is unavailable. The Nix build
// wraps the binary with libwebp on PATH, but `go test` in a bare shell may not
// have it, and a skipped test is more useful than a failure that says nothing
// about the code.
func requireCWebP(t *testing.T) {
t.Helper()
if _, err := exec.LookPath("cwebp"); err != nil {
t.Skip("cwebp not on PATH; skipping (the Nix build guarantees it)")
}
}
func encodePNG(t *testing.T, img image.Image) []byte {
t.Helper()
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
t.Fatalf("encode png: %v", err)
}
return buf.Bytes()
}
func encodeJPEG(t *testing.T, img image.Image) []byte {
t.Helper()
var buf bytes.Buffer
if err := jpeg.Encode(&buf, img, nil); err != nil {
t.Fatalf("encode jpeg: %v", err)
}
return buf.Bytes()
}
// isWebP checks the RIFF container signature, so the test asserts the actual
// output format rather than trusting the MIME string we set ourselves.
func isWebP(b []byte) bool {
return len(b) >= 12 && bytes.Equal(b[0:4], []byte("RIFF")) && bytes.Equal(b[8:12], []byte("WEBP"))
}
func TestProcessImageAlwaysProducesWebP(t *testing.T) {
requireCWebP(t)
img := testImage(200, 100)
cases := map[string][]byte{
"image/png": encodePNG(t, img),
"image/jpeg": encodeJPEG(t, img),
}
for mime, data := range cases {
t.Run(mime, func(t *testing.T) {
proc, err := processImage(data, mime)
if err != nil {
t.Fatalf("processImage: %v", err)
}
if proc.Width != 200 || proc.Height != 100 {
t.Errorf("dimensions = %dx%d, want 200x100", proc.Width, proc.Height)
}
if len(proc.Renditions) != 2 {
t.Fatalf("got %d renditions, want 2", len(proc.Renditions))
}
for _, r := range proc.Renditions {
if r.MIME != "image/webp" {
t.Errorf("%s: MIME = %q", r.Variant, r.MIME)
}
if !isWebP(r.Bytes) {
t.Errorf("%s: output is not a WebP file", r.Variant)
}
}
})
}
}
// A WebP upload must be re-encoded through the same path, so every served
// image has the same quality and carries no metadata.
func TestProcessImageAcceptsWebPInput(t *testing.T) {
requireCWebP(t)
first, err := processImage(encodePNG(t, testImage(120, 80)), "image/png")
if err != nil {
t.Fatal(err)
}
again, err := processImage(first.Renditions[0].Bytes, "image/webp")
if err != nil {
t.Fatalf("re-processing a webp: %v", err)
}
if again.Width != 120 || again.Height != 80 {
t.Errorf("dimensions = %dx%d, want 120x80", again.Width, again.Height)
}
}
// Upscaling a small image would only add bytes and blur.
func TestSmallImagesAreNotUpscaled(t *testing.T) {
requireCWebP(t)
proc, err := processImage(encodePNG(t, testImage(300, 150)), "image/png")
if err != nil {
t.Fatal(err)
}
for _, r := range proc.Renditions {
if r.Width != 300 || r.Height != 150 {
t.Errorf("%s: %dx%d, want the original 300x150", r.Variant, r.Width, r.Height)
}
}
}
func TestLargeImagesAreResized(t *testing.T) {
requireCWebP(t)
// 2400x1200 exceeds both target widths.
proc, err := processImage(encodePNG(t, testImage(2400, 1200)), "image/png")
if err != nil {
t.Fatal(err)
}
want := map[string][2]int{
VariantWebP1600: {1600, 800},
VariantWebP800: {800, 400},
}
for _, r := range proc.Renditions {
w := want[r.Variant]
if r.Width != w[0] || r.Height != w[1] {
t.Errorf("%s: %dx%d, want %dx%d", r.Variant, r.Width, r.Height, w[0], w[1])
}
}
}
// A rotated phone photo must come out upright, since the EXIF tag that would
// otherwise carry the rotation is deliberately stripped from the output.
func TestEXIFRotationIsBakedIn(t *testing.T) {
requireCWebP(t)
// Portrait content stored as landscape, with "rotate 90° clockwise".
data := buildJPEGWithOrientation(t, testImage(200, 100), orientRotate90, false)
proc, err := processImage(data, "image/jpeg")
if err != nil {
t.Fatalf("processImage: %v", err)
}
if proc.Width != 100 || proc.Height != 200 {
t.Errorf("dimensions = %dx%d, want 100x200 (rotation not applied)", proc.Width, proc.Height)
}
}
func TestProcessImageRejectsGarbage(t *testing.T) {
for name, data := range map[string][]byte{
"empty": {},
"not image": []byte("just some prose, definitely not an image"),
"truncated": encodePNG(t, testImage(10, 10))[:20],
} {
t.Run(name, func(t *testing.T) {
if _, err := processImage(data, "image/png"); err == nil {
t.Error("expected an error, got nil")
}
})
}
}
func TestResizeToWidthPreservesAspect(t *testing.T) {
src := testImage(1000, 400)
got := resizeToWidth(src, 250)
if got.Bounds().Dx() != 250 || got.Bounds().Dy() != 100 {
t.Errorf("got %dx%d, want 250x100", got.Bounds().Dx(), got.Bounds().Dy())
}
// An image at or below the target is returned untouched, not copied.
if same := resizeToWidth(src, 1000); same != src {
t.Error("an already-small-enough image was copied instead of returned as is")
}
}
func TestWritePAMHeader(t *testing.T) {
var buf bytes.Buffer
if err := writePAM(&buf, testImage(3, 2)); err != nil {
t.Fatal(err)
}
out := buf.Bytes()
want := "P7\nWIDTH 3\nHEIGHT 2\nDEPTH 4\nMAXVAL 255\nTUPLTYPE RGB_ALPHA\nENDHDR\n"
if !bytes.HasPrefix(out, []byte(want)) {
t.Fatalf("header = %q", out[:min(len(out), 80)])
}
if got, wantLen := len(out)-len(want), 3*2*4; got != wantLen {
t.Errorf("pixel payload = %d bytes, want %d", got, wantLen)
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
|