A community based topic aggregation platform built on atproto
1package imageproxy
2
3import (
4 "bytes"
5 "image"
6 "image/color"
7 "image/jpeg"
8 "image/png"
9 "testing"
10
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13)
14
15// createTestJPEG creates a test JPEG image with the specified dimensions.
16func createTestJPEG(t *testing.T, width, height int) []byte {
17 t.Helper()
18 img := image.NewRGBA(image.Rect(0, 0, width, height))
19 // Fill with a solid color
20 for y := 0; y < height; y++ {
21 for x := 0; x < width; x++ {
22 img.Set(x, y, color.RGBA{R: 255, G: 128, B: 64, A: 255})
23 }
24 }
25 var buf bytes.Buffer
26 err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90})
27 require.NoError(t, err)
28 return buf.Bytes()
29}
30
31// createTestPNG creates a test PNG image with the specified dimensions.
32func createTestPNG(t *testing.T, width, height int) []byte {
33 t.Helper()
34 img := image.NewRGBA(image.Rect(0, 0, width, height))
35 // Fill with a solid color
36 for y := 0; y < height; y++ {
37 for x := 0; x < width; x++ {
38 img.Set(x, y, color.RGBA{R: 64, G: 128, B: 255, A: 255})
39 }
40 }
41 var buf bytes.Buffer
42 err := png.Encode(&buf, img)
43 require.NoError(t, err)
44 return buf.Bytes()
45}
46
47func TestProcessor_Process_CoverFit(t *testing.T) {
48 proc := NewProcessor()
49
50 tests := []struct {
51 name string
52 srcWidth int
53 srcHeight int
54 preset Preset
55 wantWidth int
56 wantHeight int
57 description string
58 }{
59 {
60 name: "landscape image to square avatar",
61 srcWidth: 800,
62 srcHeight: 600,
63 preset: Preset{Name: "avatar", Width: 1000, Height: 1000, Fit: FitCover, Quality: 85},
64 wantWidth: 1000,
65 wantHeight: 1000,
66 description: "landscape cropped to square",
67 },
68 {
69 name: "portrait image to square avatar",
70 srcWidth: 600,
71 srcHeight: 800,
72 preset: Preset{Name: "avatar", Width: 1000, Height: 1000, Fit: FitCover, Quality: 85},
73 wantWidth: 1000,
74 wantHeight: 1000,
75 description: "portrait cropped to square",
76 },
77 {
78 name: "square image to smaller square",
79 srcWidth: 500,
80 srcHeight: 500,
81 preset: Preset{Name: "avatar_small", Width: 360, Height: 360, Fit: FitCover, Quality: 80},
82 wantWidth: 360,
83 wantHeight: 360,
84 description: "square scaled down",
85 },
86 {
87 name: "landscape to banner dimensions",
88 srcWidth: 1920,
89 srcHeight: 1080,
90 preset: Preset{Name: "banner", Width: 640, Height: 300, Fit: FitCover, Quality: 85},
91 wantWidth: 640,
92 wantHeight: 300,
93 description: "banner crop",
94 },
95 {
96 name: "embed thumbnail dimensions",
97 srcWidth: 1600,
98 srcHeight: 900,
99 preset: Preset{Name: "embed_thumbnail", Width: 720, Height: 360, Fit: FitCover, Quality: 80},
100 wantWidth: 720,
101 wantHeight: 360,
102 description: "embed thumbnail crop",
103 },
104 }
105
106 for _, tt := range tests {
107 t.Run(tt.name, func(t *testing.T) {
108 srcData := createTestJPEG(t, tt.srcWidth, tt.srcHeight)
109
110 result, err := proc.Process(srcData, tt.preset)
111 require.NoError(t, err)
112 require.NotNil(t, result)
113
114 // Decode the result to verify dimensions
115 img, _, err := image.Decode(bytes.NewReader(result))
116 require.NoError(t, err)
117
118 bounds := img.Bounds()
119 assert.Equal(t, tt.wantWidth, bounds.Dx(), "width mismatch for %s", tt.description)
120 assert.Equal(t, tt.wantHeight, bounds.Dy(), "height mismatch for %s", tt.description)
121 })
122 }
123}
124
125func TestProcessor_Process_ContainFit(t *testing.T) {
126 proc := NewProcessor()
127
128 tests := []struct {
129 name string
130 srcWidth int
131 srcHeight int
132 preset Preset
133 wantMaxWidth int
134 wantMaxHeight int
135 description string
136 }{
137 {
138 name: "landscape image scaled to content_preview width",
139 srcWidth: 1600,
140 srcHeight: 900,
141 preset: Preset{Name: "content_preview", Width: 800, Height: 0, Fit: FitContain, Quality: 80},
142 wantMaxWidth: 800,
143 wantMaxHeight: 450, // 800 * (900/1600) = 450 (aspect ratio preserved)
144 description: "landscape scaled proportionally",
145 },
146 {
147 name: "portrait image scaled to content_preview width",
148 srcWidth: 900,
149 srcHeight: 1600,
150 preset: Preset{Name: "content_preview", Width: 800, Height: 0, Fit: FitContain, Quality: 80},
151 wantMaxWidth: 800,
152 wantMaxHeight: 1422, // 800 * (1600/900) ~= 1422
153 description: "portrait scaled proportionally",
154 },
155 {
156 name: "wide panorama to content_full",
157 srcWidth: 3200,
158 srcHeight: 800,
159 preset: Preset{Name: "content_full", Width: 1600, Height: 0, Fit: FitContain, Quality: 90},
160 wantMaxWidth: 1600,
161 wantMaxHeight: 400, // 1600 * (800/3200) = 400
162 description: "panorama scaled proportionally",
163 },
164 {
165 name: "image smaller than target width stays same size",
166 srcWidth: 400,
167 srcHeight: 300,
168 preset: Preset{Name: "content_preview", Width: 800, Height: 0, Fit: FitContain, Quality: 80},
169 wantMaxWidth: 400, // Don't upscale
170 wantMaxHeight: 300,
171 description: "small image not upscaled",
172 },
173 }
174
175 for _, tt := range tests {
176 t.Run(tt.name, func(t *testing.T) {
177 srcData := createTestJPEG(t, tt.srcWidth, tt.srcHeight)
178
179 result, err := proc.Process(srcData, tt.preset)
180 require.NoError(t, err)
181 require.NotNil(t, result)
182
183 // Decode the result to verify dimensions
184 img, _, err := image.Decode(bytes.NewReader(result))
185 require.NoError(t, err)
186
187 bounds := img.Bounds()
188 // For contain fit, verify width doesn't exceed max and aspect ratio is preserved
189 assert.LessOrEqual(t, bounds.Dx(), tt.wantMaxWidth, "width should not exceed max for %s", tt.description)
190 assert.Equal(t, tt.wantMaxWidth, bounds.Dx(), "width mismatch for %s", tt.description)
191 assert.Equal(t, tt.wantMaxHeight, bounds.Dy(), "height mismatch for %s", tt.description)
192 })
193 }
194}
195
196func TestProcessor_Process_InvalidImageData(t *testing.T) {
197 proc := NewProcessor()
198
199 tests := []struct {
200 name string
201 data []byte
202 wantErr error
203 }{
204 {
205 name: "empty data",
206 data: []byte{},
207 wantErr: ErrUnsupportedFormat,
208 },
209 {
210 name: "nil data",
211 data: nil,
212 wantErr: ErrUnsupportedFormat,
213 },
214 {
215 name: "random garbage data",
216 data: []byte("not an image at all"),
217 wantErr: ErrUnsupportedFormat,
218 },
219 {
220 name: "truncated JPEG header",
221 data: []byte{0xFF, 0xD8, 0xFF, 0xE0}, // Partial JPEG magic
222 wantErr: ErrProcessingFailed,
223 },
224 }
225
226 preset, _ := GetPreset("avatar")
227
228 for _, tt := range tests {
229 t.Run(tt.name, func(t *testing.T) {
230 result, err := proc.Process(tt.data, preset)
231 require.Error(t, err)
232 assert.ErrorIs(t, err, tt.wantErr)
233 assert.Nil(t, result)
234 })
235 }
236}
237
238func TestProcessor_Process_SupportsJPEG(t *testing.T) {
239 proc := NewProcessor()
240 srcData := createTestJPEG(t, 500, 500)
241 preset, _ := GetPreset("avatar")
242
243 result, err := proc.Process(srcData, preset)
244 require.NoError(t, err)
245 require.NotNil(t, result)
246
247 // Verify output is valid JPEG
248 img, format, err := image.Decode(bytes.NewReader(result))
249 require.NoError(t, err)
250 assert.Equal(t, "jpeg", format)
251 assert.Equal(t, 1000, img.Bounds().Dx())
252 assert.Equal(t, 1000, img.Bounds().Dy())
253}
254
255func TestProcessor_Process_SupportsPNG(t *testing.T) {
256 proc := NewProcessor()
257 srcData := createTestPNG(t, 500, 500)
258 preset, _ := GetPreset("avatar")
259
260 result, err := proc.Process(srcData, preset)
261 require.NoError(t, err)
262 require.NotNil(t, result)
263
264 // Verify output is valid JPEG (always output JPEG)
265 img, format, err := image.Decode(bytes.NewReader(result))
266 require.NoError(t, err)
267 assert.Equal(t, "jpeg", format)
268 assert.Equal(t, 1000, img.Bounds().Dx())
269 assert.Equal(t, 1000, img.Bounds().Dy())
270}
271
272func TestProcessor_Process_AlwaysOutputsJPEG(t *testing.T) {
273 proc := NewProcessor()
274 preset, _ := GetPreset("avatar")
275
276 // Test with PNG input
277 pngData := createTestPNG(t, 300, 300)
278 result, err := proc.Process(pngData, preset)
279 require.NoError(t, err)
280
281 // Verify output is JPEG even when input is PNG
282 _, format, err := image.Decode(bytes.NewReader(result))
283 require.NoError(t, err)
284 assert.Equal(t, "jpeg", format, "output should always be JPEG")
285}
286
287func TestProcessor_Interface(t *testing.T) {
288 // Compile-time check that ImageProcessor implements Processor
289 var _ Processor = (*ImageProcessor)(nil)
290}
291
292func TestNewProcessor(t *testing.T) {
293 proc := NewProcessor()
294 require.NotNil(t, proc)
295
296 // Verify it's an *ImageProcessor
297 _, ok := proc.(*ImageProcessor)
298 assert.True(t, ok, "NewProcessor should return *ImageProcessor")
299}