diff --git a/PIL/BmpImagePlugin.py b/PIL/BmpImagePlugin.py index 917d43b9c..8fe537586 100644 --- a/PIL/BmpImagePlugin.py +++ b/PIL/BmpImagePlugin.py @@ -23,13 +23,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 +55,147 @@ def _accept(prefix): return prefix[:2] == b"BM" -## +#=============================================================================== # Image plugin for the Windows BMP format. - +#=============================================================================== class BmpImageFile(ImageFile.ImageFile): - - format = "BMP" + """ Image plugin for the Windows Bitmap 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) + start_data = read(12) + file_info = dict() + file_info['filesize'] = i32(start_data[0:4]) # file size @offset 2 (offsets 4, 12 are reserved for OS/2 Icons) + file_info['image_offset'] = i32(start_data[8:12]) # 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'] = 1 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 + #------------------- Special case : header is reported 40, which + #----------------------- is shorter than real size for bpp >= 16 + if file_info['header_size'] < 64 and file_info['bits'] >= 16: + file_info['r_mask'] = i32(read(4)) + file_info['g_mask'] = i32(read(4)) + file_info['b_mask'] = i32(read(4)) + file_info['a_mask'] = i32(read(4)) + 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']) + + 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)], 16: [(0xf800, 0x7e0, 0x1f), (0x7c00, 0x3e0, 0x1f)]} + 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: + if file_info['bits'] == 32 and file_info['rgba_mask'] in SUPPORTED[file_info['bits']]: + raw_mode = MASK_MODES[(file_info['bits'], file_info['rgba_mask'])] + self.mode = "RGBA" if raw_mode in ("BGRA",) else self.mode + elif file_info['bits'] in (24, 16) and file_info['rgb_mask'] in SUPPORTED[file_info['bits']]: + raw_mode = MASK_MODES[(file_info['bits'], file_info['rgb_mask'])] + else: + raise IOError("BMP images with the provided bitfield mask are not supported") + 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']) + greyscale = True + indices = (0, 255) if file_info['colors'] == 2 else list(range(file_info['colors'])) + #------------------- Check if greyscale and ignore palette if so + for ind, val in enumerate(indices): + rgb = palette[ind*padding:ind*padding + 3] + if rgb != o8(val) * 3: + greyscale = False + #--------- 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" if padding == 4 else "BGR", 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']), file_info['image_offset'], + (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 +204,8 @@ class DibImageFile(BmpImageFile): def _open(self): self._bitmap() + + # # -------------------------------------------------------------------- # Write BMP file @@ -198,6 +215,7 @@ SAVE = { "L": ("L", 8, 256), "P": ("P", 8, 256), "RGB": ("BGR", 24, 0), + "RGBA": ("BGRA", 32, 0), }