| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| |
|
| | import os |
| |
|
| | from . import Image, ImageFile, ImagePalette |
| | from ._binary import i16le as i16 |
| | from ._binary import i32le as i32 |
| | from ._binary import o8 |
| | from ._binary import o16le as o16 |
| | from ._binary import o32le as o32 |
| |
|
| | |
| | |
| | |
| |
|
| | BIT2MODE = { |
| | |
| | 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" |
| |
|
| |
|
| | def _dib_accept(prefix): |
| | return i32(prefix) in [12, 40, 64, 108, 124] |
| |
|
| |
|
| | |
| | |
| | |
| | class BmpImageFile(ImageFile.ImageFile): |
| | """Image plugin for the Windows Bitmap format (BMP)""" |
| |
|
| | |
| | format_description = "Windows Bitmap" |
| | format = "BMP" |
| |
|
| | |
| | COMPRESSIONS = {"RAW": 0, "RLE8": 1, "RLE4": 2, "BITFIELDS": 3, "JPEG": 4, "PNG": 5} |
| | for k, v in COMPRESSIONS.items(): |
| | vars()[k] = v |
| |
|
| | 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 = {"header_size": i32(read(4)), "direction": -1} |
| |
|
| | |
| | |
| | header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4) |
| |
|
| | |
| | |
| | if file_info["header_size"] == 12: |
| | file_info["width"] = i16(header_data, 0) |
| | file_info["height"] = i16(header_data, 2) |
| | file_info["planes"] = i16(header_data, 4) |
| | file_info["bits"] = i16(header_data, 6) |
| | file_info["compression"] = self.RAW |
| | file_info["palette_padding"] = 3 |
| |
|
| | |
| | |
| | elif file_info["header_size"] in (40, 64, 108, 124): |
| | file_info["y_flip"] = header_data[7] == 0xFF |
| | file_info["direction"] = 1 if file_info["y_flip"] else -1 |
| | file_info["width"] = i32(header_data, 0) |
| | file_info["height"] = ( |
| | i32(header_data, 4) |
| | if not file_info["y_flip"] |
| | else 2**32 - i32(header_data, 4) |
| | ) |
| | file_info["planes"] = i16(header_data, 8) |
| | file_info["bits"] = i16(header_data, 10) |
| | file_info["compression"] = i32(header_data, 12) |
| | |
| | file_info["data_size"] = i32(header_data, 16) |
| | file_info["pixels_per_meter"] = ( |
| | i32(header_data, 20), |
| | i32(header_data, 24), |
| | ) |
| | file_info["colors"] = i32(header_data, 28) |
| | file_info["palette_padding"] = 4 |
| | self.info["dpi"] = tuple(x / 39.3701 for x in 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) |
| | else: |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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 OSError(f"Unsupported BMP header type ({file_info['header_size']})") |
| |
|
| | |
| | |
| | self._size = file_info["width"], file_info["height"] |
| |
|
| | |
| | file_info["colors"] = ( |
| | file_info["colors"] |
| | if file_info.get("colors", 0) |
| | else (1 << file_info["bits"]) |
| | ) |
| | if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8: |
| | offset += 4 * file_info["colors"] |
| |
|
| | |
| | self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) |
| | if self.mode is None: |
| | raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})") |
| |
|
| | |
| | decoder_name = "raw" |
| | if file_info["compression"] == self.BITFIELDS: |
| | SUPPORTED = { |
| | 32: [ |
| | (0xFF0000, 0xFF00, 0xFF, 0x0), |
| | (0xFF000000, 0xFF0000, 0xFF00, 0x0), |
| | (0xFF000000, 0xFF0000, 0xFF00, 0xFF), |
| | (0xFF, 0xFF00, 0xFF0000, 0xFF000000), |
| | (0xFF0000, 0xFF00, 0xFF, 0xFF000000), |
| | (0x0, 0x0, 0x0, 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, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR", |
| | (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA", |
| | (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 "A" in raw_mode 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 OSError("Unsupported BMP bitfields layout") |
| | else: |
| | raise OSError("Unsupported BMP bitfields layout") |
| | elif file_info["compression"] == self.RAW: |
| | if file_info["bits"] == 32 and header == 22: |
| | raw_mode, self.mode = "BGRA", "RGBA" |
| | elif file_info["compression"] in (self.RLE8, self.RLE4): |
| | decoder_name = "bmp_rle" |
| | else: |
| | raise OSError(f"Unsupported BMP compression ({file_info['compression']})") |
| |
|
| | |
| | if self.mode == "P": |
| |
|
| | |
| | if not (0 < file_info["colors"] <= 65536): |
| | raise OSError(f"Unsupported BMP Palette size ({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"])) |
| | ) |
| |
|
| | |
| | for ind, val in enumerate(indices): |
| | rgb = palette[ind * padding : ind * padding + 3] |
| | if rgb != o8(val) * 3: |
| | greyscale = False |
| |
|
| | |
| | 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 |
| | ) |
| |
|
| | |
| | self.info["compression"] = file_info["compression"] |
| | args = [raw_mode] |
| | if decoder_name == "bmp_rle": |
| | args.append(file_info["compression"] == self.RLE4) |
| | else: |
| | args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3)) |
| | args.append(file_info["direction"]) |
| | self.tile = [ |
| | ( |
| | decoder_name, |
| | (0, 0, file_info["width"], file_info["height"]), |
| | offset or self.fp.tell(), |
| | tuple(args), |
| | ) |
| | ] |
| |
|
| | def _open(self): |
| | """Open file, check magic number and read header""" |
| | |
| | head_data = self.fp.read(14) |
| | |
| | if not _accept(head_data): |
| | raise SyntaxError("Not a BMP file") |
| | |
| | offset = i32(head_data, 10) |
| | |
| | self._bitmap(offset=offset) |
| |
|
| |
|
| | class BmpRleDecoder(ImageFile.PyDecoder): |
| | _pulls_fd = True |
| |
|
| | def decode(self, buffer): |
| | rle4 = self.args[1] |
| | data = bytearray() |
| | x = 0 |
| | while len(data) < self.state.xsize * self.state.ysize: |
| | pixels = self.fd.read(1) |
| | byte = self.fd.read(1) |
| | if not pixels or not byte: |
| | break |
| | num_pixels = pixels[0] |
| | if num_pixels: |
| | |
| | if x + num_pixels > self.state.xsize: |
| | |
| | num_pixels = max(0, self.state.xsize - x) |
| | if rle4: |
| | first_pixel = o8(byte[0] >> 4) |
| | second_pixel = o8(byte[0] & 0x0F) |
| | for index in range(num_pixels): |
| | if index % 2 == 0: |
| | data += first_pixel |
| | else: |
| | data += second_pixel |
| | else: |
| | data += byte * num_pixels |
| | x += num_pixels |
| | else: |
| | if byte[0] == 0: |
| | |
| | while len(data) % self.state.xsize != 0: |
| | data += b"\x00" |
| | x = 0 |
| | elif byte[0] == 1: |
| | |
| | break |
| | elif byte[0] == 2: |
| | |
| | bytes_read = self.fd.read(2) |
| | if len(bytes_read) < 2: |
| | break |
| | right, up = self.fd.read(2) |
| | data += b"\x00" * (right + up * self.state.xsize) |
| | x = len(data) % self.state.xsize |
| | else: |
| | |
| | if rle4: |
| | |
| | byte_count = byte[0] // 2 |
| | bytes_read = self.fd.read(byte_count) |
| | for byte_read in bytes_read: |
| | data += o8(byte_read >> 4) |
| | data += o8(byte_read & 0x0F) |
| | else: |
| | byte_count = byte[0] |
| | bytes_read = self.fd.read(byte_count) |
| | data += bytes_read |
| | if len(bytes_read) < byte_count: |
| | break |
| | x += byte[0] |
| |
|
| | |
| | if self.fd.tell() % 2 != 0: |
| | self.fd.seek(1, os.SEEK_CUR) |
| | rawmode = "L" if self.mode == "L" else "P" |
| | self.set_as_raw(bytes(data), (rawmode, 0, self.args[-1])) |
| | return -1, 0 |
| |
|
| |
|
| | |
| | |
| | |
| | class DibImageFile(BmpImageFile): |
| |
|
| | format = "DIB" |
| | format_description = "Windows Bitmap" |
| |
|
| | def _open(self): |
| | self._bitmap() |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | SAVE = { |
| | "1": ("1", 1, 2), |
| | "L": ("L", 8, 256), |
| | "P": ("P", 8, 256), |
| | "RGB": ("BGR", 24, 0), |
| | "RGBA": ("BGRA", 32, 0), |
| | } |
| |
|
| |
|
| | def _dib_save(im, fp, filename): |
| | _save(im, fp, filename, False) |
| |
|
| |
|
| | def _save(im, fp, filename, bitmap_header=True): |
| | try: |
| | rawmode, bits, colors = SAVE[im.mode] |
| | except KeyError as e: |
| | raise OSError(f"cannot write mode {im.mode} as BMP") from e |
| |
|
| | info = im.encoderinfo |
| |
|
| | dpi = info.get("dpi", (96, 96)) |
| |
|
| | |
| | ppm = tuple(map(lambda x: int(x * 39.3701 + 0.5), dpi)) |
| |
|
| | stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3) |
| | header = 40 |
| | image = stride * im.size[1] |
| |
|
| | if im.mode == "1": |
| | palette = b"".join(o8(i) * 4 for i in (0, 255)) |
| | elif im.mode == "L": |
| | palette = b"".join(o8(i) * 4 for i in range(256)) |
| | elif im.mode == "P": |
| | palette = im.im.getpalette("RGB", "BGRX") |
| | colors = len(palette) // 4 |
| | else: |
| | palette = None |
| |
|
| | |
| | if bitmap_header: |
| | offset = 14 + header + colors * 4 |
| | file_size = offset + image |
| | if file_size > 2**32 - 1: |
| | raise ValueError("File size is too large for the BMP format") |
| | fp.write( |
| | b"BM" |
| | + o32(file_size) |
| | + o32(0) |
| | + o32(offset) |
| | ) |
| |
|
| | |
| | fp.write( |
| | o32(header) |
| | + o32(im.size[0]) |
| | + o32(im.size[1]) |
| | + o16(1) |
| | + o16(bits) |
| | + o32(0) |
| | + o32(image) |
| | + o32(ppm[0]) |
| | + o32(ppm[1]) |
| | + o32(colors) |
| | + o32(colors) |
| | ) |
| |
|
| | fp.write(b"\0" * (header - 40)) |
| |
|
| | if palette: |
| | fp.write(palette) |
| |
|
| | ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]) |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | 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") |
| |
|
| | Image.register_decoder("bmp_rle", BmpRleDecoder) |
| |
|
| | Image.register_open(DibImageFile.format, DibImageFile, _dib_accept) |
| | Image.register_save(DibImageFile.format, _dib_save) |
| |
|
| | Image.register_extension(DibImageFile.format, ".dib") |
| |
|
| | Image.register_mime(DibImageFile.format, "image/bmp") |
| |
|