mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-10-31 16:07:30 +03:00 
			
		
		
		
	* Drop unittest2 requirement * Use set literals * Use dict/set comprehension * Use str.format() automatic numbering
		
			
				
	
	
		
			296 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			296 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #
 | |
| # The Python Imaging Library.
 | |
| # $Id$
 | |
| #
 | |
| # BMP file handler
 | |
| #
 | |
| # Windows (and OS/2) native bitmap storage format.
 | |
| #
 | |
| # history:
 | |
| # 1995-09-01 fl   Created
 | |
| # 1996-04-30 fl   Added save
 | |
| # 1997-08-27 fl   Fixed save of 1-bit images
 | |
| # 1998-03-06 fl   Load P images as L where possible
 | |
| # 1998-07-03 fl   Load P images as 1 where possible
 | |
| # 1998-12-29 fl   Handle small palettes
 | |
| # 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
 | |
| #
 | |
| # Copyright (c) 1997-2003 by Secret Labs AB
 | |
| # Copyright (c) 1995-2003 by Fredrik Lundh
 | |
| #
 | |
| # See the README file for information on usage and redistribution.
 | |
| #
 | |
| 
 | |
| 
 | |
| from PIL import Image, ImageFile, ImagePalette, _binary
 | |
| import math
 | |
| 
 | |
| __version__ = "0.7"
 | |
| 
 | |
| i8 = _binary.i8
 | |
| i16 = _binary.i16le
 | |
| i32 = _binary.i32le
 | |
| o8 = _binary.o8
 | |
| o16 = _binary.o16le
 | |
| o32 = _binary.o32le
 | |
| 
 | |
| #
 | |
| # --------------------------------------------------------------------
 | |
| # Read BMP file
 | |
| 
 | |
| BIT2MODE = {
 | |
|     # bits => mode, rawmode
 | |
|     1: ("P", "P;1"),
 | |
|     4: ("P", "P;4"),
 | |
|     8: ("P", "P"),
 | |
|     16: ("RGB", "BGR;15"),
 | |
|     24: ("RGB", "BGR"),
 | |
|     32: ("RGB", "BGRX"),
 | |
| }
 | |
| 
 | |
| 
 | |
