Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/BmpImagePlugin.py: 91%
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
27import os
28from typing import IO, Any
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
37#
38# --------------------------------------------------------------------
39# Read BMP file
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}
51USE_RAW_ALPHA = False
54def _accept(prefix: bytes) -> bool:
55 return prefix.startswith(b"BM")
58def _dib_accept(prefix: bytes) -> bool:
59 return i32(prefix) in [12, 40, 52, 56, 64, 108, 124]
62# =============================================================================
63# Image plugin for the Windows BMP format.
64# =============================================================================
65class BmpImageFile(ImageFile.ImageFile):
66 """Image plugin for the Windows Bitmap format (BMP)"""
68 # ------------------------------------------------------------- Description
69 format_description = "Windows Bitmap"
70 format = "BMP"
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
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 }
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)
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
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)
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"]
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"]
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)
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)
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 )
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
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 )
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 ]
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)
327class BmpRleDecoder(ImageFile.PyDecoder):
328 _pulls_fd = True
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]
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
400# =============================================================================
401# Image plugin for the DIB format (BMP alias)
402# =============================================================================
403class DibImageFile(BmpImageFile):
404 format = "DIB"
405 format_description = "Windows Bitmap"
407 def _open(self) -> None:
408 self._bitmap()
411#
412# --------------------------------------------------------------------
413# Write BMP file
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}
425def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
426 _save(im, fp, filename, False)
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
438 info = im.encoderinfo
440 dpi = info.get("dpi", (96, 96))
442 # 1 meter == 39.3701 inches
443 ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi)
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]
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
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 )
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 )
488 fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
490 if palette:
491 fp.write(palette)
493 ImageFile._save(
494 im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]
495 )
498#
499# --------------------------------------------------------------------
500# Registry
503Image.register_open(BmpImageFile.format, BmpImageFile, _accept)
504Image.register_save(BmpImageFile.format, _save)
506Image.register_extension(BmpImageFile.format, ".bmp")
508Image.register_mime(BmpImageFile.format, "image/bmp")
510Image.register_decoder("bmp_rle", BmpRleDecoder)
512Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
513Image.register_save(DibImageFile.format, _dib_save)
515Image.register_extension(DibImageFile.format, ".dib")
517Image.register_mime(DibImageFile.format, "image/bmp")