icns

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

rle_test.go (6562B)


      1 package icns
      2 
      3 import (
      4 	"bytes"
      5 	"errors"
      6 	"image"
      7 	"image/color"
      8 	"testing"
      9 )
     10 
     11 // rleLiterals encodes data as literal runs only, which is the simplest valid
     12 // encoding and always longer than the input.
     13 func rleLiterals(data []byte) []byte {
     14 	var out []byte
     15 	for len(data) > 0 {
     16 		n := min(len(data), 128)
     17 		out = append(out, byte(n-1))
     18 		out = append(out, data[:n]...)
     19 		data = data[n:]
     20 	}
     21 	return out
     22 }
     23 
     24 func TestUnpackRLE(t *testing.T) {
     25 	t.Parallel()
     26 	tests := []struct {
     27 		desc string
     28 		data []byte
     29 		// want is the number of bytes asked for, which the caller knows from
     30 		// the icon's dimensions. It is stated rather than derived, because a
     31 		// stream whose length equals it is read as uncompressed.
     32 		want int
     33 		out  []byte
     34 		err  error
     35 	}{
     36 		{
     37 			desc: "literal run",
     38 			data: []byte{0x02, 0x01, 0x02, 0x03},
     39 			want: 3,
     40 			out:  []byte{0x01, 0x02, 0x03},
     41 		},
     42 		{
     43 			desc: "repeat run",
     44 			data: []byte{0x80, 0x07},
     45 			want: 3,
     46 			out:  []byte{0x07, 0x07, 0x07},
     47 		},
     48 		{
     49 			desc: "literal then repeat",
     50 			data: []byte{0x02, 0x01, 0x02, 0x02, 0x82, 0x03},
     51 			want: 8,
     52 			out:  []byte{0x01, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03},
     53 		},
     54 		{
     55 			desc: "longest repeat",
     56 			data: []byte{0xFF, 0x09},
     57 			want: 130,
     58 			out:  bytes.Repeat([]byte{0x09}, 130),
     59 		},
     60 		{
     61 			desc: "stored uncompressed",
     62 			data: []byte{0x01, 0x02, 0x03},
     63 			want: 3,
     64 			out:  []byte{0x01, 0x02, 0x03},
     65 		},
     66 		{
     67 			desc: "literal run overruns the element",
     68 			data: []byte{0x7F, 0x01, 0x02},
     69 			want: 200,
     70 			err:  ErrMalformed,
     71 		},
     72 		{
     73 			desc: "repeat run with no byte to repeat",
     74 			data: []byte{0x04, 0x01, 0x02, 0x03, 0x04, 0x05, 0x80},
     75 			want: 200,
     76 			err:  ErrMalformed,
     77 		},
     78 		{
     79 			desc: "expands short",
     80 			data: []byte{0x00, 0x01},
     81 			want: 3,
     82 			err:  ErrMalformed,
     83 		},
     84 		{
     85 			desc: "a single run expands past what was asked for",
     86 			data: []byte{0xFF, 0x09},
     87 			want: 3,
     88 			err:  ErrMalformed,
     89 		},
     90 	}
     91 	for _, tt := range tests {
     92 		t.Run(tt.desc, func(st *testing.T) {
     93 			got, _, err := unpackRLE(tt.data, tt.want)
     94 			if tt.err != nil {
     95 				if !errors.Is(err, tt.err) {
     96 					st.Fatalf("error = %v, want %v", err, tt.err)
     97 				}
     98 				return
     99 			}
    100 			if err != nil {
    101 				st.Fatalf("unexpected error: %v", err)
    102 			}
    103 			if !bytes.Equal(got, tt.out) {
    104 				st.Fatalf("got %v, want %v", got, tt.out)
    105 			}
    106 		})
    107 	}
    108 }
    109 
    110 func TestPadRLE(t *testing.T) {
    111 	t.Parallel()
    112 	// Compressed data gets a byte so a reader that drops the last value of
    113 	// the stream loses the padding instead of a pixel.
    114 	if got := padRLE([]byte{0x80, 0x07}, 3); !bytes.Equal(got, []byte{0x80, 0x07, 0x00}) {
    115 		t.Errorf("compressed data = %v, want a trailing zero", got)
    116 	}
    117 	// Uncompressed data has to keep its exact length, which is how a reader
    118 	// tells that it was never compressed.
    119 	raw := []byte{1, 2, 3}
    120 	if got := padRLE(raw, len(raw)); !bytes.Equal(got, raw) {
    121 		t.Errorf("uncompressed data = %v, want it unchanged", got)
    122 	}
    123 	// Whatever the padding, the data still reads back.
    124 	planes := bytes.Repeat([]byte{0x40}, 768)
    125 	out, _, err := unpackRLE(padRLE(packRLE(planes), len(planes)), len(planes))
    126 	if err != nil {
    127 		t.Fatal(err)
    128 	}
    129 	if !bytes.Equal(out, planes) {
    130 		t.Fatal("padded data did not survive the round trip")
    131 	}
    132 }
    133 
    134 // legacyIcon builds an is32 colour element and its s8mk mask for a gradient,
    135 // returning the elements and the image they describe.
    136 func legacyIcon(side int) (rgb, mask []byte, want *image.NRGBA) {
    137 	pixels := side * side
    138 	planes := make([]byte, pixels*3)
    139 	mask = make([]byte, pixels)
    140 	want = image.NewNRGBA(image.Rect(0, 0, side, side))
    141 	for y := 0; y < side; y++ {
    142 		for x := 0; x < side; x++ {
    143 			i := y*side + x
    144 			c := color.NRGBA{
    145 				R: uint8(x * 255 / side),
    146 				G: uint8(y * 255 / side),
    147 				B: 0x40,
    148 				A: uint8((x + y) * 255 / (2 * side)),
    149 			}
    150 			planes[i] = c.R
    151 			planes[pixels+i] = c.G
    152 			planes[pixels*2+i] = c.B
    153 			mask[i] = c.A
    154 			want.SetNRGBA(x, y, c)
    155 		}
    156 	}
    157 	return rleLiterals(planes), mask, want
    158 }
    159 
    160 func TestDecodeLegacyElements(t *testing.T) {
    161 	t.Parallel()
    162 	const side = 16
    163 	rgb, mask, want := legacyIcon(side)
    164 
    165 	t.Run("colour and mask", func(st *testing.T) {
    166 		data := file(encodeElement("is32", rgb), encodeElement("s8mk", mask))
    167 		imgs, err := DecodeAll(bytes.NewReader(data))
    168 		if err != nil {
    169 			st.Fatal(err)
    170 		}
    171 		if len(imgs) != 1 {
    172 			st.Fatalf("decoded %d icons, want 1", len(imgs))
    173 		}
    174 		if !imageCompare(imgs[0], want) {
    175 			st.Fatal("decoded icon differs from the source")
    176 		}
    177 	})
    178 
    179 	t.Run("mask ahead of the colour", func(st *testing.T) {
    180 		// Apple writes the mask after its element, but nothing requires it.
    181 		data := file(encodeElement("s8mk", mask), encodeElement("is32", rgb))
    182 		imgs, err := DecodeAll(bytes.NewReader(data))
    183 		if err != nil {
    184 			st.Fatal(err)
    185 		}
    186 		if !imageCompare(imgs[0], want) {
    187 			st.Fatal("decoded icon differs from the source")
    188 		}
    189 	})
    190 
    191 	t.Run("no mask leaves the icon opaque", func(st *testing.T) {
    192 		data := file(encodeElement("is32", rgb))
    193 		img, err := Decode(bytes.NewReader(data))
    194 		if err != nil {
    195 			st.Fatal(err)
    196 		}
    197 		for y := 0; y < side; y++ {
    198 			for x := 0; x < side; x++ {
    199 				if _, _, _, a := img.At(x, y).RGBA(); a != 0xFFFF {
    200 					st.Fatalf("pixel (%d,%d) alpha = %d, want opaque", x, y, a)
    201 				}
    202 			}
    203 		}
    204 	})
    205 
    206 	t.Run("uncompressed colour", func(st *testing.T) {
    207 		pixels := side * side
    208 		planes := make([]byte, pixels*3)
    209 		for i := range planes {
    210 			planes[i] = byte(i)
    211 		}
    212 		data := file(encodeElement("is32", planes), encodeElement("s8mk", mask))
    213 		img, err := Decode(bytes.NewReader(data))
    214 		if err != nil {
    215 			st.Fatal(err)
    216 		}
    217 		if got := img.Bounds().Dx(); got != side {
    218 			st.Fatalf("decoded a %dpx icon, want %d", got, side)
    219 		}
    220 	})
    221 
    222 	t.Run("mask of the wrong length", func(st *testing.T) {
    223 		data := file(encodeElement("is32", rgb), encodeElement("s8mk", mask[:10]))
    224 		if _, err := Decode(bytes.NewReader(data)); !errors.Is(err, ErrMalformed) {
    225 			st.Fatalf("error = %v, want ErrMalformed", err)
    226 		}
    227 	})
    228 }
    229 
    230 // TestDecodeIT32Header covers the one colour element that prefixes its planes
    231 // with four zero bytes.
    232 func TestDecodeIT32Header(t *testing.T) {
    233 	t.Parallel()
    234 	const side = 128
    235 	pixels := side * side
    236 	planes := make([]byte, pixels*3)
    237 	for i := range planes {
    238 		planes[i] = byte(i / side)
    239 	}
    240 	rgb := append([]byte{0, 0, 0, 0}, rleLiterals(planes)...)
    241 	mask := bytes.Repeat([]byte{0xFF}, pixels)
    242 	data := file(encodeElement("it32", rgb), encodeElement("t8mk", mask))
    243 	img, err := Decode(bytes.NewReader(data))
    244 	if err != nil {
    245 		t.Fatal(err)
    246 	}
    247 	if got := img.Bounds().Size(); got != image.Pt(side, side) {
    248 		t.Fatalf("decoded %v, want %dx%d", got, side, side)
    249 	}
    250 }