| 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) """
 | |
| 
 | |
|     # -------------------------------------------------------------- Description
 | |
|     format_description = "Windows Bitmap"
 | |
|     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 _bitmap(self, header=0, offset=0):
 | |
|         """ Read relevant info about the BMP """
 | |
|         read, seek = self.fp.read, self.fp.seek
 | |
|         if header:
 | |
|             seek(header)
 | |
|         file_info = {}
 | |
|         file_info['header_size'] = i32(read(4))  # read bmp header size @offset 14 (this is part of the header size)
 | |
|         file_info['direction'] = -1
 | |
|         # --------------------- If requested, read header at a specific position
 | |
|         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'] >= 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
 | |
|                 self.info["dpi"] = tuple(
 | |
|                     map(lambda x: int(math.ceil(x / 39.3701)),
 | |
|                         file_info['pixels_per_meter']))
 | |
|                 if file_info['compression'] == self.BITFIELDS:
 | |
|                     if len(header_data) >= 52:
 | |
|                         for idx, mask in enumerate(['r_mask', 'g_mask', 'b_mask', 'a_mask']):
 | |
|                             file_info[mask] = i32(header_data[36+idx*4:40+idx*4])
 | |
|                     else:
 | |
|                         # 40 byte headers only have the three components in the bitfields masks,
 | |
|                         # ref: https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx
 | |
|                         # See also https://github.com/python-pillow/Pillow/issues/1293
 | |
|                         # There is a 4th component in the RGBQuad, in the alpha location, but it
 | |
|                         # is listed as a reserved component, and it is not generally an alpha channel
 | |
|                         file_info['a_mask'] = 0x0
 | |
|                         for mask in ['r_mask', 'g_mask', 'b_mask']:
 | |
|                             file_info[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'])
 | |
|         else:
 | |
|             raise IOError("Unsupported BMP header type (%d)" % file_info['header_size'])
 | |
|         # ------------------ Special case : header is reported 40, which
 | |
|         # ---------------------- is shorter than real size for bpp >= 16
 | |
|         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("Unsupported BMP Size: (%dx%d)" % self.size)
 | |
|         # ----------------------- 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("Unsupported BMP pixel depth (%d)" % 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), (0x0, 0x0, 0x0, 0x0), (0xff000000, 0xff0000, 0xff00, 0x0) ],
 | |
|                 24: [(0xff0000, 0xff00, 0xff)],
 | |
|                 16: [(0xf800, 0x7e0, 0x1f), (0x7c00, 0x3e0, 0x1f)]
 | |
|             }
 | |
|             MASK_MODES = {
 | |
|                 (32, (0xff0000, 0xff00, 0xff, 0x0)): "BGRX",
 | |
|                 (32, (0xff000000, 0xff0000, 0xff00, 0x0)): "XBGR",
 | |
|                 (32, (0xff0000, 0xff00, 0xff, 0xff000000)): "BGRA",
 | |
|                 (32, (0x0, 0x0, 0x0, 0x0)): "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("Unsupported BMP bitfields layout")
 | |
|             else:
 | |
|                 raise IOError("Unsupported BMP bitfields layout")
 | |
|         elif file_info['compression'] == self.RAW:
 | |
|             if file_info['bits'] == 32 and header == 22:  # 32-bit .cur offset
 | |
|                 raw_mode, self.mode = "BGRA", "RGBA"
 | |
|         else:
 | |
|             raise IOError("Unsupported BMP compression (%d)" % file_info['compression'])
 | |
|         # ---------------- 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("Unsupported BMP Palette size (%d)" % file_info['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", 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']), offset or self.fp.tell(),
 | |
|                       (raw_mode, ((file_info['width'] * file_info['bits'] + 31) >> 3) & (~3), file_info['direction'])
 | |
|                       )]
 | |
| 
 | |
|     def _open(self):
 | |
|         """ 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("Not 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)
 | |
| 
 | |
| 
 | |
| # ==============================================================================
 | |
| # Image plugin for the DIB format (BMP alias)
 | |
| # ==============================================================================
 | |
| class DibImageFile(BmpImageFile):
 | |
| 
 | |
|     format = "DIB"
 | |
|     format_description = "Windows Bitmap"
 | |
| 
 | |
|     def _open(self):
 | |
|         self._bitmap()
 | |
| 
 | |
| #
 | |
| # --------------------------------------------------------------------
 | |
| # Write BMP file
 | |
| 
 | |
| SAVE = {
 | |
|     "1": ("1", 1, 2),
 | |
|     "L": ("L", 8, 256),
 | |
|     "P": ("P", 8, 256),
 | |
|     "RGB": ("BGR", 24, 0),
 | |
|     "RGBA": ("BGRA", 32, 0),
 | |
| }
 | |
| 
 | |
| 
 | |
| def _save(im, fp, filename, check=0):
 | |
|     try:
 | |
|         rawmode, bits, colors = SAVE[im.mode]
 | |
|     except KeyError:
 | |
|         raise IOError("cannot write mode %s as BMP" % im.mode)
 | |
| 
 | |
|     if check:
 | |
|         return check
 | |
| 
 | |
|     info = im.encoderinfo
 | |
| 
 | |
|     dpi = info.get("dpi", (96, 96))
 | |
| 
 | |
|     # 1 meter == 39.3701 inches
 | |
|     ppm = tuple(map(lambda x: int(x * 39.3701), dpi))
 | |
| 
 | |
|     stride = ((im.size[0]*bits+7)//8+3) & (~3)
 | |
|     header = 40  # or 64 for OS/2 version 2
 | |
|     offset = 14 + header + colors * 4
 | |
|     image = stride * im.size[1]
 | |
| 
 | |
|     # bitmap header
 | |
|     fp.write(b"BM" +                      # file type (magic)
 | |
|              o32(offset+image) +          # file size
 | |
|              o32(0) +                     # reserved
 | |
|              o32(offset))                 # image data offset
 | |
| 
 | |
|     # bitmap info header
 | |
|     fp.write(o32(header) +                # info header size
 | |
|              o32(im.size[0]) +            # width
 | |
|              o32(im.size[1]) +            # height
 | |
|              o16(1) +                     # planes
 | |
|              o16(bits) +                  # depth
 | |
|              o32(0) +                     # compression (0=uncompressed)
 | |
|              o32(image) +                 # size of bitmap
 | |
|              o32(ppm[0]) + o32(ppm[1]) +  # resolution
 | |
|              o32(colors) +                # colors used
 | |
|              o32(colors))                 # colors important
 | |
| 
 | |
|     fp.write(b"\0" * (header - 40))       # padding (for OS/2 format)
 | |
| 
 | |
|     if im.mode == "1":
 | |
|         for i in (0, 255):
 | |
|             fp.write(o8(i) * 4)
 | |
|     elif im.mode == "L":
 | |
|         for i in range(256):
 | |
|             fp.write(o8(i) * 4)
 | |
|     elif im.mode == "P":
 | |
|         fp.write(im.im.getpalette("RGB", "BGRX"))
 | |
| 
 | |
|     ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0,
 | |
|                     (rawmode, stride, -1))])
 | |
| 
 | |
| #
 | |
| # --------------------------------------------------------------------
 | |
| # Registry
 | |
| 
 | |
| Image.register_open(BmpImageFile.format, BmpImageFile, _accept)
 | |
| Image.register_save(BmpImageFile.format, _save)
 | |
| 
 | |
| Image.register_extension(BmpImageFile.format, ".bmp")
 | |
| 
 | |
| Image.register_mime(BmpImageFile.format, "image/bmp")
 |