icns

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

oracle_test.go (4730B)


      1 //go:build darwin
      2 
      3 package icns
      4 
      5 import (
      6 	"bytes"
      7 	"image"
      8 	"image/color"
      9 	"image/png"
     10 	"os"
     11 	"os/exec"
     12 	"path/filepath"
     13 	"sort"
     14 	"testing"
     15 )
     16 
     17 // These tests hold the package against iconutil, the tool Apple ships, in
     18 // both directions: a file it wrote has to read back as the artwork that went
     19 // in, and a file this package wrote has to come apart under its hands.
     20 
     21 // iconutil runs the tool, failing the test with whatever it printed.
     22 func iconutil(t *testing.T, args ...string) {
     23 	t.Helper()
     24 	cmd := exec.Command("iconutil", args...)
     25 	out, err := cmd.CombinedOutput()
     26 	if err != nil {
     27 		t.Fatalf("iconutil %v: %v\n%s", args, err, out)
     28 	}
     29 }
     30 
     31 func writePNG(t *testing.T, path string, img image.Image) {
     32 	t.Helper()
     33 	f, err := os.Create(path)
     34 	if err != nil {
     35 		t.Fatal(err)
     36 	}
     37 	defer f.Close()
     38 	if err := png.Encode(f, img); err != nil {
     39 		t.Fatal(err)
     40 	}
     41 }
     42 
     43 func readPNG(t *testing.T, path string) image.Image {
     44 	t.Helper()
     45 	f, err := os.Open(path)
     46 	if err != nil {
     47 		t.Fatal(err)
     48 	}
     49 	defer f.Close()
     50 	img, err := png.Decode(f)
     51 	if err != nil {
     52 		t.Fatalf("decoding %s: %v", path, err)
     53 	}
     54 	return img
     55 }
     56 
     57 // oracleArtwork paints one flat colour per slot, so a colour identifies the
     58 // file it came from wherever it ends up.
     59 func oracleArtwork() (map[Slot]image.Image, map[Slot]color.NRGBA) {
     60 	var (
     61 		images = map[Slot]image.Image{}
     62 		want   = map[Slot]color.NRGBA{}
     63 	)
     64 	for i, slot := range Slots() {
     65 		c := color.NRGBA{R: uint8(10 + i*20), G: uint8(200 - i*15), B: 0x40, A: 0xFF}
     66 		images[slot] = solid(int(slot.Pixels()), c)
     67 		want[slot] = c
     68 	}
     69 	return images, want
     70 }
     71 
     72 func listDir(t *testing.T, dir string) []string {
     73 	t.Helper()
     74 	entries, err := os.ReadDir(dir)
     75 	if err != nil {
     76 		t.Fatalf("reading %s: %v", dir, err)
     77 	}
     78 	var names []string
     79 	for _, e := range entries {
     80 		names = append(names, e.Name())
     81 	}
     82 	sort.Strings(names)
     83 	return names
     84 }
     85 
     86 // TestIconutilWrites checks that a file built by Apple's tool reads back as
     87 // the artwork that went into it, element by element.
     88 func TestIconutilWrites(t *testing.T) {
     89 	dir := t.TempDir()
     90 	set := filepath.Join(dir, "Oracle.iconset")
     91 	if err := os.MkdirAll(set, 0o755); err != nil {
     92 		t.Fatal(err)
     93 	}
     94 	images, want := oracleArtwork()
     95 	for slot, img := range images {
     96 		writePNG(t, filepath.Join(set, "icon_"+slot.String()+".png"), img)
     97 	}
     98 	out := filepath.Join(dir, "apple.icns")
     99 	iconutil(t, "-c", "icns", "-o", out, set)
    100 
    101 	data, err := os.ReadFile(out)
    102 	if err != nil {
    103 		t.Fatal(err)
    104 	}
    105 	d, err := NewDecoder(bytes.NewReader(data))
    106 	if err != nil {
    107 		t.Fatalf("decoding what iconutil wrote: %v", err)
    108 	}
    109 	icons := d.Icons()
    110 	if len(icons) == 0 {
    111 		t.Fatal("no icons were read back")
    112 	}
    113 	// Each slot was painted its own colour, so the colour that comes out
    114 	// names the file that went in. That reports the mapping for the types
    115 	// iconutil writes and this package does not, rather than guessing it.
    116 	source := map[color.NRGBA]Slot{}
    117 	for slot, c := range want {
    118 		source[c] = slot
    119 	}
    120 	for _, icon := range icons {
    121 		img, err := icon.Decode()
    122 		if err != nil {
    123 			t.Errorf("%s: %v", icon.ID, err)
    124 			continue
    125 		}
    126 		got := centre(img)
    127 		slot, ok := source[got]
    128 		if !ok {
    129 			t.Errorf("%s holds %v, which is no slot's artwork", icon.ID, got)
    130 			continue
    131 		}
    132 		t.Logf("iconutil wrote %-4s %4dpx from slot %s", icon.ID, img.Bounds().Dx(), slot)
    133 		if px := uint(img.Bounds().Dx()); px != slot.Pixels() {
    134 			t.Errorf("%s is %d pixels, but slot %s is %d", icon.ID, px, slot, slot.Pixels())
    135 		}
    136 		if ours, ok := slotOf[icon.ID]; ok && ours != slot {
    137 			t.Errorf("%s carries slot %s, but this package fills it from %s", icon.ID, slot, ours)
    138 		}
    139 	}
    140 }
    141 
    142 // TestIconutilReads checks that Apple's tool takes a file this package wrote
    143 // apart into the artwork that went in, which covers the run-length encoded
    144 // elements and the table of contents.
    145 func TestIconutilReads(t *testing.T) {
    146 	dir := t.TempDir()
    147 	images, want := oracleArtwork()
    148 	buf := bytes.NewBuffer(nil)
    149 	if err := NewEncoder(buf).EncodeSlots(images); err != nil {
    150 		t.Fatal(err)
    151 	}
    152 	ours := filepath.Join(dir, "ours.icns")
    153 	if err := os.WriteFile(ours, buf.Bytes(), 0o644); err != nil {
    154 		t.Fatal(err)
    155 	}
    156 	set := filepath.Join(dir, "ours.iconset")
    157 	iconutil(t, "-c", "iconset", "-o", set, ours)
    158 
    159 	t.Logf("iconutil extracted %v", listDir(t, set))
    160 	for slot, c := range want {
    161 		path := filepath.Join(set, "icon_"+slot.String()+".png")
    162 		if _, err := os.Stat(path); err != nil {
    163 			t.Errorf("iconutil did not extract slot %s", slot)
    164 			continue
    165 		}
    166 		img := readPNG(t, path)
    167 		if got := uint(img.Bounds().Dx()); got != slot.Pixels() {
    168 			t.Errorf("slot %s came out %d pixels, want %d", slot, got, slot.Pixels())
    169 		}
    170 		if got := centre(img); got != c {
    171 			t.Errorf("slot %s came out %v, want %v", slot, got, c)
    172 		}
    173 	}
    174 }