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 }