icns

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

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 }