icns

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

bmp.go (3971B)


      1 package ico
      2 
      3 import (
      4 	"encoding/binary"
      5 	"fmt"
      6 	"image"
      7 	"image/color"
      8 )
      9 
     10 // decodeBMP reads the device independent bitmap an icon holds: a header, a
     11 // colour table when the pixels are indexed, the pixels bottom up, and a one
     12 // bit mask. The stored height covers the pixels and the mask together, so it
     13 // is twice the height of the icon.
     14 func decodeBMP(data []byte, width, height int) (image.Image, error) {
     15 	if len(data) < headerSize {
     16 		return nil, fmt.Errorf("%w: holds %d bytes, too few for a bitmap header", ErrMalformed, len(data))
     17 	}
     18 	var (
     19 		infoSize    = int(binary.LittleEndian.Uint32(data[0:4]))
     20 		w           = int(int32(binary.LittleEndian.Uint32(data[4:8])))
     21 		storedH     = int(int32(binary.LittleEndian.Uint32(data[8:12])))
     22 		bits        = int(binary.LittleEndian.Uint16(data[14:16]))
     23 		compression = binary.LittleEndian.Uint32(data[16:20])
     24 		colours     = int(binary.LittleEndian.Uint32(data[32:36]))
     25 	)
     26 	if compression != 0 {
     27 		return nil, fmt.Errorf("%w: the bitmap is compressed", ErrUnsupportedFormat)
     28 	}
     29 	switch bits {
     30 	case 1, 4, 8, 24, 32:
     31 	default:
     32 		return nil, fmt.Errorf("%w: %d bits per pixel", ErrUnsupportedFormat, bits)
     33 	}
     34 	h := storedH / 2
     35 	if storedH <= 0 || storedH%2 != 0 || w <= 0 {
     36 		return nil, fmt.Errorf("%w: the bitmap is %dx%d", ErrMalformed, w, storedH)
     37 	}
     38 	if infoSize < headerSize || infoSize > len(data) {
     39 		return nil, fmt.Errorf("%w: the header claims %d bytes", ErrMalformed, infoSize)
     40 	}
     41 
     42 	// An indexed bitmap carries its own colour table, so nothing outside the
     43 	// file is needed to read it.
     44 	var palette []color.NRGBA
     45 	offset := infoSize
     46 	if bits <= 8 {
     47 		entries := colours
     48 		if entries == 0 {
     49 			entries = 1 << bits
     50 		}
     51 		if offset+entries*4 > len(data) {
     52 			return nil, fmt.Errorf("%w: the colour table of %d runs past the icon", ErrMalformed, entries)
     53 		}
     54 		palette = make([]color.NRGBA, entries)
     55 		for i := range palette {
     56 			at := offset + i*4
     57 			palette[i] = color.NRGBA{R: data[at+2], G: data[at+1], B: data[at], A: 0xFF}
     58 		}
     59 		offset += entries * 4
     60 	}
     61 
     62 	var (
     63 		stride     = ((w*bits + 31) / 32) * 4
     64 		maskStride = ((w + 31) / 32) * 4
     65 		pixelBytes = stride * h
     66 	)
     67 	if offset+pixelBytes > len(data) {
     68 		return nil, fmt.Errorf("%w: %d bytes of pixels do not fit in the icon", ErrMalformed, pixelBytes)
     69 	}
     70 	pixels := data[offset : offset+pixelBytes]
     71 	// The mask is optional in practice: a truncated icon still draws.
     72 	var mask []byte
     73 	if end := offset + pixelBytes + maskStride*h; end <= len(data) {
     74 		mask = data[offset+pixelBytes : end]
     75 	}
     76 
     77 	img := image.NewNRGBA(image.Rect(0, 0, w, h))
     78 	var opaque bool
     79 	for y := 0; y < h; y++ {
     80 		row := pixels[(h-1-y)*stride:]
     81 		for x := 0; x < w; x++ {
     82 			c, err := pixelAt(row, x, bits, palette)
     83 			if err != nil {
     84 				return nil, err
     85 			}
     86 			if c.A != 0 {
     87 				opaque = true
     88 			}
     89 			img.SetNRGBA(x, y, c)
     90 		}
     91 	}
     92 	// A 32 bit icon carries its own alpha, unless whoever wrote it left the
     93 	// channel empty and meant the mask to be read instead.
     94 	if mask != nil && (bits != 32 || !opaque) {
     95 		for y := 0; y < h; y++ {
     96 			row := mask[(h-1-y)*maskStride:]
     97 			for x := 0; x < w; x++ {
     98 				c := img.NRGBAAt(x, y)
     99 				c.A = 0xFF
    100 				if row[x/8]&(0x80>>(x%8)) != 0 {
    101 					c.A = 0
    102 				}
    103 				img.SetNRGBA(x, y, c)
    104 			}
    105 		}
    106 	}
    107 	return img, nil
    108 }
    109 
    110 // pixelAt reads one pixel out of a bitmap row.
    111 func pixelAt(row []byte, x, bits int, palette []color.NRGBA) (color.NRGBA, error) {
    112 	index := func(i byte) (color.NRGBA, error) {
    113 		if int(i) >= len(palette) {
    114 			return color.NRGBA{}, fmt.Errorf("%w: colour %d is outside a table of %d", ErrMalformed, i, len(palette))
    115 		}
    116 		return palette[i], nil
    117 	}
    118 	switch bits {
    119 	case 32:
    120 		return color.NRGBA{R: row[x*4+2], G: row[x*4+1], B: row[x*4], A: row[x*4+3]}, nil
    121 	case 24:
    122 		return color.NRGBA{R: row[x*3+2], G: row[x*3+1], B: row[x*3], A: 0xFF}, nil
    123 	case 8:
    124 		return index(row[x])
    125 	case 4:
    126 		if x%2 == 0 {
    127 			return index(row[x/2] >> 4)
    128 		}
    129 		return index(row[x/2] & 0x0F)
    130 	default:
    131 		return index((row[x/8] >> (7 - x%8)) & 1)
    132 	}
    133 }