reader.go (5256B)
1 package ico 2 3 import ( 4 "bytes" 5 "cmp" 6 "encoding/binary" 7 "errors" 8 "fmt" 9 "image" 10 "image/color" 11 "io" 12 "slices" 13 ) 14 15 var pngHeader = []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'} 16 17 // Decoder reads an ico file and decodes its icons on demand, so a caller 18 // after one size does not pay for the rest. 19 type Decoder struct { 20 entries []Entry 21 } 22 23 // NewDecoder reads r and identifies the icons it holds without decoding any 24 // of their pixels. 25 func NewDecoder(r io.Reader) (*Decoder, error) { 26 entries, err := directory(r) 27 if err != nil { 28 return nil, err 29 } 30 // Largest first, keeping file order between icons of equal size. 31 slices.SortStableFunc(entries, func(a, b Entry) int { 32 return cmp.Compare(b.Width*b.Height, a.Width*a.Height) 33 }) 34 return &Decoder{entries: entries}, nil 35 } 36 37 // Icons returns the icons in the file, largest first. 38 func (d *Decoder) Icons() []Entry { 39 return slices.Clone(d.entries) 40 } 41 42 // Entry is one icon in an ico file, before its pixels are decoded. 43 type Entry struct { 44 // Width and Height are the dimensions the directory gives. 45 Width, Height int 46 // Format is how the icon's pixels are stored. 47 Format Format 48 49 data []byte 50 } 51 52 func (e Entry) String() string { 53 return fmt.Sprintf("%dx%d (%s)", e.Width, e.Height, e.Format) 54 } 55 56 // Payload returns the bytes the file stores for the icon. For a PNG icon it 57 // is a whole image file; for a bitmap it is the header, pixels and mask. 58 // 59 // The bytes are not copied, and must not be modified. 60 func (e Entry) Payload() []byte { 61 return e.data 62 } 63 64 // Decode decodes the icon's pixels. A PNG icon is passed to image.Decode, so 65 // it is read by whatever the program has registered. 66 func (e Entry) Decode() (image.Image, error) { 67 if e.Format == FormatPNG { 68 img, _, err := image.Decode(bytes.NewReader(e.data)) 69 if errors.Is(err, image.ErrFormat) { 70 return nil, fmt.Errorf("%w: a %s icon, which no registered decoder reads", ErrUnsupportedFormat, e.Format) 71 } 72 if err != nil { 73 return nil, fmt.Errorf("decoding %s icon: %w", e, err) 74 } 75 return img, nil 76 } 77 img, err := decodeBMP(e.data, e.Width, e.Height) 78 if err != nil { 79 return nil, fmt.Errorf("decoding %s icon: %w", e, err) 80 } 81 return img, nil 82 } 83 84 // directory splits an ico file into the icons it lists. 85 func directory(r io.Reader) ([]Entry, error) { 86 data, err := io.ReadAll(r) 87 if err != nil { 88 return nil, err 89 } 90 if len(data) < directorySize { 91 return nil, ErrInvalidHeader 92 } 93 var ( 94 reserved = binary.LittleEndian.Uint16(data[0:2]) 95 kind = binary.LittleEndian.Uint16(data[2:4]) 96 count = int(binary.LittleEndian.Uint16(data[4:6])) 97 ) 98 if reserved != 0 || kind != 1 { 99 return nil, ErrInvalidHeader 100 } 101 if count == 0 { 102 return nil, ErrNoIcons 103 } 104 if len(data) < directorySize+entrySize*count { 105 return nil, fmt.Errorf("%w: the directory lists %d icons but is truncated", ErrMalformed, count) 106 } 107 entries := make([]Entry, 0, count) 108 for i := 0; i < count; i++ { 109 row := data[directorySize+entrySize*i:] 110 var ( 111 width = int(row[0]) 112 height = int(row[1]) 113 size = int(binary.LittleEndian.Uint32(row[8:12])) 114 offset = int(binary.LittleEndian.Uint32(row[12:16])) 115 ) 116 // Zero stands for 256, which does not fit in a byte. 117 if width == 0 { 118 width = 256 119 } 120 if height == 0 { 121 height = 256 122 } 123 if size < 0 || offset < 0 || offset+size > len(data) || offset < directorySize { 124 return nil, fmt.Errorf("%w: icon %d lies at %d for %d bytes, outside the file", ErrMalformed, i, offset, size) 125 } 126 payload := data[offset : offset+size] 127 entry := Entry{Width: width, Height: height, data: payload} 128 if bytes.HasPrefix(payload, pngHeader) { 129 entry.Format = FormatPNG 130 } 131 entries = append(entries, entry) 132 } 133 return entries, nil 134 } 135 136 // Decode returns the largest icon in the ico file that can be decoded. An 137 // icon in a format no registered decoder reads is passed over, so the result 138 // may be smaller than the largest present. 139 func Decode(r io.Reader) (image.Image, error) { 140 d, err := NewDecoder(r) 141 if err != nil { 142 return nil, err 143 } 144 for _, icon := range d.entries { 145 img, err := icon.Decode() 146 if errors.Is(err, ErrUnsupportedFormat) { 147 continue 148 } 149 return img, err 150 } 151 return nil, fmt.Errorf("%w: no icon is in a format a registered decoder reads", ErrUnsupportedFormat) 152 } 153 154 // DecodeAll extracts every icon in the file that can be decoded, largest 155 // first. 156 func DecodeAll(r io.Reader) (images []image.Image, err error) { 157 d, err := NewDecoder(r) 158 if err != nil { 159 return nil, err 160 } 161 for _, icon := range d.entries { 162 img, err := icon.Decode() 163 if errors.Is(err, ErrUnsupportedFormat) { 164 continue 165 } 166 if err != nil { 167 return nil, err 168 } 169 images = append(images, img) 170 } 171 if len(images) == 0 { 172 return nil, fmt.Errorf("%w: no icon is in a format a registered decoder reads", ErrUnsupportedFormat) 173 } 174 return images, nil 175 } 176 177 // DecodeConfig returns the dimensions of the largest icon in the file. 178 func DecodeConfig(r io.Reader) (image.Config, error) { 179 d, err := NewDecoder(r) 180 if err != nil { 181 return image.Config{}, err 182 } 183 largest := d.entries[0] 184 return image.Config{ 185 Width: largest.Width, 186 Height: largest.Height, 187 ColorModel: color.NRGBAModel, 188 }, nil 189 } 190 191 func init() { 192 image.RegisterFormat("ico", "\x00\x00\x01\x00", Decode, DecodeConfig) 193 }