image/bmp: support v4 and v5 info header versions

Decode BITMAPV4INFOHEADER and BITMAPV5INFOHEADER in addition to
BITMAPINFOHEADER and check if any of their features are used. If this is
not the case, the bmp can be decoded as if it had the BITMAPINFOHEADER.
Otherwise an ErrUnsupported is returned.

The colormap.bmp and yellow_rose-small-v5.bmp files were generated using
imagemagick using the following conversions:

convert video-001.bmp -depth 8 -palette colormap.bmp
convert yellow_rose-small.bmp -format BMP5 yellow_rose-small-v5.bmp

The corresponding png files were created using imagemagick convert
without any arguments.

Fixes golang/go#27767

Change-Id: I5c0138b231c68132d39a29c71b61faa546921511
Reviewed-on: https://go-review.googlesource.com/c/141799
Reviewed-by: Nigel Tao <nigeltao@golang.org>
Run-TryBot: Nigel Tao <nigeltao@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
This commit is contained in:
David Heuschmann 2018-10-10 16:55:54 +02:00 committed by Nigel Tao
parent 991ec62608
commit a9455cf03d
6 changed files with 22 additions and 7 deletions

View File

@ -137,20 +137,26 @@ func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown b
// We only support those BMP images that are a BITMAPFILEHEADER // We only support those BMP images that are a BITMAPFILEHEADER
// immediately followed by a BITMAPINFOHEADER. // immediately followed by a BITMAPINFOHEADER.
const ( const (
fileHeaderLen = 14 fileHeaderLen = 14
infoHeaderLen = 40 infoHeaderLen = 40
v4InfoHeaderLen = 108
v5InfoHeaderLen = 124
) )
var b [1024]byte var b [1024]byte
if _, err := io.ReadFull(r, b[:fileHeaderLen+infoHeaderLen]); err != nil { if _, err := io.ReadFull(r, b[:fileHeaderLen+4]); err != nil {
return image.Config{}, 0, false, err return image.Config{}, 0, false, err
} }
if string(b[:2]) != "BM" { if string(b[:2]) != "BM" {
return image.Config{}, 0, false, errors.New("bmp: invalid format") return image.Config{}, 0, false, errors.New("bmp: invalid format")
} }
offset := readUint32(b[10:14]) offset := readUint32(b[10:14])
if readUint32(b[14:18]) != infoHeaderLen { infoLen := readUint32(b[14:18])
if infoLen != infoHeaderLen && infoLen != v4InfoHeaderLen && infoLen != v5InfoHeaderLen {
return image.Config{}, 0, false, ErrUnsupported return image.Config{}, 0, false, ErrUnsupported
} }
if _, err := io.ReadFull(r, b[fileHeaderLen+4:fileHeaderLen+infoLen]); err != nil {
return image.Config{}, 0, false, err
}
width := int(int32(readUint32(b[18:22]))) width := int(int32(readUint32(b[18:22])))
height := int(int32(readUint32(b[22:26]))) height := int(int32(readUint32(b[22:26])))
if height < 0 { if height < 0 {
@ -161,12 +167,19 @@ func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown b
} }
// We only support 1 plane, 8 or 24 bits per pixel and no compression. // We only support 1 plane, 8 or 24 bits per pixel and no compression.
planes, bpp, compression := readUint16(b[26:28]), readUint16(b[28:30]), readUint32(b[30:34]) planes, bpp, compression := readUint16(b[26:28]), readUint16(b[28:30]), readUint32(b[30:34])
// if compression is set to BITFIELDS, but the bitmask is set to the default bitmask
// that would be used if compression was set to 0, we can continue as if compression was 0
if compression == 3 && infoLen > infoHeaderLen &&
readUint32(b[54:58]) == 0xff0000 && readUint32(b[58:62]) == 0xff00 &&
readUint32(b[62:66]) == 0xff && readUint32(b[66:70]) == 0xff000000 {
compression = 0
}
if planes != 1 || compression != 0 { if planes != 1 || compression != 0 {
return image.Config{}, 0, false, ErrUnsupported return image.Config{}, 0, false, ErrUnsupported
} }
switch bpp { switch bpp {
case 8: case 8:
if offset != fileHeaderLen+infoHeaderLen+256*4 { if offset != fileHeaderLen+infoLen+256*4 {
return image.Config{}, 0, false, ErrUnsupported return image.Config{}, 0, false, ErrUnsupported
} }
_, err = io.ReadFull(r, b[:256*4]) _, err = io.ReadFull(r, b[:256*4])
@ -181,12 +194,12 @@ func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown b
} }
return image.Config{ColorModel: pcm, Width: width, Height: height}, 8, topDown, nil return image.Config{ColorModel: pcm, Width: width, Height: height}, 8, topDown, nil
case 24: case 24:
if offset != fileHeaderLen+infoHeaderLen { if offset != fileHeaderLen+infoLen {
return image.Config{}, 0, false, ErrUnsupported return image.Config{}, 0, false, ErrUnsupported
} }
return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 24, topDown, nil return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 24, topDown, nil
case 32: case 32:
if offset != fileHeaderLen+infoHeaderLen { if offset != fileHeaderLen+infoLen {
return image.Config{}, 0, false, ErrUnsupported return image.Config{}, 0, false, ErrUnsupported
} }
return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 32, topDown, nil return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 32, topDown, nil

View File

@ -38,8 +38,10 @@ func compare(img0, img1 image.Image) error {
// same pixel data. // same pixel data.
func TestDecode(t *testing.T) { func TestDecode(t *testing.T) {
testCases := []string{ testCases := []string{
"colormap",
"video-001", "video-001",
"yellow_rose-small", "yellow_rose-small",
"yellow_rose-small-v5",
} }
for _, tc := range testCases { for _, tc := range testCases {

BIN
testdata/colormap.bmp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
testdata/colormap.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
testdata/yellow_rose-small-v5.bmp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

BIN
testdata/yellow_rose-small-v5.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 B