icns

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

ico_test.go (5386B)


      1 package ico
      2 
      3 import (
      4 	"bytes"
      5 	"encoding/binary"
      6 	"image"
      7 	"image/color"
      8 	"reflect"
      9 	"testing"
     10 )
     11 
     12 // gradient returns a side by side image whose colour and alpha vary with
     13 // position, so a round trip cannot pass by accident.
     14 func gradient(side int) *image.NRGBA {
     15 	img := image.NewNRGBA(image.Rect(0, 0, side, side))
     16 	for y := 0; y < side; y++ {
     17 		for x := 0; x < side; x++ {
     18 			img.SetNRGBA(x, y, color.NRGBA{
     19 				R: uint8(x * 255 / side),
     20 				G: uint8(y * 255 / side),
     21 				B: uint8((x + y) * 255 / (2 * side)),
     22 				A: uint8(255 - y*255/(2*side)),
     23 			})
     24 		}
     25 	}
     26 	return img
     27 }
     28 
     29 func solid(side int, c color.NRGBA) *image.NRGBA {
     30 	img := image.NewNRGBA(image.Rect(0, 0, side, side))
     31 	for i := 0; i < len(img.Pix); i += 4 {
     32 		img.Pix[i], img.Pix[i+1], img.Pix[i+2], img.Pix[i+3] = c.R, c.G, c.B, c.A
     33 	}
     34 	return img
     35 }
     36 
     37 func centre(img image.Image) color.NRGBA {
     38 	b := img.Bounds()
     39 	return color.NRGBAModel.Convert(img.At(b.Min.X+b.Dx()/2, b.Min.Y+b.Dy()/2)).(color.NRGBA)
     40 }
     41 
     42 func same(a, b image.Image) bool {
     43 	if a.Bounds().Size() != b.Bounds().Size() {
     44 		return false
     45 	}
     46 	ab, bb := a.Bounds(), b.Bounds()
     47 	for y := 0; y < ab.Dy(); y++ {
     48 		for x := 0; x < ab.Dx(); x++ {
     49 			if a.At(ab.Min.X+x, ab.Min.Y+y) != b.At(bb.Min.X+x, bb.Min.Y+y) {
     50 				ar, ag, al, aa := a.At(ab.Min.X+x, ab.Min.Y+y).RGBA()
     51 				br, bg, bl, ba := b.At(bb.Min.X+x, bb.Min.Y+y).RGBA()
     52 				if ar != br || ag != bg || al != bl || aa != ba {
     53 					return false
     54 				}
     55 			}
     56 		}
     57 	}
     58 	return true
     59 }
     60 
     61 func TestEncode(t *testing.T) {
     62 	t.Parallel()
     63 	buf := bytes.NewBuffer(nil)
     64 	if err := Encode(buf, gradient(256)); err != nil {
     65 		t.Fatal(err)
     66 	}
     67 	d, err := NewDecoder(bytes.NewReader(buf.Bytes()))
     68 	if err != nil {
     69 		t.Fatal(err)
     70 	}
     71 	var (
     72 		got     []int
     73 		formats []Format
     74 	)
     75 	for _, icon := range d.Icons() {
     76 		got = append(got, icon.Width)
     77 		formats = append(formats, icon.Format)
     78 		if icon.Width != icon.Height {
     79 			t.Errorf("icon %s is not square", icon)
     80 		}
     81 	}
     82 	if want := []int{256, 128, 64, 48, 32, 24, 16}; !reflect.DeepEqual(got, want) {
     83 		t.Fatalf("sizes = %v, want %v", got, want)
     84 	}
     85 	// The largest is a PNG, since a bitmap at 256 is a quarter of a megabyte.
     86 	want := []Format{FormatPNG, FormatBMP, FormatBMP, FormatBMP, FormatBMP, FormatBMP, FormatBMP}
     87 	if !reflect.DeepEqual(formats, want) {
     88 		t.Fatalf("formats = %v, want %v", formats, want)
     89 	}
     90 }
     91 
     92 // TestDirectory reads the directory by hand, since every reader of the file
     93 // finds its icons through those offsets.
     94 func TestDirectory(t *testing.T) {
     95 	t.Parallel()
     96 	buf := bytes.NewBuffer(nil)
     97 	if err := Encode(buf, gradient(64)); err != nil {
     98 		t.Fatal(err)
     99 	}
    100 	data := buf.Bytes()
    101 	if got := binary.LittleEndian.Uint16(data[0:2]); got != 0 {
    102 		t.Errorf("reserved = %d, want 0", got)
    103 	}
    104 	if got := binary.LittleEndian.Uint16(data[2:4]); got != 1 {
    105 		t.Errorf("type = %d, want 1 for an icon", got)
    106 	}
    107 	count := int(binary.LittleEndian.Uint16(data[4:6]))
    108 	if count != 5 { // 64, 48, 32, 24 and 16.
    109 		t.Fatalf("count = %d, want 5", count)
    110 	}
    111 	offset := directorySize + entrySize*count
    112 	for i := 0; i < count; i++ {
    113 		row := data[directorySize+entrySize*i:]
    114 		size := int(binary.LittleEndian.Uint32(row[8:12]))
    115 		at := int(binary.LittleEndian.Uint32(row[12:16]))
    116 		if at != offset {
    117 			t.Errorf("icon %d starts at %d, want %d, so the images are not packed in order", i, at, offset)
    118 		}
    119 		if at+size > len(data) {
    120 			t.Fatalf("icon %d runs past the end of the file", i)
    121 		}
    122 		offset += size
    123 	}
    124 	if offset != len(data) {
    125 		t.Errorf("the icons end at %d but the file is %d bytes", offset, len(data))
    126 	}
    127 }
    128 
    129 // TestRoundTrip checks that an image at an exact icon size comes back
    130 // unchanged: nothing is resampled and a 32 bit bitmap is lossless.
    131 func TestRoundTrip(t *testing.T) {
    132 	t.Parallel()
    133 	src := gradient(64)
    134 	buf := bytes.NewBuffer(nil)
    135 	if err := Encode(buf, src); err != nil {
    136 		t.Fatal(err)
    137 	}
    138 	img, err := Decode(bytes.NewReader(buf.Bytes()))
    139 	if err != nil {
    140 		t.Fatal(err)
    141 	}
    142 	if !same(img, src) {
    143 		t.Fatal("the decoded icon differs from the source")
    144 	}
    145 }
    146 
    147 func TestEncodeSizes(t *testing.T) {
    148 	t.Parallel()
    149 	var (
    150 		images = map[uint]image.Image{}
    151 		want   = map[uint]color.NRGBA{}
    152 	)
    153 	for i, size := range Sizes() {
    154 		c := color.NRGBA{R: uint8(20 + i*30), G: uint8(200 - i*20), B: 0x40, A: 0xFF}
    155 		images[size] = solid(int(size), c)
    156 		want[size] = c
    157 	}
    158 	buf := bytes.NewBuffer(nil)
    159 	if err := NewEncoder(buf).EncodeSizes(images); err != nil {
    160 		t.Fatal(err)
    161 	}
    162 	d, err := NewDecoder(bytes.NewReader(buf.Bytes()))
    163 	if err != nil {
    164 		t.Fatal(err)
    165 	}
    166 	icons := d.Icons()
    167 	if len(icons) != len(want) {
    168 		t.Fatalf("encoded %d icons, want %d", len(icons), len(want))
    169 	}
    170 	for _, icon := range icons {
    171 		img, err := icon.Decode()
    172 		if err != nil {
    173 			t.Fatalf("%s: %v", icon, err)
    174 		}
    175 		if got := centre(img); got != want[uint(icon.Width)] {
    176 			t.Errorf("the %d pixel icon is %v, want %v", icon.Width, got, want[uint(icon.Width)])
    177 		}
    178 	}
    179 }
    180 
    181 func TestEncodeRejects(t *testing.T) {
    182 	t.Parallel()
    183 	buf := bytes.NewBuffer(nil)
    184 	if err := Encode(buf, nil); err == nil {
    185 		t.Error("encoding a nil image was accepted")
    186 	}
    187 	if err := Encode(nil, gradient(64)); err == nil {
    188 		t.Error("encoding to a nil writer was accepted")
    189 	}
    190 	if err := Encode(buf, gradient(8)); err == nil {
    191 		t.Error("encoding an image below the smallest icon was accepted")
    192 	}
    193 	if err := NewEncoder(buf).EncodeSizes(nil); err == nil {
    194 		t.Error("encoding without artwork was accepted")
    195 	}
    196 }