CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
BmpImagePlugin.py518 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        file_info["colors"] = (
183            file_info["colors"]
184            if file_info.get("colors", 0)
185            else (1 << file_info["bits"])
186        )
187        assert isinstance(file_info["colors"], int)
188        if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8:
189            offset += 4 * file_info["colors"]
190
191        # ---------------------- Check bit depth for unusual unsupported values
192        self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", ""))
193        if not self.mode:
194            msg = f"Unsupported BMP pixel depth ({file_info['bits']})"
195            raise OSError(msg)
196
197        # ---------------- Process BMP with Bitfields compression (not palette)
198        decoder_name = "raw"
199        if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
200            SUPPORTED: dict[int, list[tuple[int, ...]]] = {
201                32: [
202                    (0xFF0000, 0xFF00, 0xFF, 0x0),
203                    (0xFF000000, 0xFF0000, 0xFF00, 0x0),
204                    (0xFF000000, 0xFF00, 0xFF, 0x0),
205                    (0xFF000000, 0xFF0000, 0xFF00, 0xFF),
206                    (0xFF, 0xFF00, 0xFF0000, 0xFF000000),
207                    (0xFF0000, 0xFF00, 0xFF, 0xFF000000),
208                    (0xFF000000, 0xFF00, 0xFF, 0xFF0000),
209                    (0x0, 0x0, 0x0, 0x0),
210                ],
211                24: [(0xFF0000, 0xFF00, 0xFF)],
212                16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)],
213            }
214            MASK_MODES = {
215                (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX",
216                (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR",
217                (32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR",
218                (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR",
219                (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA",
220                (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA",
221                (32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR",
222                (32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
223                (24, (0xFF0000, 0xFF00, 0xFF)): "BGR",
224                (16, (0xF800, 0x7E0, 0x1F)): "BGR;16",
225                (16, (0x7C00, 0x3E0, 0x1F)): "BGR;15",
226            }
227            if file_info["bits"] in SUPPORTED:
228                if (
229                    file_info["bits"] == 32
230                    and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]]
231                ):
232                    assert isinstance(file_info["rgba_mask"], tuple)
233                    raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])]
234                    self._mode = "RGBA" if "A" in raw_mode else self.mode
235                elif (
236                    file_info["bits"] in (24, 16)
237                    and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]]
238                ):
239                    assert isinstance(file_info["rgb_mask"], tuple)
240                    raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])]
241                else:
242                    msg = "Unsupported BMP bitfields layout"
243                    raise OSError(msg)
244            else:
245                msg = "Unsupported BMP bitfields layout"
246                raise OSError(msg)
247        elif file_info["compression"] == self.COMPRESSIONS["RAW"]:
248            if file_info["bits"] == 32 and (
249                header == 22 or USE_RAW_ALPHA  # 32-bit .cur offset
250            ):
251                raw_mode, self._mode = "BGRA", "RGBA"
252        elif file_info["compression"] in (
253            self.COMPRESSIONS["RLE8"],
254            self.COMPRESSIONS["RLE4"],
255        ):
256            decoder_name = "bmp_rle"
257        else:
258            msg = f"Unsupported BMP compression ({file_info['compression']})"
259            raise OSError(msg)
260
261        # --------------- Once the header is processed, process the palette/LUT
262        if self.mode == "P":  # Paletted for 1, 4 and 8 bit images
263            # ---------------------------------------------------- 1-bit images
264            if not (0 < file_info["colors"] <= 65536):
265                msg = f"Unsupported BMP Palette size ({file_info['colors']})"
266                raise OSError(msg)
267            else:
268                assert isinstance(file_info["palette_padding"], int)
269                padding = file_info["palette_padding"]
270                palette = read(padding * file_info["colors"])
271                grayscale = True
272                indices = (
273                    (0, 255)
274                    if file_info["colors"] == 2
275                    else list(range(file_info["colors"]))
276                )
277
278                # ----------------- Check if grayscale and ignore palette if so
279                for ind, val in enumerate(indices):
280                    rgb = palette[ind * padding : ind * padding + 3]
281                    if rgb != o8(val) * 3:
282                        grayscale = False
283
284                # ------- If all colors are gray, white or black, ditch palette
285                if grayscale:
286                    self._mode = "1" if file_info["colors"] == 2 else "L"
287                    raw_mode = self.mode
288                else:
289                    self._mode = "P"
290                    self.palette = ImagePalette.raw(
291                        "BGRX" if padding == 4 else "BGR", palette
292                    )
293
294        # ---------------------------- Finally set the tile data for the plugin
295        self.info["compression"] = file_info["compression"]
296        args: list[Any] = [raw_mode]
297        if decoder_name == "bmp_rle":
298            args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"])
299        else:
300            assert isinstance(file_info["width"], int)
301            args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
302        args.append(file_info["direction"])
303        self.tile = [
304            ImageFile._Tile(
305                decoder_name,
306                (0, 0, file_info["width"], file_info["height"]),
307                offset or self.fp.tell(),
308                tuple(args),
309            )
310        ]
311
312    def _open(self) -> None:
313        """Open file, check magic number and read header"""
314        # read 14 bytes: magic number, filesize, reserved, header final offset
315        assert self.fp is not None
316        head_data = self.fp.read(14)
317        # choke if the file does not have the required magic bytes
318        if not _accept(head_data):
319            msg = "Not a BMP file"
320            raise SyntaxError(msg)
321        # read the start position of the BMP image data (u32)
322        offset = i32(head_data, 10)
323        # load bitmap information (offset=raster info)
324        self._bitmap(offset=offset)
325
326
327class BmpRleDecoder(ImageFile.PyDecoder):
328    _pulls_fd = True
329
330    def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
331        assert self.fd is not None
332        rle4 = self.args[1]
333        data = bytearray()
334        x = 0
335        dest_length = self.state.xsize * self.state.ysize
336        while len(data) < dest_length:
337            pixels = self.fd.read(1)
338            byte = self.fd.read(1)
339            if not pixels or not byte:
340                break
341            num_pixels = pixels[0]
342            if num_pixels:
343                # encoded mode
344                if x + num_pixels > self.state.xsize:
345                    # Too much data for row
346                    num_pixels = max(0, self.state.xsize - x)
347                if rle4:
348                    first_pixel = o8(byte[0] >> 4)
349                    second_pixel = o8(byte[0] & 0x0F)
350                    for index in range(num_pixels):
351                        if index % 2 == 0:
352                            data += first_pixel
353                        else:
354                            data += second_pixel
355                else:
356                    data += byte * num_pixels
357                x += num_pixels
358            else:
359                if byte[0] == 0:
360                    # end of line
361                    while len(data) % self.state.xsize != 0:
362                        data += b"\x00"
363                    x = 0
364                elif byte[0] == 1:
365                    # end of bitmap
366                    break
367                elif byte[0] == 2:
368                    # delta
369                    bytes_read = self.fd.read(2)
370                    if len(bytes_read) < 2:
371                        break
372                    right, up = self.fd.read(2)
373                    data += b"\x00" * (right + up * self.state.xsize)
374                    x = len(data) % self.state.xsize
375                else:
376                    # absolute mode
377                    if rle4:
378                        # 2 pixels per byte
379                        byte_count = byte[0] // 2
380                        bytes_read = self.fd.read(byte_count)
381                        for byte_read in bytes_read:
382                            data += o8(byte_read >> 4)
383                            data += o8(byte_read & 0x0F)
384                    else:
385                        byte_count = byte[0]
386                        bytes_read = self.fd.read(byte_count)
387                        data += bytes_read
388                    if len(bytes_read) < byte_count:
389                        break
390                    x += byte[0]
391
392                    # align to 16-bit word boundary
393                    if self.fd.tell() % 2 != 0:
394                        self.fd.seek(1, os.SEEK_CUR)
395        rawmode = "L" if self.mode == "L" else "P"
396        self.set_as_raw(bytes(data), rawmode, (0, self.args[-1]))
397        return -1, 0
398
399
400# =============================================================================
401# Image plugin for the DIB format (BMP alias)
402# =============================================================================
403class DibImageFile(BmpImageFile):
404    format = "DIB"
405    format_description = "Windows Bitmap"
406
407    def _open(self) -> None:
408        self._bitmap()
409
410
411#
412# --------------------------------------------------------------------
413# Write BMP file
414
415
416SAVE = {
417    "1": ("1", 1, 2),
418    "L": ("L", 8, 256),
419    "P": ("P", 8, 256),
420    "RGB": ("BGR", 24, 0),
421    "RGBA": ("BGRA", 32, 0),
422}
423
424
425def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
426    _save(im, fp, filename, False)
427
428
429def _save(
430    im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True
431) -> None:
432    try:
433        rawmode, bits, colors = SAVE[im.mode]
434    except KeyError as e:
435        msg = f"cannot write mode {im.mode} as BMP"
436        raise OSError(msg) from e
437
438    info = im.encoderinfo
439
440    dpi = info.get("dpi", (96, 96))
441
442    # 1 meter == 39.3701 inches
443    ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi)
444
445    stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3)
446    header = 40  # or 64 for OS/2 version 2
447    image = stride * im.size[1]
448
449    if im.mode == "1":
450        palette = b"".join(o8(i) * 3 + b"\x00" for i in (0, 255))
451    elif im.mode == "L":
452        palette = b"".join(o8(i) * 3 + b"\x00" for i in range(256))
453    elif im.mode == "P":
454        palette = im.im.getpalette("RGB", "BGRX")
455        colors = len(palette) // 4
456    else:
457        palette = None
458
459    # bitmap header
460    if bitmap_header:
461        offset = 14 + header + colors * 4
462        file_size = offset + image
463        if file_size > 2**32 - 1:
464            msg = "File size is too large for the BMP format"
465            raise ValueError(msg)
466        fp.write(
467            b"BM"  # file type (magic)
468            + o32(file_size)  # file size
469            + o32(0)  # reserved
470            + o32(offset)  # image data offset
471        )
472
473    # bitmap info header
474    fp.write(
475        o32(header)  # info header size
476        + o32(im.size[0])  # width
477        + o32(im.size[1])  # height
478        + o16(1)  # planes
479        + o16(bits)  # depth
480        + o32(0)  # compression (0=uncompressed)
481        + o32(image)  # size of bitmap
482        + o32(ppm[0])  # resolution
483        + o32(ppm[1])  # resolution
484        + o32(colors)  # colors used
485        + o32(colors)  # colors important
486    )
487
488    fp.write(b"\0" * (header - 40))  # padding (for OS/2 format)
489
490    if palette:
491        fp.write(palette)
492
493    ImageFile._save(
494        im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]
495    )
496
497
498#
499# --------------------------------------------------------------------
500# Registry
501
502
503Image.register_open(BmpImageFile.format, BmpImageFile, _accept)
504Image.register_save(BmpImageFile.format, _save)
505
506Image.register_extension(BmpImageFile.format, ".bmp")
507
508Image.register_mime(BmpImageFile.format, "image/bmp")
509
510Image.register_decoder("bmp_rle", BmpRleDecoder)
511
512Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
513Image.register_save(DibImageFile.format, _dib_save)
514
515Image.register_extension(DibImageFile.format, ".dib")
516
517Image.register_mime(DibImageFile.format, "image/bmp")
518 
Aluode/PerceptionLabPortable · CoolFace