git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/imaging/io_test.go (about) 1 package imaging 2 3 import ( 4 "bytes" 5 "errors" 6 "image" 7 "image/color" 8 "image/color/palette" 9 "image/draw" 10 "image/png" 11 "io" 12 "io/ioutil" 13 "os" 14 "path/filepath" 15 "strings" 16 "testing" 17 ) 18 19 var ( 20 errCreate = errors.New("failed to create file") 21 errClose = errors.New("failed to close file") 22 errOpen = errors.New("failed to open file") 23 ) 24 25 type badFS struct{} 26 27 func (badFS) Create(name string) (io.WriteCloser, error) { 28 if name == "badFile.jpg" { 29 return badFile{ioutil.Discard}, nil 30 } 31 return nil, errCreate 32 } 33 34 func (badFS) Open(name string) (io.ReadCloser, error) { 35 return nil, errOpen 36 } 37 38 type badFile struct { 39 io.Writer 40 } 41 42 func (badFile) Close() error { 43 return errClose 44 } 45 46 type quantizer struct { 47 palette []color.Color 48 } 49 50 func (q quantizer) Quantize(p color.Palette, m image.Image) color.Palette { 51 pal := make([]color.Color, len(p), cap(p)) 52 copy(pal, p) 53 n := cap(p) - len(p) 54 if n > len(q.palette) { 55 n = len(q.palette) 56 } 57 for i := 0; i < n; i++ { 58 pal = append(pal, q.palette[i]) 59 } 60 return pal 61 } 62 63 func TestOpenSave(t *testing.T) { 64 imgWithoutAlpha := image.NewNRGBA(image.Rect(0, 0, 4, 6)) 65 imgWithoutAlpha.Pix = []uint8{ 66 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 67 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 68 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 69 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 70 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x88, 0x88, 0x88, 0xff, 0x88, 0x88, 0x88, 0xff, 71 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x88, 0x88, 0x88, 0xff, 0x88, 0x88, 0x88, 0xff, 72 } 73 imgWithAlpha := image.NewNRGBA(image.Rect(0, 0, 4, 6)) 74 imgWithAlpha.Pix = []uint8{ 75 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 76 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 77 0xff, 0x00, 0x00, 0x80, 0xff, 0x00, 0x00, 0x80, 0x00, 0xff, 0x00, 0x80, 0x00, 0xff, 0x00, 0x80, 78 0xff, 0x00, 0x00, 0x80, 0xff, 0x00, 0x00, 0x80, 0x00, 0xff, 0x00, 0x80, 0x00, 0xff, 0x00, 0x80, 79 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x88, 0x88, 0x88, 0x00, 0x88, 0x88, 0x88, 0x00, 80 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x88, 0x88, 0x88, 0x00, 0x88, 0x88, 0x88, 0x00, 81 } 82 83 options := [][]EncodeOption{ 84 { 85 JPEGQuality(100), 86 }, 87 { 88 JPEGQuality(99), 89 GIFDrawer(draw.FloydSteinberg), 90 GIFNumColors(256), 91 GIFQuantizer(quantizer{palette.Plan9}), 92 PNGCompressionLevel(png.BestSpeed), 93 }, 94 } 95 96 dir, err := ioutil.TempDir("", "imaging") 97 if err != nil { 98 t.Fatalf("failed to create temporary directory: %v", err) 99 } 100 defer os.RemoveAll(dir) 101 102 for _, ext := range []string{"jpg", "jpeg", "png", "gif", "bmp", "tif", "tiff"} { 103 filename := filepath.Join(dir, "test."+ext) 104 105 img := imgWithoutAlpha 106 if ext == "png" { 107 img = imgWithAlpha 108 } 109 110 for _, opts := range options { 111 err := Save(img, filename, opts...) 112 if err != nil { 113 t.Fatalf("failed to save image (%q): %v", filename, err) 114 } 115 116 img2, err := Open(filename) 117 if err != nil { 118 t.Fatalf("failed to open image (%q): %v", filename, err) 119 } 120 got := Clone(img2) 121 122 delta := 0 123 if ext == "jpg" || ext == "jpeg" || ext == "gif" { 124 delta = 3 125 } 126 127 if !compareNRGBA(got, img, delta) { 128 t.Fatalf("bad encode-decode result (ext=%q): got %#v want %#v", ext, got, img) 129 } 130 } 131 } 132 133 buf := &bytes.Buffer{} 134 err = Encode(buf, imgWithAlpha, JPEG) 135 if err != nil { 136 t.Fatalf("failed to encode alpha to JPEG: %v", err) 137 } 138 139 buf = &bytes.Buffer{} 140 err = Encode(buf, imgWithAlpha, Format(100)) 141 if err != ErrUnsupportedFormat { 142 t.Fatalf("got %v want ErrUnsupportedFormat", err) 143 } 144 145 buf = bytes.NewBuffer([]byte("bad data")) 146 _, err = Decode(buf) 147 if err == nil { 148 t.Fatalf("decoding bad data: expected error got nil") 149 } 150 151 err = Save(imgWithAlpha, filepath.Join(dir, "test.unknown")) 152 if err != ErrUnsupportedFormat { 153 t.Fatalf("got %v want ErrUnsupportedFormat", err) 154 } 155 156 prevFS := fs 157 fs = badFS{} 158 defer func() { fs = prevFS }() 159 160 err = Save(imgWithAlpha, "test.jpg") 161 if err != errCreate { 162 t.Fatalf("got error %v want errCreate", err) 163 } 164 165 err = Save(imgWithAlpha, "badFile.jpg") 166 if err != errClose { 167 t.Fatalf("got error %v want errClose", err) 168 } 169 170 _, err = Open("test.jpg") 171 if err != errOpen { 172 t.Fatalf("got error %v want errOpen", err) 173 } 174 } 175 176 func TestFormats(t *testing.T) { 177 formatNames := map[Format]string{ 178 JPEG: "JPEG", 179 PNG: "PNG", 180 GIF: "GIF", 181 BMP: "BMP", 182 TIFF: "TIFF", 183 Format(-1): "", 184 } 185 for format, name := range formatNames { 186 got := format.String() 187 if got != name { 188 t.Fatalf("got format name %q want %q", got, name) 189 } 190 } 191 } 192 193 func TestFormatFromExtension(t *testing.T) { 194 testCases := []struct { 195 name string 196 ext string 197 want Format 198 err error 199 }{ 200 { 201 name: "jpg without leading dot", 202 ext: "jpg", 203 want: JPEG, 204 }, 205 { 206 name: "jpg with leading dot", 207 ext: ".jpg", 208 want: JPEG, 209 }, 210 { 211 name: "jpg uppercase", 212 ext: ".JPG", 213 want: JPEG, 214 }, 215 { 216 name: "unsupported", 217 ext: ".unsupportedextension", 218 want: -1, 219 err: ErrUnsupportedFormat, 220 }, 221 } 222 223 for _, tc := range testCases { 224 t.Run(tc.name, func(t *testing.T) { 225 got, err := FormatFromExtension(tc.ext) 226 if err != tc.err { 227 t.Errorf("got error %#v want %#v", err, tc.err) 228 } 229 if got != tc.want { 230 t.Errorf("got result %#v want %#v", got, tc.want) 231 } 232 }) 233 } 234 } 235 236 func TestReadOrientation(t *testing.T) { 237 testCases := []struct { 238 path string 239 orient orientation 240 }{ 241 {"testdata/orientation_0.jpg", 0}, 242 {"testdata/orientation_1.jpg", 1}, 243 {"testdata/orientation_2.jpg", 2}, 244 {"testdata/orientation_3.jpg", 3}, 245 {"testdata/orientation_4.jpg", 4}, 246 {"testdata/orientation_5.jpg", 5}, 247 {"testdata/orientation_6.jpg", 6}, 248 {"testdata/orientation_7.jpg", 7}, 249 {"testdata/orientation_8.jpg", 8}, 250 } 251 for _, tc := range testCases { 252 f, err := os.Open(tc.path) 253 if err != nil { 254 t.Fatalf("%q: failed to open: %v", tc.path, err) 255 } 256 orient := readOrientation(f) 257 if orient != tc.orient { 258 t.Fatalf("%q: got orientation %d want %d", tc.path, orient, tc.orient) 259 } 260 } 261 } 262 263 func TestReadOrientationFails(t *testing.T) { 264 testCases := []struct { 265 name string 266 data string 267 }{ 268 { 269 "empty", 270 "", 271 }, 272 { 273 "missing SOI marker", 274 "\xff\xe1", 275 }, 276 { 277 "missing APP1 marker", 278 "\xff\xd8", 279 }, 280 { 281 "short read marker", 282 "\xff\xd8\xff", 283 }, 284 { 285 "short read block size", 286 "\xff\xd8\xff\xe1\x00", 287 }, 288 { 289 "invalid marker", 290 "\xff\xd8\x00\xe1\x00\x00", 291 }, 292 { 293 "block size too small", 294 "\xff\xd8\xff\xe0\x00\x01", 295 }, 296 { 297 "short read block", 298 "\xff\xd8\xff\xe0\x00\x08\x00", 299 }, 300 { 301 "missing EXIF header", 302 "\xff\xd8\xff\xe1\x00\xff", 303 }, 304 { 305 "invalid EXIF header", 306 "\xff\xd8\xff\xe1\x00\xff\x00\x00\x00\x00", 307 }, 308 { 309 "missing EXIF header tail", 310 "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66", 311 }, 312 { 313 "missing byte order tag", 314 "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00", 315 }, 316 { 317 "invalid byte order tag", 318 "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x00\x00", 319 }, 320 { 321 "missing byte order tail", 322 "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x49\x49", 323 }, 324 { 325 "missing exif offset", 326 "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x49\x49\x00\x2a", 327 }, 328 { 329 "invalid exif offset", 330 "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x07", 331 }, 332 { 333 "read exif offset error", 334 "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x09", 335 }, 336 { 337 "missing number of tags", 338 "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08", 339 }, 340 { 341 "zero number of tags", 342 "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x00", 343 }, 344 { 345 "missing tag", 346 "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x01", 347 }, 348 { 349 "missing tag offset", 350 "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x01\x00\x00", 351 }, 352 { 353 "missing orientation tag", 354 "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 355 }, 356 { 357 "missing orientation tag value offset", 358 "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x01\x01\x12", 359 }, 360 { 361 "missing orientation value", 362 "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x01\x01\x12\x00\x03\x00\x00\x00\x01", 363 }, 364 { 365 "invalid orientation value", 366 "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x01\x01\x12\x00\x03\x00\x00\x00\x01\x00\x09", 367 }, 368 } 369 for _, tc := range testCases { 370 t.Run(tc.name, func(t *testing.T) { 371 if o := readOrientation(strings.NewReader(tc.data)); o != orientationUnspecified { 372 t.Fatalf("got orientation %d want %d", o, orientationUnspecified) 373 } 374 }) 375 } 376 } 377 378 func TestAutoOrientation(t *testing.T) { 379 toBW := func(img image.Image) []byte { 380 b := img.Bounds() 381 data := make([]byte, 0, b.Dx()*b.Dy()) 382 for x := b.Min.X; x < b.Max.X; x++ { 383 for y := b.Min.Y; y < b.Max.Y; y++ { 384 c := color.GrayModel.Convert(img.At(x, y)).(color.Gray) 385 if c.Y < 128 { 386 data = append(data, 1) 387 } else { 388 data = append(data, 0) 389 } 390 } 391 } 392 return data 393 } 394 395 f, err := os.Open("testdata/orientation_0.jpg") 396 if err != nil { 397 t.Fatalf("os.Open(%q): %v", "testdata/orientation_0.jpg", err) 398 } 399 orig, _, err := image.Decode(f) 400 if err != nil { 401 t.Fatalf("image.Decode(%q): %v", "testdata/orientation_0.jpg", err) 402 } 403 origBW := toBW(orig) 404 405 testCases := []struct { 406 path string 407 }{ 408 {"testdata/orientation_0.jpg"}, 409 {"testdata/orientation_1.jpg"}, 410 {"testdata/orientation_2.jpg"}, 411 {"testdata/orientation_3.jpg"}, 412 {"testdata/orientation_4.jpg"}, 413 {"testdata/orientation_5.jpg"}, 414 {"testdata/orientation_6.jpg"}, 415 {"testdata/orientation_7.jpg"}, 416 {"testdata/orientation_8.jpg"}, 417 } 418 for _, tc := range testCases { 419 img, err := Open(tc.path, AutoOrientation(true)) 420 if err != nil { 421 t.Fatal(err) 422 } 423 if img.Bounds() != orig.Bounds() { 424 t.Fatalf("%s: got bounds %v want %v", tc.path, img.Bounds(), orig.Bounds()) 425 } 426 imgBW := toBW(img) 427 if !bytes.Equal(imgBW, origBW) { 428 t.Fatalf("%s: got bw data %v want %v", tc.path, imgBW, origBW) 429 } 430 } 431 432 if _, err := Decode(strings.NewReader("invalid data"), AutoOrientation(true)); err == nil { 433 t.Fatal("expected error got nil") 434 } 435 }