Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
BmpImagePlugin.py515 linesDownload Raw Back to PIL
1#
2# The Python Imaging Library.
3# $Id$
4#
5# BMP file handler
6#
7# Windows (and OS/2) native bitmap storage format.
8#
9# history:
10# 1995-09-01 fl   Created
11# 1996-04-30 fl   Added save
12# 1997-08-27 fl   Fixed save of 1-bit images
13# 1998-03-06 fl   Load P images as L where possible
14# 1998-07-03 fl   Load P images as 1 where possible
15# 1998-12-29 fl   Handle small palettes
16# 2002-12-30 fl   Fixed load of 1-bit palette images
17# 2003-04-21 fl   Fixed load of 1-bit monochrome images
18# 2003-04-23 fl   Added limited support for BI_BITFIELDS compression
19#
20# Copyright (c) 1997-2003 by Secret Labs AB
21# Copyright (c) 1995-2003 by Fredrik Lundh
22#
23# See the README file for information on usage and redistribution.
24#
25from __future__ import annotations
26
27import os
28from typing import IO, Any
29
30from . import Image, ImageFile, ImagePalette
31from ._binary import i16le as i16
32from ._binary import i32le as i32
33from ._binary import o8
34from ._binary import o16le as o16
35from ._binary import o32le as o32
36
37#
38# --------------------------------------------------------------------
39# Read BMP file
40
41BIT2MODE = {
42    # bits => mode, rawmode
43    1: ("P", "P;1"),
44    4: ("P", "P;4"),
45    8: ("P", "P"),
46    16: ("RGB", "BGR;15"),
47    24: ("RGB", "BGR"),
48    32: ("RGB", "BGRX"),
49}
50
51USE_RAW_ALPHA = False
52
53
54def _accept(prefix: bytes) -> bool:
55    return prefix.startswith(b"BM")
56
57
58def _dib_accept(prefix: bytes) -> bool:
59    return i32(prefix) in [12, 40, 52, 56, 64, 108, 124]
60
61
62# =============================================================================
63# Image plugin for the Windows BMP format.
64# =============================================================================
65class BmpImageFile(ImageFile.ImageFile):
66    """Image plugin for the Windows Bitmap format (BMP)"""
67
68    # ------------------------------------------------------------- Description
69    format_description = "Windows Bitmap"
70    format = "BMP"
71
72    # -------------------------------------------------- BMP Compression values
73    COMPRESSIONS = {"RAW": 0, "RLE8": 1, "RLE4": 2, "BITFIELDS": 3, "JPEG": 4, "PNG": 5}
74    for k, v in COMPRESSIONS.items():
75        vars()[k] = v
76
77    def _bitmap(self, header: int = 0, offset: int = 0) -> None:
78        """Read relevant info about the BMP"""
79        assert self.fp is not None
80        read, seek = self.fp.read, self.fp.seek
81        if header:
82            seek(header)
83        # read bmp header size @offset 14 (this is part of the header size)
84        file_info: dict[str, bool | int | tuple[int, ...]] = {
85            "header_size": i32(read(4)),
86            "direction": -1,
87        }
88
89        # -------------------- If requested, read header at a specific position
90        # read the rest of the bmp header, without its size
91        assert isinstance(file_info["header_size"], int)
92        header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4)
93
94        # ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1
95        # ----- This format has different offsets because of width/height types
96        # 12: BITMAPCOREHEADER/OS21XBITMAPHEADER
97        if file_info["header_size"] == 12:
98            file_info["width"] = i16(header_data, 0)
99            file_info["height"] = i16(header_data, 2)
100            file_info["planes"] = i16(header_data, 4)
101            file_info["bits"] = i16(header_data, 6)
102            file_info["compression"] = self.COMPRESSIONS["RAW"]
103            file_info["palette_padding"] = 3
104
105        # --------------------------------------------- Windows Bitmap v3 to v5
106        #  40: BITMAPINFOHEADER
107        #  52: BITMAPV2HEADER
108        #  56: BITMAPV3HEADER
109        #  64: BITMAPCOREHEADER2/OS22XBITMAPHEADER
110        # 108: BITMAPV4HEADER
111        # 124: BITMAPV5HEADER
112        elif file_info["header_size"] in (40, 52, 56, 64, 108, 124):
113            file_info["y_flip"] = header_data[7] == 0xFF
114            file_info["direction"] = 1 if file_info["y_flip"] else -1
115            file_info["width"] = i32(header_data, 0)
116            file_info["height"] = (
117                i32(header_data, 4)
118                if not file_info["y_flip"]
119                else 2**32 - i32(header_data, 4)
120            )
121            file_info["planes"] = i16(header_data, 8)
122            file_info["bits"] = i16(header_data, 10)
123            file_info["compression"] = i32(header_data, 12)
124            # byte size of pixel data
125            file_info["data_size"] = i32(header_data, 16)
126            file_info["pixels_per_meter"] = (
127                i32(header_data, 20),
128                i32(header_data, 24),
129            )
130            file_info["colors"] = i32(header_data, 28)
131            file_info["palette_padding"] = 4
132            assert isinstance(file_info["pixels_per_meter"], tuple)
133            self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"])
134            if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
135                masks = ["r_mask", "g_mask", "b_mask"]
136                if len(header_data) >= 48:
137                    if len(header_data) >= 52:
138                        masks.append("a_mask")
139                    else:
140                        file_info["a_mask"] = 0x0
141                    for idx, mask in enumerate(masks):
142                        file_info[mask] = i32(header_data, 36 + idx * 4)
143                else:
144                    # 40 byte headers only have the three components in the
145                    # bitfields masks, ref:
146                    # https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx
147                    # See also
148                    # https://github.com/python-pillow/Pillow/issues/1293
149                    # There is a 4th component in the RGBQuad, in the alpha
150                    # location, but it is listed as a reserved component,
151                    # and it is not generally an alpha channel
152                    file_info["a_mask"] = 0x0
153                    for mask in masks:
154                        file_info[mask] = i32(read(4))
155                assert isinstance(file_info["r_mask"], int)
156                assert isinstance(file_info["g_mask"], int)
157                assert isinstance(file_info["b_mask"], int)
158                assert isinstance(file_info["a_mask"], int)
159                file_info["rgb_mask"] = (
160                    file_info["r_mask"],
161                    file_info["g_mask"],
162                    file_info["b_mask"],
163                )
164                file_info["rgba_mask"] = (
165                    file_info["r_mask"],
166                    file_info["g_mask"],
167                    file_info["b_mask"],
168                    file_info["a_mask"],
169                )
170        else:
171            msg = f"Unsupported BMP header type ({file_info['header_size']})"
172            raise OSError(msg)
173
174        # ------------------ Special case : header is reported 40, which
175        # ---------------------- is shorter than real size for bpp >= 16
176        assert isinstance(file_info["width"], int)
177        assert isinstance(file_info["height"], int)
178        self._size = file_info["width"], file_info["height"]
179
180        # ------- If color count was not found in the header, compute from bits
181        assert isinstance(file_info["bits"], int)
182        if not file_info.get("colors", 0):
183            file_info["colors"] = 1 << file_info["bits"]
184        assert isinstance(file_info["palette_padding"], int)
185        assert isinstance(file_info["colors"], int)
186        if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8:
187            offset += file_info["palette_padding"] * file_info["colors"]
188
189        # ---------------------- Check bit depth for unusual unsupported values
190        self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", ""))
191        if not self.mode:
192            msg = f"Unsupported BMP pixel depth ({file_info['bits']})"
193            raise OSError(msg)
194
195        # ---------------- Process BMP with Bitfields compression (not palette)
196        decoder_name = "raw"
197        if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
198            SUPPORTED: dict[int, list[tuple[int, ...]]] = {
199                32: [
200                    (0xFF0000, 0xFF00, 0xFF, 0x0),
201                    (0xFF000000, 0xFF0000, 0xFF00, 0x0),
202                    (0xFF000000, 0xFF00, 0xFF, 0x0),
203                    (0xFF000000, 0xFF0000, 0xFF00, 0xFF),
204                    (0xFF, 0xFF00, 0xFF0000, 0xFF000000),
205                    (0xFF0000, 0xFF00, 0xFF, 0xFF000000),
206                    (0xFF000000, 0xFF00, 0xFF, 0xFF0000),
207                    (0x0, 0x0, 0x0, 0x0),
208                ],
209                24: [(0xFF0000, 0xFF00, 0xFF)],
210                16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)],
211            }
212            MASK_MODES = {
213                (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX",
214                (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR",
215                (32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR",
216                (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR",
217                (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA",
218                (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA",
219                (32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR",
220                (32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
221                (24, (0xFF0000, 0xFF00, 0xFF)): "BGR",
222                (16, (0xF800, 0x7E0, 0x1F)): "BGR;16",
223                (16, (0x7C00, 0x3E0, 0x1F)): "BGR;15",
224            }
225            if file_info["bits"] in SUPPORTED:
226                if (
227                    file_info["bits"] == 32
228                    and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]]
229                ):
230                    assert isinstance(file_info["rgba_mask"], tuple)
231                    raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])]
232                    self._mode = "RGBA" if "A" in raw_mode else self.mode
233                elif (
234                    file_info["bits"] in (24, 16)
235                    and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]]
236                ):
237                    assert isinstance(file_info["rgb_mask"], tuple)
238                    raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])]
239                else:
240                    msg = "Unsupported BMP bitfields layout"
241                    raise OSError(msg)
242            else:
243                msg = "Unsupported BMP bitfields layout"
244                raise OSError(msg)
245        elif file_info["compression"] == self.COMPRESSIONS["RAW"]:
246            if file_info["bits"] == 32 and (
247                header == 22 or USE_RAW_ALPHA  # 32-bit .cur offset
248            ):
249                raw_mode, self._mode = "BGRA", "RGBA"
250        elif file_info["compression"] in (
251            self.COMPRESSIONS["RLE8"],
252            self.COMPRESSIONS["RLE4"],
253        ):
254            decoder_name = "bmp_rle"
255        else:
256            msg = f"Unsupported BMP compression ({file_info['compression']})"
257            raise OSError(msg)
258
259        # --------------- Once the header is processed, process the palette/LUT
260        if self.mode == "P":  # Paletted for 1, 4 and 8 bit images
261            # ---------------------------------------------------- 1-bit images
262            if not (0 < file_info["colors"] <= 65536):
263                msg = f"Unsupported BMP Palette size ({file_info['colors']})"
264                raise OSError(msg)
265            else:
266                padding = file_info["palette_padding"]
267                palette = read(padding * file_info["colors"])
268                grayscale = True
269                indices = (
270                    (0, 255)
271                    if file_info["colors"] == 2
272                    else list(range(file_info["colors"]))
273                )
274
275                # ----------------- Check if grayscale and ignore palette if so
276                for ind, val in enumerate(indices):
277                    rgb = palette[ind * padding : ind * padding + 3]
278                    if rgb != o8(val) * 3:
279                        grayscale = False
280
281                # ------- If all colors are gray, white or black, ditch palette
282                if grayscale:
283                    self._mode = "1" if file_info["colors"] == 2 else "L"
284                    raw_mode = self.mode
285                else:
286                    self._mode = "P"
287                    self.palette = ImagePalette.raw(
288                        "BGRX" if padding == 4 else "BGR", palette
289                    )
290
291        # ---------------------------- Finally set the tile data for the plugin
292        self.info["compression"] = file_info["compression"]
293        args: list[Any] = [raw_mode]
294        if decoder_name == "bmp_rle":
295            args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"])
296        else:
297            assert isinstance(file_info["width"], int)
298            args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
299        args.append(file_info["direction"])
300        self.tile = [
301            ImageFile._Tile(
302                decoder_name,
303                (0, 0, file_info["width"], file_info["height"]),
304                offset or self.fp.tell(),
305                tuple(args),
306            )
307        ]
308
309    def _open(self) -> None:
310        """Open file, check magic number and read header"""
311        # read 14 bytes: magic number, filesize, reserved, header final offset
312        assert self.fp is not None
313        head_data = self.fp.read(14)
314        # choke if the file does not have the required magic bytes
315        if not _accept(head_data):
316            msg = "Not a BMP file"
317            raise SyntaxError(msg)
318        # read the start position of the BMP image data (u32)
319        offset = i32(head_data, 10)
320        # load bitmap information (offset=raster info)
321        self._bitmap(offset=offset)
322
323
324class BmpRleDecoder(ImageFile.PyDecoder):
325    _pulls_fd = True
326
327    def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
328        assert self.fd is not None
329        rle4 = self.args[1]
330        data = bytearray()
331        x = 0
332        dest_length = self.state.xsize * self.state.ysize
333        while len(data) < dest_length:
334            pixels = self.fd.read(1)
335            byte = self.fd.read(1)
336            if not pixels or not byte:
337                break
338            num_pixels = pixels[0]
339            if num_pixels:
340                # encoded mode
341                if x + num_pixels > self.state.xsize:
342                    # Too much data for row
343                    num_pixels = max(0, self.state.xsize - x)
344                if rle4:
345                    first_pixel = o8(byte[0] >> 4)
346                    second_pixel = o8(byte[0] & 0x0F)
347                    for index in range(num_pixels):
348                        if index % 2 == 0:
349                            data += first_pixel
350                        else:
351                            data += second_pixel
352                else:
353                    data += byte * num_pixels
354                x += num_pixels
355            else:
356                if byte[0] == 0:
357                    # end of line
358                    while len(data) % self.state.xsize != 0:
359                        data += b"\x00"
360                    x = 0
361                elif byte[0] == 1:
362                    # end of bitmap
363                    break
364                elif byte[0] == 2:
365                    # delta
366                    bytes_read = self.fd.read(2)
367                    if len(bytes_read) < 2:
368                        break
369                    right, up = bytes_read
370                    data += b"\x00" * (right + up * self.state.xsize)
371                    x = len(data) % self.state.xsize
372                else:
373                    # absolute mode
374                    if rle4:
375                        # 2 pixels per byte
376                        byte_count = byte[0] // 2
377                        bytes_read = self.fd.read(byte_count)
378                        for byte_read in bytes_read:
379                            data += o8(byte_read >> 4)
380                            data += o8(byte_read & 0x0F)
381                    else:
382                        byte_count = byte[0]
383                        bytes_read = self.fd.read(byte_count)
384                        data += bytes_read
385                    if len(bytes_read) < byte_count:
386                        break
387                    x += byte[0]
388
389                    # align to 16-bit word boundary
390                    if self.fd.tell() % 2 != 0:
391                        self.fd.seek(1, os.SEEK_CUR)
392        rawmode = "L" if self.mode == "L" else "P"
393        self.set_as_raw(bytes(data), rawmode, (0, self.args[-1]))
394        return -1, 0
395
396
397# =============================================================================
398# Image plugin for the DIB format (BMP alias)
399# =============================================================================
400class DibImageFile(BmpImageFile):
401    format = "DIB"
402    format_description = "Windows Bitmap"
403
404    def _open(self) -> None:
405        self._bitmap()
406
407
408#
409# --------------------------------------------------------------------
410# Write BMP file
411
412
413SAVE = {
414    "1": ("1", 1, 2),
415    "L": ("L", 8, 256),
416    "P": ("P", 8, 256),
417    "RGB": ("BGR", 24, 0),
418    "RGBA": ("BGRA", 32, 0),
419}
420
421
422def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
423    _save(im, fp, filename, False)
424
425
426def _save(
427    im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True
428) -> None:
429    try:
430        rawmode, bits, colors = SAVE[im.mode]
431    except KeyError as e:
432        msg = f"cannot write mode {im.mode} as BMP"
433        raise OSError(msg) from e
434
435    info = im.encoderinfo
436
437    dpi = info.get("dpi", (96, 96))
438
439    # 1 meter == 39.3701 inches
440    ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi)
441
442    stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3)
443    header = 40  # or 64 for OS/2 version 2
444    image = stride * im.size[1]
445
446    if im.mode == "1":
447        palette = b"".join(o8(i) * 3 + b"\x00" for i in (0, 255))
448    elif im.mode == "L":
449        palette = b"".join(o8(i) * 3 + b"\x00" for i in range(256))
450    elif im.mode == "P":
451        palette = im.im.getpalette("RGB", "BGRX")
452        colors = len(palette) // 4
453    else:
454        palette = None
455
456    # bitmap header
457    if bitmap_header:
458        offset = 14 + header + colors * 4
459        file_size = offset + image
460        if file_size > 2**32 - 1:
461            msg = "File size is too large for the BMP format"
462            raise ValueError(msg)
463        fp.write(
464            b"BM"  # file type (magic)
465            + o32(file_size)  # file size
466            + o32(0)  # reserved
467            + o32(offset)  # image data offset
468        )
469
470    # bitmap info header
471    fp.write(
472        o32(header)  # info header size
473        + o32(im.size[0])  # width
474        + o32(im.size[1])  # height
475        + o16(1)  # planes
476        + o16(bits)  # depth
477        + o32(0)  # compression (0=uncompressed)
478        + o32(image)  # size of bitmap
479        + o32(ppm[0])  # resolution
480        + o32(ppm[1])  # resolution
481        + o32(colors)  # colors used
482        + o32(colors)  # colors important
483    )
484
485    fp.write(b"\0" * (header - 40))  # padding (for OS/2 format)
486
487    if palette:
488        fp.write(palette)
489
490    ImageFile._save(
491        im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]
492    )
493
494
495#
496# --------------------------------------------------------------------
497# Registry
498
499
500Image.register_open(BmpImageFile.format, BmpImageFile, _accept)
501Image.register_save(BmpImageFile.format, _save)
502
503Image.register_extension(BmpImageFile.format, ".bmp")
504
505Image.register_mime(BmpImageFile.format, "image/bmp")
506
507Image.register_decoder("bmp_rle", BmpRleDecoder)
508
509Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
510Image.register_save(DibImageFile.format, _dib_save)
511
512Image.register_extension(DibImageFile.format, ".dib")
513
514Image.register_mime(DibImageFile.format, "image/bmp")
515 
codekingpro/portable-devtools · Team Ai