mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-26 18:06:18 +03:00
Fix 32-bit BMP loading (RGBA or RGBX)
PIL choked on perfectly valid BMP files (32 bits with Alpha). It could not handle valid RGBA masks to determine the raw format. To clarify things, I: - Rewrote the `BmpImagePlugin.BmpImageFile` class to be far more readable - Made error messages more explicit (e.g. say that RLE bitmaps are unsupported) - Made a readable dict to contain BMP header information - Kept the existing security checks - Instead of reading palette info by chunks of 3/4 bytes, read the whole palette info at once and parse the data. - Now works with BMPv4/5 with Alpha (and can be exported to alpha PNG for example) - Tested load and save with RGB24, RGB8, RGB8L, RGB32 and RGBA32. - Tested with one bogus file. File not accepted, as expected. I wanted to test more BMP formats, but I could not find that many images. But for all the types I tested, it worked flawlessly.
This commit is contained in:
parent
2c70c9e5e9
commit
456bd96565
|
@ -16,6 +16,7 @@
|
|||
# 2002-12-30 fl Fixed load of 1-bit palette images
|
||||
# 2003-04-21 fl Fixed load of 1-bit monochrome images
|
||||
# 2003-04-23 fl Added limited support for BI_BITFIELDS compression
|
||||
# 2015-03-04 sk Added support for 32-bit images + alpha channel
|
||||
#
|
||||
# Copyright (c) 1997-2003 by Secret Labs AB
|
||||
# Copyright (c) 1995-2003 by Fredrik Lundh
|
||||
|
@ -23,13 +24,12 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
|
||||
__version__ = "0.7"
|
||||
|
||||
|
||||
from PIL import Image, ImageFile, ImagePalette, _binary
|
||||
import math
|
||||
|
||||
|
||||
i8 = _binary.i8
|
||||
i16 = _binary.i16le
|
||||
i32 = _binary.i32le
|
||||
|
@ -56,131 +56,127 @@ def _accept(prefix):
|
|||
return prefix[:2] == b"BM"
|
||||
|
||||
|
||||
##
|
||||
#===============================================================================
|
||||
# Image plugin for the Windows BMP format.
|
||||
|
||||
#===============================================================================
|
||||
class BmpImageFile(ImageFile.ImageFile):
|
||||
""" Image plugin for the Windows Bitmap format (BMP) """
|
||||
|
||||
format = "BMP"
|
||||
#--------------------------------------------------------------- Description
|
||||
format_description = "Windows Bitmap"
|
||||
|
||||
def _bitmap(self, header=0, offset=0):
|
||||
if header:
|
||||
self.fp.seek(header)
|
||||
|
||||
read = self.fp.read
|
||||
|
||||
# CORE/INFO
|
||||
s = read(4)
|
||||
s = s + ImageFile._safe_read(self.fp, i32(s)-4)
|
||||
|
||||
if len(s) == 12:
|
||||
|
||||
# OS/2 1.0 CORE
|
||||
bits = i16(s[10:])
|
||||
self.size = i16(s[4:]), i16(s[6:])
|
||||
compression = 0
|
||||
lutsize = 3
|
||||
colors = 0
|
||||
direction = -1
|
||||
|
||||
elif len(s) in [40, 64, 108, 124]:
|
||||
|
||||
# WIN 3.1 or OS/2 2.0 INFO
|
||||
bits = i16(s[14:])
|
||||
self.size = i32(s[4:]), i32(s[8:])
|
||||
compression = i32(s[16:])
|
||||
pxperm = (i32(s[24:]), i32(s[28:])) # Pixels per meter
|
||||
lutsize = 4
|
||||
colors = i32(s[32:])
|
||||
direction = -1
|
||||
if i8(s[11]) == 0xff:
|
||||
# upside-down storage
|
||||
self.size = self.size[0], 2**32 - self.size[1]
|
||||
direction = 0
|
||||
|
||||
self.info["dpi"] = tuple(map(lambda x: math.ceil(x / 39.3701),
|
||||
pxperm))
|
||||
|
||||
else:
|
||||
raise IOError("Unsupported BMP header type (%d)" % len(s))
|
||||
|
||||
if (self.size[0]*self.size[1]) > 2**31:
|
||||
# Prevent DOS for > 2gb images
|
||||
raise IOError("Unsupported BMP Size: (%dx%d)" % self.size)
|
||||
|
||||
if not colors:
|
||||
colors = 1 << bits
|
||||
|
||||
# MODE
|
||||
try:
|
||||
self.mode, rawmode = BIT2MODE[bits]
|
||||
except KeyError:
|
||||
raise IOError("Unsupported BMP pixel depth (%d)" % bits)
|
||||
|
||||
if compression == 3:
|
||||
# BI_BITFIELDS compression
|
||||
mask = i32(read(4)), i32(read(4)), i32(read(4))
|
||||
if bits == 32 and mask == (0xff0000, 0x00ff00, 0x0000ff):
|
||||
rawmode = "BGRX"
|
||||
elif bits == 16 and mask == (0x00f800, 0x0007e0, 0x00001f):
|
||||
rawmode = "BGR;16"
|
||||
elif bits == 16 and mask == (0x007c00, 0x0003e0, 0x00001f):
|
||||
rawmode = "BGR;15"
|
||||
else:
|
||||
# print bits, map(hex, mask)
|
||||
raise IOError("Unsupported BMP bitfields layout")
|
||||
elif compression != 0:
|
||||
raise IOError("Unsupported BMP compression (%d)" % compression)
|
||||
|
||||
# LUT
|
||||
if self.mode == "P":
|
||||
palette = []
|
||||
greyscale = 1
|
||||
if colors == 2:
|
||||
indices = (0, 255)
|
||||
elif colors > 2**16 or colors <= 0: # We're reading a i32.
|
||||
raise IOError("Unsupported BMP Palette size (%d)" % colors)
|
||||
else:
|
||||
indices = list(range(colors))
|
||||
for i in indices:
|
||||
rgb = read(lutsize)[:3]
|
||||
if rgb != o8(i)*3:
|
||||
greyscale = 0
|
||||
palette.append(rgb)
|
||||
if greyscale:
|
||||
if colors == 2:
|
||||
self.mode = rawmode = "1"
|
||||
else:
|
||||
self.mode = rawmode = "L"
|
||||
else:
|
||||
self.mode = "P"
|
||||
self.palette = ImagePalette.raw(
|
||||
"BGR", b"".join(palette)
|
||||
)
|
||||
|
||||
if not offset:
|
||||
offset = self.fp.tell()
|
||||
|
||||
self.tile = [("raw",
|
||||
(0, 0) + self.size,
|
||||
offset,
|
||||
(rawmode, ((self.size[0]*bits+31) >> 3) & (~3),
|
||||
direction))]
|
||||
|
||||
self.info["compression"] = compression
|
||||
format = "BMP"
|
||||
#---------------------------------------------------- BMP Compression values
|
||||
COMPRESSIONS = {'RAW': 0, 'RLE8': 1, 'RLE4': 2, 'BITFIELDS': 3, 'JPEG': 4, 'PNG': 5}
|
||||
RAW, RLE8, RLE4, BITFIELDS, JPEG, PNG = 0, 1, 2, 3, 4, 5
|
||||
|
||||
def _open(self):
|
||||
|
||||
# HEAD
|
||||
s = self.fp.read(14)
|
||||
if s[:2] != b"BM":
|
||||
raise SyntaxError("Not a BMP file")
|
||||
offset = i32(s[10:])
|
||||
|
||||
""" Open file, check magic number and read header """
|
||||
# read 14 bytes: magic number, filesize, reserved, header final offset
|
||||
head_data = self.fp.read(14)
|
||||
# choke if the file does not have the required magic bytes
|
||||
if head_data[0:2] != b"BM":
|
||||
raise SyntaxError("Expected a BMP file.")
|
||||
# read the start position of the BMP image data (u32)
|
||||
offset = i32(head_data[10:14])
|
||||
# load bitmap information (offset=raster info)
|
||||
self._bitmap(offset=offset)
|
||||
|
||||
def _bitmap(self, header=0, offset=0):
|
||||
""" Read relevant info about the BMP """
|
||||
read, seek = self.fp.read, self.fp.seek
|
||||
seek(2)
|
||||
file_info = dict()
|
||||
file_info['filesize'] = i32(read(12)[0:4]) # file size @offset 2 (offsets 4, 12 are reserved for OS/2 Icons)
|
||||
file_info['header_size'] = i32(read(4)) # read bmp header size @offset 14 (this is part of the header size)
|
||||
file_info['direction'] = -1
|
||||
header_data = ImageFile._safe_read(self.fp, file_info['header_size'] - 4) # read the rest of the bmp header, without its size
|
||||
|
||||
#---------------------------------------------------- IBM OS/2 Bitmap v1
|
||||
#------- This format has different offsets because of width/height types
|
||||
if file_info['header_size'] == 12:
|
||||
file_info['width'] = i16(header_data[0:2])
|
||||
file_info['height'] = i16(header_data[2:4])
|
||||
file_info['planes'] = i16(header_data[4:6])
|
||||
file_info['bits'] = i16(header_data[6:8])
|
||||
file_info['compression'] = self.RAW
|
||||
file_info['palette_padding'] = 3
|
||||
#----------------------------------------------- Windows Bitmap v2 to v5
|
||||
elif file_info['header_size'] in {40, 64, 108, 124}: # v3, OS/2 v2, v4, v5
|
||||
if file_info['header_size'] >= 64:
|
||||
file_info['r_mask'] = i32(header_data[36:40])
|
||||
file_info['g_mask'] = i32(header_data[40:44])
|
||||
file_info['b_mask'] = i32(header_data[44:48])
|
||||
file_info['a_mask'] = i32(header_data[48:52])
|
||||
file_info['rgb_mask'] = (file_info['r_mask'], file_info['g_mask'], file_info['b_mask'])
|
||||
file_info['rgba_mask'] = (file_info['r_mask'], file_info['g_mask'], file_info['b_mask'], file_info['a_mask'])
|
||||
if file_info['header_size'] >= 40: # v3 and OS/2
|
||||
file_info['y_flip'] = i8(header_data[7]) == 0xff
|
||||
file_info['direction'] = 0 if file_info['y_flip'] else -1
|
||||
file_info['width'] = i32(header_data[0:4])
|
||||
file_info['height'] = i32(header_data[4:8]) if not file_info['y_flip'] else 2**32 - i32(header_data[4:8])
|
||||
file_info['planes'] = i16(header_data[8:10])
|
||||
file_info['bits'] = i16(header_data[10:12])
|
||||
file_info['compression'] = i32(header_data[12:16])
|
||||
file_info['data_size'] = i32(header_data[16:20]) # byte size of pixel data
|
||||
file_info['pixels_per_meter'] = (i32(header_data[20:24]), i32(header_data[24:28]))
|
||||
file_info['colors'] = i32(header_data[28:32])
|
||||
file_info['palette_padding'] = 4
|
||||
self.info["dpi"] = tuple(map(lambda x: math.ceil(x / 39.3701), file_info['pixels_per_meter']))
|
||||
else:
|
||||
raise IOError("BMP images with a {0} byte header are not supported".format(file_info['header_size']))
|
||||
self.size = file_info['width'], file_info['height']
|
||||
#--------- If color count was not found in the header, compute from bits
|
||||
file_info['colors'] = file_info['colors'] if file_info.get('colors', 0) else (1 << file_info['bits'])
|
||||
#--------------------------------- Check abnormal values for DOS attacks
|
||||
if file_info['width'] * file_info['height'] > 2**31:
|
||||
raise IOError("BMP images with more than 2 billion pixels are not supported (here {0} pixels)".format(file_info['width'] * file_info['height']))
|
||||
|
||||
#------------------------ Check bit depth for unusual unsupported values
|
||||
self.mode, raw_mode = BIT2MODE.get(file_info['bits'], (None, None))
|
||||
if self.mode is None:
|
||||
raise IOError("BMP images with a {0}-bit pixel depth are not supported".format(file_info['bits']))
|
||||
|
||||
#------------------ Process BMP with Bitfields compression (not palette)
|
||||
if file_info['compression'] == self.BITFIELDS:
|
||||
SUPPORTED = {32: [(0xff0000, 0xff00, 0xff, 0x0), (0xff0000, 0xff00, 0xff, 0xff000000)], 24: [(0xff0000, 0xff00, 0xff, 0x0)], 16: [(0xf800, 0x7e0, 0x1f, 0x0), (0x7c00, 0x3e0, 0x1f, 0x0)]}
|
||||
MASK_MODES = {(32, (0xff0000, 0xff00, 0xff, 0x0)): "BGRX", (32, (0xff0000, 0xff00, 0xff, 0xff000000)): "BGRA", (24, (0xff0000, 0xff00, 0xff)): "BGR", (16, (0xf800, 0x7e0, 0x1f)): "BGR;16", (16, (0x7c00, 0x3e0, 0x1f)): "BGR;15"}
|
||||
if file_info['bits'] in SUPPORTED and file_info['rgba_mask'] in SUPPORTED[file_info['bits']]:
|
||||
raw_mode = MASK_MODES[(file_info['bits'], file_info['rgba_mask'])]
|
||||
if raw_mode in {"BGRA"}:
|
||||
self.mode = "RGBA"
|
||||
else:
|
||||
raise IOError("BMP images with the provided bitfield information are not supported")
|
||||
elif file_info['compression'] != self.RAW:
|
||||
raise IOError("BMP files with RLE (1/2), JPEG (4) and PNG (5) compression are not supported")
|
||||
|
||||
#----------------- Once the header is processed, process the palette/LUT
|
||||
if self.mode == "P": # Paletted for 1, 4 and 8 bit images
|
||||
#------------------------------------------------------ 1-bit images
|
||||
if not (0 < file_info['colors'] <= 65536):
|
||||
raise IOError("BMP palette must have between 1 and 256 colors")
|
||||
else:
|
||||
padding = file_info['palette_padding']
|
||||
palette = read(padding * file_info['colors'])
|
||||
#------------------- Check if greyscale and ignore palette if so
|
||||
greyscale = all([palette[ind] == palette[ind+1] == palette[ind+2] for ind in range(len(palette), padding)])
|
||||
#--------- If all colors are grey, white or black, ditch palette
|
||||
if greyscale:
|
||||
self.mode = "1" if file_info['colors'] == 2 else "L"
|
||||
raw_mode = self.mode
|
||||
else:
|
||||
self.mode = "P"
|
||||
self.palette = ImagePalette.raw("BGRX", b"".join(palette))
|
||||
|
||||
#------------------------------ Finally set the tile data for the plugin
|
||||
self.info['compression'] = file_info['compression']
|
||||
self.tile = [('raw', (0, 0, file_info['width'], file_info['height']), self.fp.tell(),
|
||||
(raw_mode, ((file_info['width'] * file_info['bits'] + 31) >> 3) & (~3), file_info['direction'])
|
||||
)]
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# Image plugin for the DIB format (BMP alias)
|
||||
#===============================================================================
|
||||
class DibImageFile(BmpImageFile):
|
||||
|
||||
format = "DIB"
|
||||
|
@ -189,6 +185,8 @@ class DibImageFile(BmpImageFile):
|
|||
def _open(self):
|
||||
self._bitmap()
|
||||
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# Write BMP file
|
||||
|
@ -198,11 +196,13 @@ SAVE = {
|
|||
"L": ("L", 8, 256),
|
||||
"P": ("P", 8, 256),
|
||||
"RGB": ("BGR", 24, 0),
|
||||
"RGBA": ("BGRA", 32, 0),
|
||||
}
|
||||
|
||||
|
||||
def _save(im, fp, filename, check=0):
|
||||
try:
|
||||
print im.mode
|
||||
rawmode, bits, colors = SAVE[im.mode]
|
||||
except KeyError:
|
||||
raise IOError("cannot write mode %s as BMP" % im.mode)
|
||||
|
|
Loading…
Reference in New Issue
Block a user