| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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") |
|
|