A community based topic aggregation platform built on atproto
at main 299 lines 8.5 kB view raw
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}