icns

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

bmp_test.go (6333B)


      1 package ico
      2 
      3 import (
      4 	"bytes"
      5 	"encoding/binary"
      6 	"errors"
      7 	"image/color"
      8 	"testing"
      9 )
     10 
     11 // colourTable builds a table of n entries where index 1 is c.
     12 func colourTable(n int, c color.NRGBA) []byte {
     13 	out := make([]byte, n*4)
     14 	out[4], out[5], out[6] = c.B, c.G, c.R
     15 	return out
     16 }
     17 
     18 // bmpIcon builds one bitmap icon of the given depth, every pixel the same
     19 // colour, with an all opaque mask.
     20 func bmpIcon(size, bits int, c color.NRGBA) []byte {
     21 	var (
     22 		stride     = ((size*bits + 31) / 32) * 4
     23 		maskStride = ((size + 31) / 32) * 4
     24 		palette    []byte
     25 		fill       byte
     26 	)
     27 	switch bits {
     28 	case 1:
     29 		palette, fill = colourTable(2, c), 0xFF
     30 	case 4:
     31 		palette, fill = colourTable(16, c), 0x11
     32 	case 8:
     33 		palette, fill = colourTable(256, c), 0x01
     34 	}
     35 	out := make([]byte, 0, headerSize+len(palette)+stride*size+maskStride*size)
     36 	out = binary.LittleEndian.AppendUint32(out, headerSize)
     37 	out = binary.LittleEndian.AppendUint32(out, uint32(size))
     38 	out = binary.LittleEndian.AppendUint32(out, uint32(size*2))
     39 	out = binary.LittleEndian.AppendUint16(out, 1)
     40 	out = binary.LittleEndian.AppendUint16(out, uint16(bits))
     41 	out = binary.LittleEndian.AppendUint32(out, 0)
     42 	out = binary.LittleEndian.AppendUint32(out, 0)
     43 	out = binary.LittleEndian.AppendUint32(out, 0)
     44 	out = binary.LittleEndian.AppendUint32(out, 0)
     45 	out = binary.LittleEndian.AppendUint32(out, uint32(len(palette)/4))
     46 	out = binary.LittleEndian.AppendUint32(out, 0)
     47 	out = append(out, palette...)
     48 
     49 	rows := make([]byte, stride*size)
     50 	switch bits {
     51 	case 32:
     52 		for y := 0; y < size; y++ {
     53 			row := rows[y*stride:]
     54 			for x := 0; x < size; x++ {
     55 				row[x*4], row[x*4+1], row[x*4+2], row[x*4+3] = c.B, c.G, c.R, c.A
     56 			}
     57 		}
     58 	case 24:
     59 		for y := 0; y < size; y++ {
     60 			row := rows[y*stride:]
     61 			for x := 0; x < size; x++ {
     62 				row[x*3], row[x*3+1], row[x*3+2] = c.B, c.G, c.R
     63 			}
     64 		}
     65 	default:
     66 		for i := range rows {
     67 			rows[i] = fill
     68 		}
     69 	}
     70 	out = append(out, rows...)
     71 	return append(out, make([]byte, maskStride*size)...)
     72 }
     73 
     74 // icoFile wraps one icon in a directory.
     75 func icoFile(size int, data []byte) []byte {
     76 	out := make([]byte, 0, directorySize+entrySize+len(data))
     77 	out = binary.LittleEndian.AppendUint16(out, 0)
     78 	out = binary.LittleEndian.AppendUint16(out, 1)
     79 	out = binary.LittleEndian.AppendUint16(out, 1)
     80 	out = append(out, byte(size), byte(size), 0, 0)
     81 	out = binary.LittleEndian.AppendUint16(out, 1)
     82 	out = binary.LittleEndian.AppendUint16(out, 32)
     83 	out = binary.LittleEndian.AppendUint32(out, uint32(len(data)))
     84 	out = binary.LittleEndian.AppendUint32(out, uint32(directorySize+entrySize))
     85 	return append(out, data...)
     86 }
     87 
     88 // TestDecodeDepths covers the bit depths older icons are written at, which
     89 // carry their own colour table and take their alpha from the mask.
     90 func TestDecodeDepths(t *testing.T) {
     91 	t.Parallel()
     92 	for _, bits := range []int{1, 4, 8, 24, 32} {
     93 		t.Run(map[int]string{1: "one bit", 4: "four bit", 8: "eight bit", 24: "twenty four bit", 32: "thirty two bit"}[bits], func(st *testing.T) {
     94 			want := color.NRGBA{R: 0x30, G: 0x90, B: 0xC0, A: 0xFF}
     95 			img, err := Decode(bytes.NewReader(icoFile(32, bmpIcon(32, bits, want))))
     96 			if err != nil {
     97 				st.Fatal(err)
     98 			}
     99 			if got := img.Bounds().Dx(); got != 32 {
    100 				st.Fatalf("decoded a %dpx icon, want 32", got)
    101 			}
    102 			if got := centre(img); got != want {
    103 				st.Fatalf("centre = %v, want %v", got, want)
    104 			}
    105 		})
    106 	}
    107 }
    108 
    109 // TestDecodeMaskWhenAlphaIsEmpty covers the writers that left the alpha
    110 // channel of a 32 bit icon at zero and meant the mask to be read.
    111 func TestDecodeMaskWhenAlphaIsEmpty(t *testing.T) {
    112 	t.Parallel()
    113 	const size = 16
    114 	data := bmpIcon(size, 32, color.NRGBA{R: 0x80, G: 0x40, B: 0x20, A: 0x00})
    115 	img, err := Decode(bytes.NewReader(icoFile(size, data)))
    116 	if err != nil {
    117 		t.Fatal(err)
    118 	}
    119 	// The mask is all opaque, so the icon has to come back opaque rather
    120 	// than invisible.
    121 	if got := centre(img); got.A != 0xFF {
    122 		t.Fatalf("centre = %v, want it opaque from the mask", got)
    123 	}
    124 }
    125 
    126 func TestDecodeMalformed(t *testing.T) {
    127 	t.Parallel()
    128 	valid := bmpIcon(16, 32, color.NRGBA{A: 0xFF})
    129 	tests := []struct {
    130 		desc string
    131 		data []byte
    132 		want error
    133 	}{
    134 		{"empty", nil, ErrInvalidHeader},
    135 		{"short", []byte{0, 0}, ErrInvalidHeader},
    136 		{"reserved is not zero", []byte{1, 0, 1, 0, 1, 0}, ErrInvalidHeader},
    137 		{"a cursor, not an icon", []byte{0, 0, 2, 0, 1, 0}, ErrInvalidHeader},
    138 		{"no entries", []byte{0, 0, 1, 0, 0, 0}, ErrNoIcons},
    139 		{"truncated directory", []byte{0, 0, 1, 0, 2, 0, 1, 2, 3}, ErrMalformed},
    140 		{"icon lies outside the file", icoFile(16, valid)[:directorySize+entrySize+4], ErrMalformed},
    141 		{"bitmap header is short", icoFile(16, []byte{1, 2, 3}), ErrMalformed},
    142 		{"pixels do not fit", icoFile(16, bmpIcon(16, 32, color.NRGBA{})[:headerSize+16]), ErrMalformed},
    143 	}
    144 	for _, tt := range tests {
    145 		t.Run(tt.desc, func(st *testing.T) {
    146 			if _, err := Decode(bytes.NewReader(tt.data)); !errors.Is(err, tt.want) {
    147 				st.Fatalf("error = %v, want %v", err, tt.want)
    148 			}
    149 		})
    150 	}
    151 }
    152 
    153 func TestDecodeUnsupported(t *testing.T) {
    154 	t.Parallel()
    155 	// Sixteen bits per pixel, which this package does not read.
    156 	odd := bmpIcon(16, 32, color.NRGBA{A: 0xFF})
    157 	binary.LittleEndian.PutUint16(odd[14:16], 16)
    158 	if _, err := Decode(bytes.NewReader(icoFile(16, odd))); !errors.Is(err, ErrUnsupportedFormat) {
    159 		t.Errorf("error = %v, want ErrUnsupportedFormat", err)
    160 	}
    161 	// A compressed bitmap, which this package does not read either.
    162 	compressed := bmpIcon(16, 32, color.NRGBA{A: 0xFF})
    163 	binary.LittleEndian.PutUint32(compressed[16:20], 1)
    164 	if _, err := Decode(bytes.NewReader(icoFile(16, compressed))); !errors.Is(err, ErrUnsupportedFormat) {
    165 		t.Errorf("error = %v, want ErrUnsupportedFormat", err)
    166 	}
    167 }
    168 
    169 // FuzzDecode checks that arbitrary input never panics or hangs the decoder.
    170 func FuzzDecode(f *testing.F) {
    171 	var valid bytes.Buffer
    172 	if err := Encode(&valid, gradient(64)); err != nil {
    173 		f.Fatal(err)
    174 	}
    175 	f.Add(valid.Bytes())
    176 	f.Add([]byte{})
    177 	f.Add([]byte{0, 0, 1, 0, 0, 0})
    178 	for _, bits := range []int{1, 4, 8, 24, 32} {
    179 		f.Add(icoFile(16, bmpIcon(16, bits, color.NRGBA{A: 0xFF})))
    180 	}
    181 	f.Add(icoFile(16, []byte{1, 2, 3}))
    182 	f.Fuzz(func(t *testing.T, data []byte) {
    183 		if d, err := NewDecoder(bytes.NewReader(data)); err == nil {
    184 			for _, icon := range d.Icons() {
    185 				icon.Decode()
    186 			}
    187 		}
    188 		Decode(bytes.NewReader(data))
    189 		DecodeAll(bytes.NewReader(data))
    190 	})
    191 }