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