icns

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

slot_test.go (6278B)


      1 package icns
      2 
      3 import (
      4 	"bytes"
      5 	"image"
      6 	"image/color"
      7 	"reflect"
      8 	"testing"
      9 )
     10 
     11 // slotOf names the element that fills each slot, which is what per-slot
     12 // artwork has to land in.
     13 var slotOf = map[string]Slot{
     14 	"ic10": {512, 2},
     15 	"ic09": {512, 1},
     16 	"ic14": {256, 2},
     17 	"ic08": {256, 1},
     18 	"ic13": {128, 2},
     19 	"ic07": {128, 1},
     20 	"ic12": {32, 2},
     21 	"ic11": {16, 2},
     22 	"il32": {32, 1},
     23 	"is32": {16, 1},
     24 }
     25 
     26 func solid(side int, c color.NRGBA) *image.NRGBA {
     27 	img := image.NewNRGBA(image.Rect(0, 0, side, side))
     28 	for i := 0; i < len(img.Pix); i += 4 {
     29 		img.Pix[i], img.Pix[i+1], img.Pix[i+2], img.Pix[i+3] = c.R, c.G, c.B, c.A
     30 	}
     31 	return img
     32 }
     33 
     34 // centre reports the colour at the middle of img.
     35 func centre(img image.Image) color.NRGBA {
     36 	b := img.Bounds()
     37 	return color.NRGBAModel.Convert(img.At(b.Min.X+b.Dx()/2, b.Min.Y+b.Dy()/2)).(color.NRGBA)
     38 }
     39 
     40 func TestParseSlot(t *testing.T) {
     41 	t.Parallel()
     42 	tests := []struct {
     43 		name string
     44 		want Slot
     45 		ok   bool
     46 	}{
     47 		{"icon_16x16.png", Slot{16, 1}, true},
     48 		{"icon_16x16@2x.png", Slot{16, 2}, true},
     49 		{"icon_32x32@2x.png", Slot{32, 2}, true},
     50 		{"icon_512x512.png", Slot{512, 1}, true},
     51 		{"icon_512x512@2x.png", Slot{512, 2}, true},
     52 		{"icon_16x32.png", Slot{}, false},
     53 		{"icon_0x0.png", Slot{}, false},
     54 		{"icon_16x16@0x.png", Slot{}, false},
     55 		{"icon_16x16.jpg", Slot{}, false},
     56 		{"16x16.png", Slot{}, false},
     57 		{"icon_16x16@2x.png.bak", Slot{}, false},
     58 		{"", Slot{}, false},
     59 	}
     60 	for _, tt := range tests {
     61 		t.Run(tt.name, func(st *testing.T) {
     62 			got, ok := ParseSlot(tt.name)
     63 			if ok != tt.ok || got != tt.want {
     64 				st.Fatalf("ParseSlot(%q) = %v, %v; want %v, %v", tt.name, got, ok, tt.want, tt.ok)
     65 			}
     66 		})
     67 	}
     68 }
     69 
     70 func TestSlotString(t *testing.T) {
     71 	t.Parallel()
     72 	if got := (Slot{16, 1}).String(); got != "16x16" {
     73 		t.Errorf("got %q, want 16x16", got)
     74 	}
     75 	if got := (Slot{512, 2}).String(); got != "512x512@2x" {
     76 		t.Errorf("got %q, want 512x512@2x", got)
     77 	}
     78 	if got := (Slot{16, 2}).Pixels(); got != 32 {
     79 		t.Errorf("16x16@2x is %d pixels, want 32", got)
     80 	}
     81 }
     82 
     83 // TestSlots checks that the slots reported are exactly the ten an iconset
     84 // holds, so a caller knows what artwork to supply.
     85 func TestSlots(t *testing.T) {
     86 	t.Parallel()
     87 	got := Slots()
     88 	if len(got) != len(slotOf) {
     89 		t.Fatalf("Slots returned %d entries, want %d", len(got), len(slotOf))
     90 	}
     91 	seen := map[Slot]bool{}
     92 	for _, slot := range got {
     93 		seen[slot] = true
     94 	}
     95 	for id, slot := range slotOf {
     96 		if !seen[slot] {
     97 			t.Errorf("slot %s, filled by %s, is missing", slot, id)
     98 		}
     99 	}
    100 	// Largest artwork first.
    101 	for i := 1; i < len(got); i++ {
    102 		if got[i].Pixels() > got[i-1].Pixels() {
    103 			t.Fatalf("slot %d (%s) is larger than the one before it (%s)", i, got[i], got[i-1])
    104 		}
    105 	}
    106 }
    107 
    108 // TestEncodeSlots checks that artwork given for a slot reaches the element
    109 // that fills it, including the three pixel sizes two slots share.
    110 func TestEncodeSlots(t *testing.T) {
    111 	t.Parallel()
    112 	var (
    113 		images = map[Slot]image.Image{}
    114 		want   = map[Slot]color.NRGBA{}
    115 	)
    116 	for i, slot := range Slots() {
    117 		c := color.NRGBA{R: uint8(10 + i*20), G: uint8(200 - i*15), B: 0x40, A: 0xFF}
    118 		// Artwork at exactly the slot's size, so nothing is resampled and
    119 		// the colour has to survive exactly.
    120 		images[slot] = solid(int(slot.Pixels()), c)
    121 		want[slot] = c
    122 	}
    123 	buf := bytes.NewBuffer(nil)
    124 	if err := NewEncoder(buf).EncodeSlots(images); err != nil {
    125 		t.Fatal(err)
    126 	}
    127 	d, err := NewDecoder(bytes.NewReader(buf.Bytes()))
    128 	if err != nil {
    129 		t.Fatal(err)
    130 	}
    131 	icons := d.Icons()
    132 	if len(icons) != len(slotOf) {
    133 		t.Fatalf("encoded %d icons, want %d", len(icons), len(slotOf))
    134 	}
    135 	for _, icon := range icons {
    136 		slot, ok := slotOf[icon.ID]
    137 		if !ok {
    138 			t.Fatalf("unexpected element %s", icon.ID)
    139 		}
    140 		img, err := icon.Decode()
    141 		if err != nil {
    142 			t.Fatalf("%s: %v", icon.ID, err)
    143 		}
    144 		if got := uint(img.Bounds().Dx()); got != slot.Pixels() {
    145 			t.Errorf("%s is %d pixels, want %d", icon.ID, got, slot.Pixels())
    146 		}
    147 		if got := centre(img); got != want[slot] {
    148 			t.Errorf("%s fills slot %s with %v, want %v", icon.ID, slot, got, want[slot])
    149 		}
    150 	}
    151 }
    152 
    153 func TestEncodeSlotsFallback(t *testing.T) {
    154 	t.Parallel()
    155 	var (
    156 		source = color.NRGBA{R: 0x20, G: 0x40, B: 0x60, A: 0xFF}
    157 		tuned  = color.NRGBA{R: 0xF0, G: 0x10, B: 0x80, A: 0xFF}
    158 	)
    159 	// Only two slots are given artwork, and the 16 pixel one is supplied at
    160 	// the wrong size so it has to be resized into place.
    161 	images := map[Slot]image.Image{
    162 		{Points: 512, Scale: 2}: solid(1024, source),
    163 		{Points: 16, Scale: 1}:  solid(64, tuned),
    164 	}
    165 	buf := bytes.NewBuffer(nil)
    166 	if err := NewEncoder(buf).EncodeSlots(images); err != nil {
    167 		t.Fatal(err)
    168 	}
    169 	d, err := NewDecoder(bytes.NewReader(buf.Bytes()))
    170 	if err != nil {
    171 		t.Fatal(err)
    172 	}
    173 	if got := len(d.Icons()); got != len(slotOf) {
    174 		t.Fatalf("encoded %d icons, want the full set of %d", got, len(slotOf))
    175 	}
    176 	for _, icon := range d.Icons() {
    177 		img, err := icon.Decode()
    178 		if err != nil {
    179 			t.Fatalf("%s: %v", icon.ID, err)
    180 		}
    181 		want := source
    182 		if icon.ID == "is32" {
    183 			want = tuned
    184 			if got := img.Bounds().Dx(); got != 16 {
    185 				t.Errorf("is32 is %d pixels, want 16", got)
    186 			}
    187 		}
    188 		if got := centre(img); got != want {
    189 			t.Errorf("%s = %v, want %v", icon.ID, got, want)
    190 		}
    191 	}
    192 }
    193 
    194 func TestEncodeSlotsRejects(t *testing.T) {
    195 	t.Parallel()
    196 	buf := bytes.NewBuffer(nil)
    197 	if err := NewEncoder(buf).EncodeSlots(nil); err == nil {
    198 		t.Error("encoding without artwork was accepted")
    199 	}
    200 	if err := NewEncoder(buf).EncodeSlots(map[Slot]image.Image{{16, 1}: nil}); err == nil {
    201 		t.Error("encoding a nil image was accepted")
    202 	}
    203 	if err := NewEncoder(buf).EncodeSlots(map[Slot]image.Image{{16, 1}: solid(8, color.NRGBA{})}); err == nil {
    204 		t.Error("encoding artwork below the smallest icon was accepted")
    205 	}
    206 }
    207 
    208 // TestEncodeSlotsMatchesSingleImage checks the two entry points agree when
    209 // the artwork is the same.
    210 func TestEncodeSlotsMatchesSingleImage(t *testing.T) {
    211 	t.Parallel()
    212 	src := gradient(256)
    213 	var single, slots bytes.Buffer
    214 	if err := Encode(&single, src); err != nil {
    215 		t.Fatal(err)
    216 	}
    217 	if err := NewEncoder(&slots).EncodeSlots(map[Slot]image.Image{{Points: 128, Scale: 2}: src}); err != nil {
    218 		t.Fatal(err)
    219 	}
    220 	if !reflect.DeepEqual(single.Bytes(), slots.Bytes()) {
    221 		t.Fatal("the same artwork through each entry point produced different files")
    222 	}
    223 }