icns

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

reader.go (9481B)


      1 package icns
      2 
      3 import (
      4 	"bytes"
      5 	"cmp"
      6 	"encoding/binary"
      7 	"errors"
      8 	"fmt"
      9 	"image"
     10 	"io"
     11 	"slices"
     12 )
     13 
     14 var (
     15 	jpeg2000header = []byte{0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20}
     16 	argbHeader     = []byte("ARGB")
     17 )
     18 
     19 // Decoder reads an icns file and decodes its icons on demand, so a caller
     20 // after one size does not pay for the rest.
     21 type Decoder struct {
     22 	entries []Entry
     23 }
     24 
     25 // NewDecoder reads r and identifies the icons it holds without decoding any
     26 // of their pixels.
     27 func NewDecoder(r io.Reader) (*Decoder, error) {
     28 	entries, err := decode(r)
     29 	if err != nil {
     30 		return nil, err
     31 	}
     32 	// Largest first, and at a given size the one carrying the most colour,
     33 	// since the older files hold several depths of the same icon.
     34 	slices.SortStableFunc(entries, func(a, b Entry) int {
     35 		if order := cmp.Compare(b.Size, a.Size); order != 0 {
     36 			return order
     37 		}
     38 		return cmp.Compare(b.colours(), a.colours())
     39 	})
     40 	return &Decoder{entries: entries}, nil
     41 }
     42 
     43 // Icons returns the icons in the file, largest first.
     44 func (d *Decoder) Icons() []Entry {
     45 	return slices.Clone(d.entries)
     46 }
     47 
     48 // Entry is one icon in an icns file, before its pixels are decoded.
     49 type Entry struct {
     50 	IconDescription
     51 
     52 	data []byte
     53 	// mask holds the alpha channel for ImageFormatRGB icons, when the file
     54 	// carries the matching mask element.
     55 	mask []byte
     56 }
     57 
     58 // Decode decodes the icon's pixels. The formats icns defines itself are
     59 // decoded here; an element holding a whole image file is passed to
     60 // image.Decode, so it is read by whatever the program has registered.
     61 func (e Entry) Decode() (image.Image, error) {
     62 	switch e.ImageFormat {
     63 	case ImageFormatRGB:
     64 		data := e.data
     65 		// it32 is the one colour element that prefixes its planes with four
     66 		// zero bytes.
     67 		if e.ID == "it32" && len(data) >= 4 && binary.BigEndian.Uint32(data[:4]) == 0 {
     68 			data = data[4:]
     69 		}
     70 		img, err := decodeRGB(data, e.mask, int(e.Size))
     71 		if err != nil {
     72 			return nil, fmt.Errorf("decoding icon %s %s: %w", e.OsType, e.ImageFormat, err)
     73 		}
     74 		return img, nil
     75 	case ImageFormatARGB:
     76 		img, err := decodeARGB(e.data[len(argbHeader):], int(e.Size))
     77 		if err != nil {
     78 			return nil, fmt.Errorf("decoding icon %s %s: %w", e.OsType, e.ImageFormat, err)
     79 		}
     80 		return img, nil
     81 	case ImageFormatBitmap, ImageFormatIndexed:
     82 		img, err := e.indexed()
     83 		if err != nil {
     84 			return nil, fmt.Errorf("decoding icon %s %s: %w", e.OsType, e.ImageFormat, err)
     85 		}
     86 		return img, nil
     87 	default:
     88 		img, _, err := image.Decode(bytes.NewReader(e.data))
     89 		if errors.Is(err, image.ErrFormat) {
     90 			return nil, fmt.Errorf("%w: icon %s is %s, which no registered decoder reads", ErrUnsupportedFormat, e.OsType, e.ImageFormat)
     91 		}
     92 		if err != nil {
     93 			return nil, fmt.Errorf("decoding icon %s %s: %w", e.OsType, e.ImageFormat, err)
     94 		}
     95 		return img, nil
     96 	}
     97 }
     98 
     99 // indexed decodes the icon types that hold an index per pixel, finding their
    100 // alpha in the mask half of a companion element or of their own payload.
    101 func (e Entry) indexed() (image.Image, error) {
    102 	var (
    103 		width  = int(e.Size)
    104 		height = int(e.Size)
    105 		bits   = 1
    106 	)
    107 	if e.height > 0 {
    108 		height = int(e.height)
    109 	}
    110 	switch e.enc {
    111 	case encodingIndexed4:
    112 		bits = 4
    113 	case encodingIndexed8:
    114 		bits = 8
    115 	}
    116 	// A "#" element holds its bitmap first and its mask second, whether it
    117 	// is the icon itself or the companion an indexed icon points at.
    118 	var (
    119 		plane = width * height / 8
    120 		mask  = e.mask
    121 	)
    122 	if e.enc == encodingBitmap {
    123 		mask = e.data
    124 	}
    125 	if len(mask) >= plane*2 {
    126 		mask = mask[plane : plane*2]
    127 	} else {
    128 		mask = nil
    129 	}
    130 	return decodeIndexed(e.data, mask, width, height, bits)
    131 }
    132 
    133 // colours ranks how much colour an icon carries, so the richest at a size
    134 // comes first.
    135 func (e Entry) colours() int {
    136 	switch e.enc {
    137 	case encodingBitmap:
    138 		return 0
    139 	case encodingIndexed4:
    140 		return 1
    141 	case encodingIndexed8:
    142 		return 2
    143 	default:
    144 		return 3
    145 	}
    146 }
    147 
    148 // Payload returns the bytes the file stores for the icon. For PNG and JPEG
    149 // 2000 icons it is a complete image file; for the colour and mask types it is
    150 // the run-length encoded colour planes, without the mask that holds their
    151 // alpha.
    152 //
    153 // The bytes are not copied, and must not be modified.
    154 func (e Entry) Payload() []byte {
    155 	return e.data
    156 }
    157 
    158 // Decode returns the largest icon in the icns file that can be decoded,
    159 // ignoring all other sizes. An icon in a format no registered decoder reads
    160 // is passed over, so the result may be smaller than the largest present.
    161 func Decode(r io.Reader) (image.Image, error) {
    162 	d, err := NewDecoder(r)
    163 	if err != nil {
    164 		return nil, err
    165 	}
    166 	for _, icon := range d.entries {
    167 		img, err := icon.Decode()
    168 		if errors.Is(err, ErrUnsupportedFormat) {
    169 			continue
    170 		}
    171 		return img, err
    172 	}
    173 	return nil, fmt.Errorf("%w: no icon is in a format a registered decoder reads", ErrUnsupportedFormat)
    174 }
    175 
    176 // DecodeAll extracts every icon resolution present in the icns data that can
    177 // be decoded. An icon in a format no registered decoder reads is ignored.
    178 func DecodeAll(r io.Reader) (images []image.Image, err error) {
    179 	d, err := NewDecoder(r)
    180 	if err != nil {
    181 		return nil, err
    182 	}
    183 	for _, icon := range d.entries {
    184 		img, err := icon.Decode()
    185 		if errors.Is(err, ErrUnsupportedFormat) {
    186 			continue
    187 		}
    188 		if err != nil {
    189 			return nil, err
    190 		}
    191 		images = append(images, img)
    192 	}
    193 	if len(images) == 0 {
    194 		return nil, fmt.Errorf("%w: no icon is in a format a registered decoder reads", ErrUnsupportedFormat)
    195 	}
    196 	// An element may hold an image of a size other than the one its type
    197 	// names, so order by what was actually decoded.
    198 	slices.SortStableFunc(images, func(a, b image.Image) int {
    199 		left, right := a.Bounds().Size(), b.Bounds().Size()
    200 		return cmp.Compare(right.X+right.Y, left.X+left.Y)
    201 	})
    202 	return images, nil
    203 }
    204 
    205 // Probe extracts descriptions of the icons in the icns, largest first.
    206 func Probe(r io.Reader) (desc []IconDescription, _ error) {
    207 	d, err := NewDecoder(r)
    208 	if err != nil {
    209 		return nil, err
    210 	}
    211 	for _, icon := range d.entries {
    212 		desc = append(desc, icon.IconDescription)
    213 	}
    214 	return desc, nil
    215 }
    216 
    217 // elementHeaderSize is the size of the type and length fields that begin
    218 // every element, the file header included.
    219 const elementHeaderSize = 8
    220 
    221 // element is one type and payload pair from the file.
    222 type element struct {
    223 	id      string
    224 	payload []byte
    225 }
    226 
    227 // decode identifies the icons in the icns without decoding the image data.
    228 //
    229 // An icns file is a sequence of elements, each a 4-byte type followed by a
    230 // 4-byte big-endian length that counts the whole element, header included.
    231 // The file itself is one such element of type "icns" enclosing the rest.
    232 // Every length is checked against the data present, and input that disagrees
    233 // is reported as ErrMalformed.
    234 func decode(r io.Reader) (icons []Entry, err error) {
    235 	elements, err := elementsOf(r)
    236 	if err != nil {
    237 		return nil, err
    238 	}
    239 	// Masks are separate elements that may appear either side of the icon
    240 	// they belong to, so the payloads are indexed before they are paired up.
    241 	payloads := make(map[string][]byte, len(elements))
    242 	for _, el := range elements {
    243 		payloads[el.id] = el.payload
    244 	}
    245 	for _, el := range elements {
    246 		osType, ok := getTypeFromID(el.id)
    247 		if !ok || len(el.payload) == 0 {
    248 			// Elements this package does not read: the table of contents,
    249 			// version and name records, masks, and icon types it cannot
    250 			// decode.
    251 			continue
    252 		}
    253 		icon := Entry{
    254 			IconDescription: IconDescription{OsType: osType},
    255 			data:            el.payload,
    256 		}
    257 		// Several types carry more than one format, so the payload decides
    258 		// wherever it says what it holds.
    259 		switch {
    260 		case osType.enc == encodingRGB:
    261 			icon.ImageFormat = ImageFormatRGB
    262 			icon.mask = payloads[osType.mask]
    263 		case osType.enc == encodingBitmap:
    264 			icon.ImageFormat = ImageFormatBitmap
    265 		case osType.enc == encodingIndexed4, osType.enc == encodingIndexed8:
    266 			icon.ImageFormat = ImageFormatIndexed
    267 			icon.mask = payloads[osType.mask]
    268 		case bytes.HasPrefix(el.payload, argbHeader):
    269 			icon.ImageFormat = ImageFormatARGB
    270 		case bytes.HasPrefix(el.payload, jpeg2000header):
    271 			icon.ImageFormat = ImageFormatJPEG2000
    272 		}
    273 		icons = append(icons, icon)
    274 	}
    275 	if len(icons) == 0 {
    276 		return nil, ErrNoIcons
    277 	}
    278 	return icons, nil
    279 }
    280 
    281 // elementsOf splits an icns file into its elements.
    282 func elementsOf(r io.Reader) ([]element, error) {
    283 	data, err := io.ReadAll(r)
    284 	if err != nil {
    285 		return nil, err
    286 	}
    287 	if len(data) < elementHeaderSize || string(data[0:4]) != "icns" {
    288 		return nil, ErrInvalidHeader
    289 	}
    290 	fileSize := int(binary.BigEndian.Uint32(data[4:8]))
    291 	if fileSize > len(data) {
    292 		return nil, fmt.Errorf("%w: header declares %d bytes but only %d are present", ErrMalformed, fileSize, len(data))
    293 	}
    294 	data = data[:fileSize]
    295 	var elements []element
    296 	for offset := elementHeaderSize; offset < len(data); {
    297 		if len(data)-offset < elementHeaderSize {
    298 			return nil, fmt.Errorf("%w: truncated element header at offset %d", ErrMalformed, offset)
    299 		}
    300 		var (
    301 			id   = string(data[offset : offset+4])
    302 			size = int(binary.BigEndian.Uint32(data[offset+4 : offset+8]))
    303 		)
    304 		if size < elementHeaderSize || size > len(data)-offset {
    305 			return nil, fmt.Errorf("%w: element %q at offset %d declares %d bytes", ErrMalformed, id, offset, size)
    306 		}
    307 		elements = append(elements, element{
    308 			id:      id,
    309 			payload: data[offset+elementHeaderSize : offset+size],
    310 		})
    311 		offset += size
    312 	}
    313 	return elements, nil
    314 }
    315 
    316 func isOsType(ID string) bool {
    317 	_, ok := getTypeFromID(ID)
    318 	return ok
    319 }
    320 
    321 func init() {
    322 	image.RegisterFormat("icns", "icns", Decode, nil)
    323 }