| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | from __future__ import annotations |
| |
|
| | import os |
| | from typing import IO, Any |
| |
|
| | 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"), |
| | } |
| |
|
| | USE_RAW_ALPHA = False |
| |
|
| |
|
| | def _accept(prefix: bytes) -> bool: |
| | return prefix.startswith(b"BM") |
| |
|
| |
|
| | def _dib_accept(prefix: bytes) -> bool: |
| | return i32(prefix) in [12, 40, 52, 56, 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: int = 0, offset: int = 0) -> None: |
| | """Read relevant info about the BMP""" |
| | assert self.fp is not None |
| | read, seek = self.fp.read, self.fp.seek |
| | if header: |
| | seek(header) |
| | |
| | file_info: dict[str, bool | int | tuple[int, ...]] = { |
| | "header_size": i32(read(4)), |
| | "direction": -1, |
| | } |
| |
|
| | |
| | |
| | assert isinstance(file_info["header_size"], int) |
| | 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.COMPRESSIONS["RAW"] |
| | file_info["palette_padding"] = 3 |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | elif file_info["header_size"] in (40, 52, 56, 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 |
| | assert isinstance(file_info["pixels_per_meter"], tuple) |
| | self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"]) |
| | if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]: |
| | masks = ["r_mask", "g_mask", "b_mask"] |
| | if len(header_data) >= 48: |
| | if len(header_data) >= 52: |
| | masks.append("a_mask") |
| | else: |
| | file_info["a_mask"] = 0x0 |
| | for idx, mask in enumerate(masks): |
| | file_info[mask] = i32(header_data, 36 + idx * 4) |
| | else: |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | file_info["a_mask"] = 0x0 |
| | for mask in masks: |
| | file_info[mask] = i32(read(4)) |
| | assert isinstance(file_info["r_mask"], int) |
| | assert isinstance(file_info["g_mask"], int) |
| | assert isinstance(file_info["b_mask"], int) |
| | assert isinstance(file_info["a_mask"], int) |
| | 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: |
| | msg = f"Unsupported BMP header type ({file_info['header_size']})" |
| | raise OSError(msg) |
| |
|
| | |
| | |
| | assert isinstance(file_info["width"], int) |
| | assert isinstance(file_info["height"], int) |
| | self._size = file_info["width"], file_info["height"] |
| |
|
| | |
| | assert isinstance(file_info["bits"], int) |
| | file_info["colors"] = ( |
| | file_info["colors"] |
| | if file_info.get("colors", 0) |
| | else (1 << file_info["bits"]) |
| | ) |
| | assert isinstance(file_info["colors"], int) |
| | 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"], ("", "")) |
| | if not self.mode: |
| | msg = f"Unsupported BMP pixel depth ({file_info['bits']})" |
| | raise OSError(msg) |
| |
|
| | |
| | decoder_name = "raw" |
| | if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]: |
| | SUPPORTED: dict[int, list[tuple[int, ...]]] = { |
| | 32: [ |
| | (0xFF0000, 0xFF00, 0xFF, 0x0), |
| | (0xFF000000, 0xFF0000, 0xFF00, 0x0), |
| | (0xFF000000, 0xFF00, 0xFF, 0x0), |
| | (0xFF000000, 0xFF0000, 0xFF00, 0xFF), |
| | (0xFF, 0xFF00, 0xFF0000, 0xFF000000), |
| | (0xFF0000, 0xFF00, 0xFF, 0xFF000000), |
| | (0xFF000000, 0xFF00, 0xFF, 0xFF0000), |
| | (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, 0xFF00, 0xFF, 0x0)): "BGXR", |
| | (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR", |
| | (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA", |
| | (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA", |
| | (32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR", |
| | (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"]] |
| | ): |
| | assert isinstance(file_info["rgba_mask"], tuple) |
| | 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"]] |
| | ): |
| | assert isinstance(file_info["rgb_mask"], tuple) |
| | raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])] |
| | else: |
| | msg = "Unsupported BMP bitfields layout" |
| | raise OSError(msg) |
| | else: |
| | msg = "Unsupported BMP bitfields layout" |
| | raise OSError(msg) |
| | elif file_info["compression"] == self.COMPRESSIONS["RAW"]: |
| | if file_info["bits"] == 32 and ( |
| | header == 22 or USE_RAW_ALPHA |
| | ): |
| | raw_mode, self._mode = "BGRA", "RGBA" |
| | elif file_info["compression"] in ( |
| | self.COMPRESSIONS["RLE8"], |
| | self.COMPRESSIONS["RLE4"], |
| | ): |
| | decoder_name = "bmp_rle" |
| | else: |
| | msg = f"Unsupported BMP compression ({file_info['compression']})" |
| | raise OSError(msg) |
| |
|
| | |
| | if self.mode == "P": |
| | |
| | if not (0 < file_info["colors"] <= 65536): |
| | msg = f"Unsupported BMP Palette size ({file_info['colors']})" |
| | raise OSError(msg) |
| | else: |
| | assert isinstance(file_info["palette_padding"], int) |
| | padding = file_info["palette_padding"] |
| | palette = read(padding * file_info["colors"]) |
| | grayscale = 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: |
| | grayscale = False |
| |
|
| | |
| | if grayscale: |
| | 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: list[Any] = [raw_mode] |
| | if decoder_name == "bmp_rle": |
| | args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"]) |
| | else: |
| | assert isinstance(file_info["width"], int) |
| | args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3)) |
| | args.append(file_info["direction"]) |
| | self.tile = [ |
| | ImageFile._Tile( |
| | decoder_name, |
| | (0, 0, file_info["width"], file_info["height"]), |
| | offset or self.fp.tell(), |
| | tuple(args), |
| | ) |
| | ] |
| |
|
| | def _open(self) -> None: |
| | """Open file, check magic number and read header""" |
| | |
| | assert self.fp is not None |
| | head_data = self.fp.read(14) |
| | |
| | if not _accept(head_data): |
| | msg = "Not a BMP file" |
| | raise SyntaxError(msg) |
| | |
| | offset = i32(head_data, 10) |
| | |
| | self._bitmap(offset=offset) |
| |
|
| |
|
| | class BmpRleDecoder(ImageFile.PyDecoder): |
| | _pulls_fd = True |
| |
|
| | def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: |
| | assert self.fd is not None |
| | rle4 = self.args[1] |
| | data = bytearray() |
| | x = 0 |
| | dest_length = self.state.xsize * self.state.ysize |
| | while len(data) < dest_length: |
| | 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) -> None: |
| | 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: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: |
| | _save(im, fp, filename, False) |
| |
|
| |
|
| | def _save( |
| | im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True |
| | ) -> None: |
| | try: |
| | rawmode, bits, colors = SAVE[im.mode] |
| | except KeyError as e: |
| | msg = f"cannot write mode {im.mode} as BMP" |
| | raise OSError(msg) from e |
| |
|
| | info = im.encoderinfo |
| |
|
| | dpi = info.get("dpi", (96, 96)) |
| |
|
| | |
| | ppm = tuple(int(x * 39.3701 + 0.5) for x in 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) * 3 + b"\x00" for i in (0, 255)) |
| | elif im.mode == "L": |
| | palette = b"".join(o8(i) * 3 + b"\x00" 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: |
| | msg = "File size is too large for the BMP format" |
| | raise ValueError(msg) |
| | 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, [ImageFile._Tile("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") |
| |
|