icns

Easily create .icns files (Mac Icons) with this Go library or the included CLI.
Log | Files | Refs | LICENSE

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 }