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 }