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