icns_test.go (11938B)
1 package icns 2 3 import ( 4 "bytes" 5 "encoding/binary" 6 "fmt" 7 "image" 8 "image/color" 9 "image/jpeg" 10 "image/png" 11 "io" 12 "reflect" 13 "testing" 14 15 "github.com/jackmordaunt/icns/v4/internal/resample" 16 ) 17 18 // TestDecode relies on Encode being correct. 19 // We are testing that an ICNS with a series of icons will only yield the 20 // largest icon in the series. 21 func TestDecode(t *testing.T) { 22 t.Parallel() 23 tests := []struct { 24 desc string 25 input image.Image 26 want int // Side of the decoded icon. 27 }{ 28 {"valid square icon, exact size", gradient(256), 256}, 29 {"non exact size", gradient(50), 32}, 30 // 32px wide but with Max at 72: measuring Max instead of Dx would 31 // pick the 64px tier and upscale. 32 {"not at origin", gradient(128).SubImage(image.Rect(40, 40, 72, 72)), 32}, 33 } 34 for _, tt := range tests { 35 t.Run(tt.desc, func(st *testing.T) { 36 buf := bytes.NewBuffer(nil) 37 if err := Encode(buf, tt.input); err != nil { 38 st.Fatalf("unexpected error while encoding: %v", err) 39 } 40 img, err := Decode(buf) 41 if err != nil { 42 st.Fatalf("unexpected error: %v", err) 43 } 44 if got := img.Bounds().Size(); got != image.Pt(tt.want, tt.want) { 45 st.Fatalf("decoded icon is %v, want %dx%d", got, tt.want, tt.want) 46 } 47 }) 48 } 49 } 50 51 // TestRoundTrip checks that an image of an exact icon size survives 52 // encode(decode(img)) pixel for pixel: no resampling happens for that size 53 // and PNG is lossless. 54 func TestRoundTrip(t *testing.T) { 55 t.Parallel() 56 src := gradient(128) 57 buf := bytes.NewBuffer(nil) 58 if err := Encode(buf, src); err != nil { 59 t.Fatal(err) 60 } 61 imgs, err := DecodeAll(buf) 62 if err != nil { 63 t.Fatal(err) 64 } 65 // ic07, ic12, ic11, il32 and is32. 66 if len(imgs) != 5 { 67 t.Fatalf("DecodeAll returned %d icons, want 5", len(imgs)) 68 } 69 if !imageCompare(imgs[0], src) { 70 t.Fatal("largest decoded icon differs from the source image") 71 } 72 } 73 74 // TestInterpolationFunctions checks that every algorithm, and any value 75 // outside the enumeration, produces the full icon set at the right sizes. 76 func TestInterpolationFunctions(t *testing.T) { 77 t.Parallel() 78 src := gradient(128) 79 tests := []struct { 80 desc string 81 interp InterpolationFunction 82 }{ 83 {"nearest neighbor", NearestNeighbor}, 84 {"bilinear", Bilinear}, 85 {"bicubic", Bicubic}, 86 {"mitchell-netravali", MitchellNetravali}, 87 {"lanczos2", Lanczos2}, 88 {"lanczos3", Lanczos3}, 89 {"out of range falls back to the default", InterpolationFunction(99)}, 90 } 91 for _, tt := range tests { 92 t.Run(tt.desc, func(st *testing.T) { 93 buf := bytes.NewBuffer(nil) 94 if err := NewEncoder(buf).WithAlgorithm(tt.interp).Encode(src); err != nil { 95 st.Fatalf("encoding: %v", err) 96 } 97 imgs, err := DecodeAll(buf) 98 if err != nil { 99 st.Fatalf("decoding: %v", err) 100 } 101 var sides []int 102 for _, img := range imgs { 103 b := img.Bounds() 104 if b.Dx() != b.Dy() { 105 st.Fatalf("icon is not square: %v", b) 106 } 107 sides = append(sides, b.Dx()) 108 } 109 // 32 twice: ic11 carries 16@2x and il32 carries a true 32. 110 if want := []int{128, 64, 32, 32, 16}; !reflect.DeepEqual(sides, want) { 111 st.Fatalf("icon sides = %v, want %v", sides, want) 112 } 113 // A resampled icon must carry the source's colour, not a blank 114 // or transparent frame. 115 if _, _, _, a := imgs[1].At(32, 32).RGBA(); a == 0 { 116 st.Error("the resampled 64px icon is transparent at its centre") 117 } 118 }) 119 } 120 } 121 122 // TestEncodedElements pins the set of elements the encoder writes, which 123 // matches what iconutil produces for a full iconset. 124 func TestEncodedElements(t *testing.T) { 125 t.Parallel() 126 buf := bytes.NewBuffer(nil) 127 if err := Encode(buf, gradient(1024)); err != nil { 128 t.Fatal(err) 129 } 130 els, err := elementsOf(bytes.NewReader(buf.Bytes())) 131 if err != nil { 132 t.Fatal(err) 133 } 134 var got []string 135 for _, el := range els { 136 got = append(got, el.id) 137 } 138 want := []string{ 139 "TOC ", "ic10", "ic14", "ic09", "ic13", "ic08", "ic07", "ic12", 140 "ic11", "il32", "l8mk", "is32", "s8mk", 141 } 142 if !reflect.DeepEqual(got, want) { 143 t.Fatalf("elements = %v, want %v", got, want) 144 } 145 // The table of contents covers everything after it: type and total size. 146 toc := els[0].payload 147 if len(toc) != (len(els)-1)*elementHeaderSize { 148 t.Fatalf("table of contents is %d bytes, want %d", len(toc), (len(els)-1)*elementHeaderSize) 149 } 150 for i, el := range els[1:] { 151 entry := toc[i*elementHeaderSize:] 152 if id := string(entry[:4]); id != el.id { 153 t.Errorf("entry %d names %q, want %q", i, id, el.id) 154 } 155 size := binary.BigEndian.Uint32(entry[4:8]) 156 if want := uint32(elementHeaderSize + len(el.payload)); size != want { 157 t.Errorf("entry %d for %s gives size %d, want %d", i, el.id, size, want) 158 } 159 } 160 } 161 162 // TestLegacyRoundTrip checks that a 16px source survives the colour and mask 163 // encoding unchanged: it is written at its own size, so nothing is resampled 164 // and the planes are lossless. 165 func TestLegacyRoundTrip(t *testing.T) { 166 t.Parallel() 167 src := gradient(16) 168 buf := bytes.NewBuffer(nil) 169 if err := Encode(buf, src); err != nil { 170 t.Fatal(err) 171 } 172 img, err := Decode(buf) 173 if err != nil { 174 t.Fatal(err) 175 } 176 if !imageCompare(img, src) { 177 t.Fatal("the decoded icon differs from the source") 178 } 179 } 180 181 // TestResizeKeepsColorOutOfTransparentPixels guards the reason resampling 182 // happens in premultiplied space. Filtering an opaque edge against 183 // transparent pixels in straight space drags their colour into the edge, 184 // the familiar dark halo around a downscaled icon. 185 func TestResizeKeepsColorOutOfTransparentPixels(t *testing.T) { 186 t.Parallel() 187 // Left half opaque white, right half transparent black. 188 src := image.NewNRGBA(image.Rect(0, 0, 64, 64)) 189 for y := 0; y < 64; y++ { 190 for x := 0; x < 32; x++ { 191 src.SetNRGBA(x, y, color.NRGBA{R: 255, G: 255, B: 255, A: 255}) 192 } 193 } 194 // Bilinear has no negative lobes, so every output pixel is a plain 195 // average of its neighbours and the expected values are exact. 196 got := resample.Square(src, 32, Bilinear) 197 var blended int 198 for y := got.Bounds().Min.Y; y < got.Bounds().Max.Y; y++ { 199 for x := got.Bounds().Min.X; x < got.Bounds().Max.X; x++ { 200 c := color.NRGBAModel.Convert(got.At(x, y)).(color.NRGBA) 201 if c.A == 0 { 202 continue // Fully transparent: colour is unobservable. 203 } 204 if c.A < 255 { 205 blended++ 206 } 207 if c.R != 255 || c.G != 255 || c.B != 255 { 208 t.Fatalf("pixel (%d,%d) is %v, want white at any alpha", x, y, c) 209 } 210 } 211 } 212 // Without pixels that actually mix the two halves there is nothing to 213 // bleed, and the check above would pass for the wrong reason. 214 if blended == 0 { 215 t.Fatal("no partially transparent pixels: the edge never blended") 216 } 217 } 218 219 // imageCompare reports whether two images have identical bounds and pixels. 220 func imageCompare(left, right image.Image) bool { 221 if left == nil || right == nil { 222 return left == nil && right == nil 223 } 224 lb := left.Bounds() 225 if lb.Size() != right.Bounds().Size() { 226 return false 227 } 228 offset := right.Bounds().Min.Sub(lb.Min) 229 for x := lb.Min.X; x < lb.Max.X; x++ { 230 for y := lb.Min.Y; y < lb.Max.Y; y++ { 231 lr, lg, lbl, la := left.At(x, y).RGBA() 232 rr, rg, rb, ra := right.At(x+offset.X, y+offset.Y).RGBA() 233 if lr != rr || lg != rg || lbl != rb || la != ra { 234 return false 235 } 236 } 237 } 238 return true 239 } 240 241 // TestEncode tests for input validation, sanity checks and errors. 242 // The validity of the encoding is not tested here. 243 // Super large images are not tested because the resizing takes too 244 // long for unit testing. 245 func TestEncode(t *testing.T) { 246 t.Parallel() 247 tests := []struct { 248 desc string 249 wr io.Writer 250 img image.Image 251 252 wantErr bool 253 }{ 254 { 255 "nil image", 256 io.Discard, 257 nil, 258 true, 259 }, 260 { 261 "nil writer", 262 nil, 263 rect(0, 0, 50, 50), 264 true, 265 }, 266 { 267 "valid sqaure", 268 io.Discard, 269 rect(0, 0, 50, 50), 270 false, 271 }, 272 { 273 "valid non-square", 274 io.Discard, 275 rect(0, 0, 10, 50), 276 false, 277 }, 278 { 279 "valid non-square, weird dimensions", 280 io.Discard, 281 rect(0, 0, 17, 77), 282 false, 283 }, 284 { 285 "invalid zero img", 286 io.Discard, 287 rect(0, 0, 0, 0), 288 true, 289 }, 290 { 291 "invalid small img", 292 io.Discard, 293 rect(0, 0, 1, 1), 294 true, 295 }, 296 { 297 "valid square not at origin point", 298 io.Discard, 299 rect(10, 10, 50, 50), 300 false, 301 }, 302 } 303 for _, tt := range tests { 304 t.Run(tt.desc, func(st *testing.T) { 305 err := Encode(tt.wr, tt.img) 306 if tt.wantErr && err == nil { 307 st.Fatal("expected an error") 308 } 309 if !tt.wantErr && err != nil { 310 st.Fatalf("unexpected error: %v", err) 311 } 312 }) 313 } 314 } 315 316 func TestSizesFromMax(t *testing.T) { 317 t.Parallel() 318 tests := []struct { 319 desc string 320 from uint 321 want []uint 322 }{ 323 { 324 "small", 325 100, 326 []uint{64, 32, 16}, 327 }, 328 { 329 "large", 330 99999, 331 []uint{1024, 512, 256, 128, 64, 32, 16}, 332 }, 333 { 334 "smallest", 335 0, 336 []uint{}, 337 }, 338 } 339 for _, tt := range tests { 340 t.Run(tt.desc, func(st *testing.T) { 341 got := sizesFrom(tt.from) 342 if !reflect.DeepEqual(got, tt.want) { 343 st.Errorf("want=%d, got=%d", tt.want, got) 344 } 345 }) 346 } 347 } 348 349 func TestBiggestSide(t *testing.T) { 350 t.Parallel() 351 tests := []struct { 352 desc string 353 img image.Image 354 want uint 355 }{ 356 { 357 "equal", 358 rect(0, 0, 100, 100), 359 100, 360 }, 361 { 362 "right larger", 363 rect(0, 0, 50, 100), 364 100, 365 }, 366 { 367 "left larger", 368 rect(0, 0, 100, 50), 369 100, 370 }, 371 { 372 "off by one", 373 rect(0, 0, 100, 99), 374 100, 375 }, 376 { 377 "empty", 378 rect(0, 0, 0, 0), 379 0, 380 }, 381 { 382 "left empty", 383 rect(0, 0, 0, 10), 384 10, 385 }, 386 { 387 "right empty", 388 rect(0, 0, 10, 0), 389 10, 390 }, 391 } 392 for _, tt := range tests { 393 t.Run(tt.desc, func(st *testing.T) { 394 got := resample.BiggestSide(tt.img) 395 if got != tt.want { 396 st.Errorf("want=%d, got=%d", tt.want, got) 397 } 398 }) 399 } 400 } 401 402 func TestFindNearestSize(t *testing.T) { 403 t.Parallel() 404 tests := []struct { 405 desc string 406 img image.Image 407 want uint 408 }{ 409 { 410 "small", 411 rect(0, 0, 100, 100), 412 64, 413 }, 414 { 415 "very large", 416 rect(0, 0, 123456789, 123456789), 417 1024, 418 }, 419 { 420 "too small", 421 rect(0, 0, 15, 15), 422 0, 423 }, 424 { 425 "off by one", 426 rect(0, 0, 33, 33), 427 32, 428 }, 429 { 430 "exact", 431 rect(0, 0, 256, 256), 432 256, 433 }, 434 { 435 "exact", 436 rect(0, 0, 1024, 1024), 437 1024, 438 }, 439 } 440 for _, tt := range tests { 441 t.Run(tt.desc, func(st *testing.T) { 442 got := findNearestSize(tt.img) 443 if tt.want != got { 444 st.Errorf("want=%d, got=%d", tt.want, got) 445 } 446 }) 447 } 448 } 449 450 func TestEncodeImage(t *testing.T) { 451 t.Parallel() 452 tests := []struct { 453 desc string 454 455 img image.Image 456 format string 457 458 want string 459 }{ 460 { 461 "png - png", 462 _decode(_png(rect(0, 0, 50, 50))), 463 "png", 464 "png", 465 }, 466 { 467 "default png - png", 468 _decode(_png(rect(0, 0, 50, 50))), 469 "", 470 "png", 471 }, 472 { 473 "jpg - jpg", 474 _decode(_jpg(rect(0, 0, 50, 50))), 475 "jpeg", 476 "png", 477 }, 478 { 479 "default jpg - png", 480 _decode(_jpg(rect(0, 0, 50, 50))), 481 "", 482 "png", 483 }, 484 { 485 "invalid format identifier", 486 _decode(_jpg(rect(0, 0, 50, 50))), 487 "asdf", 488 "png", 489 }, 490 { 491 "not actually a jpeg", 492 _decode(_png(rect(0, 0, 50, 50))), 493 "jpeg", 494 "png", 495 }, 496 } 497 for _, tt := range tests { 498 t.Run(tt.desc, func(st *testing.T) { 499 data, err := encodeImage(tt.img) 500 if err != nil { 501 st.Fatalf("encoding image: %v", err) 502 } 503 _, f, err := image.Decode(bytes.NewBuffer(data)) 504 if err != nil { 505 st.Fatalf("decoding iamge: %v", err) 506 } 507 if f != tt.want { 508 st.Fatalf("formats: want=%s, got=%s", tt.want, f) 509 } 510 }) 511 } 512 } 513 514 func rect(x0, y0, x1, y1 int) image.Image { 515 return image.Rect(x0, y0, x1, y1) 516 } 517 518 func _png(img image.Image) io.Reader { 519 buf := bytes.NewBuffer(nil) 520 if err := png.Encode(buf, img); err != nil { 521 panic(fmt.Errorf("encoding png: %w", err)) 522 } 523 return buf 524 } 525 526 func _jpg(img image.Image) io.Reader { 527 buf := bytes.NewBuffer(nil) 528 if err := jpeg.Encode(buf, img, nil); err != nil { 529 panic(fmt.Errorf("encoding jpeg: %w", err)) 530 } 531 return buf 532 } 533 534 func _decode(r io.Reader) image.Image { 535 m, _, err := image.Decode(r) 536 if err != nil { 537 panic(fmt.Errorf("decoding image: %w", err)) 538 } 539 return m 540 }