Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/BmpImagePlugin.py: 83%
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 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"]
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)
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)
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 )
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
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 )
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 ]
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)
324class BmpRleDecoder(ImageFile.PyDecoder):
325 _pulls_fd = True
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]
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
397# =============================================================================
398# Image plugin for the DIB format (BMP alias)
399# =============================================================================
400class DibImageFile(BmpImageFile):
401 format = "DIB"
402 format_description = "Windows Bitmap"
404 def _open(self) -> None:
405 self._bitmap()
408#
409# --------------------------------------------------------------------
410# Write BMP file
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}
422def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
423 _save(im, fp, filename, False)
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
435 info = im.encoderinfo
437 dpi = info.get("dpi", (96, 96))
439 # 1 meter == 39.3701 inches
440 ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi)
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]
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
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 )
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 )
485 fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
487 if palette:
488 fp.write(palette)
490 ImageFile._save(
491 im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]
492 )
495#
496# --------------------------------------------------------------------
497# Registry
500Image.register_open(BmpImageFile.format, BmpImageFile, _accept)
501Image.register_save(BmpImageFile.format, _save)
503Image.register_extension(BmpImageFile.format, ".bmp")
505Image.register_mime(BmpImageFile.format, "image/bmp")
507Image.register_decoder("bmp_rle", BmpRleDecoder)
509Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
510Image.register_save(DibImageFile.format, _dib_save)
512Image.register_extension(DibImageFile.format, ".dib")
514Image.register_mime(DibImageFile.format, "image/bmp")