icns

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

writer.go (4591B)


      1 package ico
      2 
      3 import (
      4 	"bytes"
      5 	"encoding/binary"
      6 	"image"
      7 	"image/color"
      8 	"image/png"
      9 
     10 	"github.com/jackmordaunt/icns/v4/internal/resample"
     11 )
     12 
     13 const (
     14 	// directorySize is the header that counts the icons.
     15 	directorySize = 6
     16 	// entrySize is one icon's row in the directory.
     17 	entrySize = 16
     18 	// headerSize is the BITMAPINFOHEADER every bitmap icon begins with.
     19 	headerSize = 40
     20 	// pngAbove is the size from which a PNG is written instead of a bitmap:
     21 	// a 256 pixel bitmap is a quarter of a megabyte on its own.
     22 	pngAbove = 256
     23 )
     24 
     25 // write encodes an icon at every size up to the source, taking artwork from
     26 // images where a size has its own.
     27 func (enc *Encoder) write(images map[uint]image.Image, source image.Image) error {
     28 	if resample.BiggestSide(source) < smallest {
     29 		return ErrImageTooSmall{image: source, need: smallest}
     30 	}
     31 	type icon struct {
     32 		size uint
     33 		data []byte
     34 	}
     35 	var icons []icon
     36 	for _, size := range sizes {
     37 		if size > resample.BiggestSide(source) {
     38 			continue
     39 		}
     40 		art := source
     41 		if supplied, ok := images[size]; ok {
     42 			art = supplied
     43 		}
     44 		scaled := resample.Square(art, size, enc.Algorithm)
     45 		data, err := encodeIcon(scaled, int(size))
     46 		if err != nil {
     47 			return err
     48 		}
     49 		icons = append(icons, icon{size: size, data: data})
     50 	}
     51 	if len(icons) == 0 {
     52 		return ErrImageTooSmall{image: source, need: smallest}
     53 	}
     54 
     55 	out := make([]byte, 0, directorySize+entrySize*len(icons))
     56 	out = binary.LittleEndian.AppendUint16(out, 0) // Reserved.
     57 	out = binary.LittleEndian.AppendUint16(out, 1) // An icon, not a cursor.
     58 	out = binary.LittleEndian.AppendUint16(out, uint16(len(icons)))
     59 	offset := directorySize + entrySize*len(icons)
     60 	for _, ic := range icons {
     61 		// 256 does not fit in a byte and is written as zero.
     62 		side := byte(ic.size)
     63 		out = append(out, side, side, 0, 0)
     64 		out = binary.LittleEndian.AppendUint16(out, 1)  // Colour planes.
     65 		out = binary.LittleEndian.AppendUint16(out, 32) // Bits per pixel.
     66 		out = binary.LittleEndian.AppendUint32(out, uint32(len(ic.data)))
     67 		out = binary.LittleEndian.AppendUint32(out, uint32(offset))
     68 		offset += len(ic.data)
     69 	}
     70 	if _, err := enc.Wr.Write(out); err != nil {
     71 		return err
     72 	}
     73 	for _, ic := range icons {
     74 		if _, err := enc.Wr.Write(ic.data); err != nil {
     75 			return err
     76 		}
     77 	}
     78 	return nil
     79 }
     80 
     81 // encodeIcon stores one icon, as a PNG at the largest size and as a bitmap
     82 // below it.
     83 func encodeIcon(img image.Image, size int) ([]byte, error) {
     84 	if size >= pngAbove {
     85 		buf := bytes.NewBuffer(nil)
     86 		if err := png.Encode(buf, withAlpha{img}); err != nil {
     87 			return nil, err
     88 		}
     89 		return buf.Bytes(), nil
     90 	}
     91 	return encodeBMP(img, size), nil
     92 }
     93 
     94 // withAlpha reports that an image has transparency whatever its pixels hold,
     95 // so image/png gives the icon an alpha channel. Windows reads a PNG icon only
     96 // when it is stored as 32 bit RGBA, and the encoder writes 24 bit truecolour
     97 // for an image that is entirely opaque.
     98 type withAlpha struct{ image.Image }
     99 
    100 func (withAlpha) Opaque() bool { return false }
    101 
    102 // encodeBMP writes the bitmap an icon holds: a header, the pixels bottom up
    103 // in blue, green, red, alpha order, then the one bit mask that predates the
    104 // alpha channel and that some of Windows still reads.
    105 func encodeBMP(img image.Image, size int) []byte {
    106 	var (
    107 		maskStride = ((size + 31) / 32) * 4
    108 		pixels     = size * size * 4
    109 		out        = make([]byte, 0, headerSize+pixels+maskStride*size)
    110 	)
    111 	out = binary.LittleEndian.AppendUint32(out, headerSize)
    112 	out = binary.LittleEndian.AppendUint32(out, uint32(size))
    113 	// The height covers the pixels and the mask together.
    114 	out = binary.LittleEndian.AppendUint32(out, uint32(size*2))
    115 	out = binary.LittleEndian.AppendUint16(out, 1)
    116 	out = binary.LittleEndian.AppendUint16(out, 32)
    117 	out = binary.LittleEndian.AppendUint32(out, 0) // Uncompressed.
    118 	out = binary.LittleEndian.AppendUint32(out, uint32(pixels+maskStride*size))
    119 	out = binary.LittleEndian.AppendUint32(out, 0) // Pixels per metre, across.
    120 	out = binary.LittleEndian.AppendUint32(out, 0) // Pixels per metre, down.
    121 	out = binary.LittleEndian.AppendUint32(out, 0) // Colours used.
    122 	out = binary.LittleEndian.AppendUint32(out, 0) // Colours that matter.
    123 
    124 	origin := img.Bounds().Min
    125 	mask := make([]byte, maskStride*size)
    126 	for y := size - 1; y >= 0; y-- {
    127 		for x := 0; x < size; x++ {
    128 			c := color.NRGBAModel.Convert(img.At(origin.X+x, origin.Y+y)).(color.NRGBA)
    129 			out = append(out, c.B, c.G, c.R, c.A)
    130 			if c.A == 0 {
    131 				// A set bit means the background shows through.
    132 				row := (size - 1 - y) * maskStride
    133 				mask[row+x/8] |= 0x80 >> (x % 8)
    134 			}
    135 		}
    136 	}
    137 	return append(out, mask...)
    138 }