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