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

252 statements  

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 

27__lazy_modules__ = {"PIL._binary"} 

28 

29import os 

30 

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 

37 

38TYPE_CHECKING = False 

39if TYPE_CHECKING: 

40 from typing import IO, Any 

41 

42# 

43# -------------------------------------------------------------------- 

44# Read BMP file 

45 

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} 

55 

56USE_RAW_ALPHA = False 

57 

58 

59def _accept(prefix: bytes) -> bool: 

60 return prefix.startswith(b"BM") 

61 

62 

63def _dib_accept(prefix: bytes) -> bool: 

64 return i32(prefix) in [12, 40, 52, 56, 64, 108, 124] 

65 

66 

67# ============================================================================= 

68# Image plugin for the Windows BMP format. 

69# ============================================================================= 

70class BmpImageFile(ImageFile.ImageFile): 

71 """Image plugin for the Windows Bitmap format (BMP)""" 

72 

73 # ------------------------------------------------------------- Description 

74 format_description = "Windows Bitmap" 

75 format = "BMP" 

76 

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 

81 

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 } 

93 

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) 

98 

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 

109 

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) 

180 

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"] 

186 

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"] 

195 

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) 

201 

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) 

265 

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 ) 

281 

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 

287 

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 ) 

297 

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 ] 

315 

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) 

329 

330 

331class BmpRleDecoder(ImageFile.PyDecoder): 

332 _pulls_fd = True 

333 

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] 

399 

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 

406 

407 

408# ============================================================================= 

409# Image plugin for the DIB format (BMP alias) 

410# ============================================================================= 

411class DibImageFile(BmpImageFile): 

412 format = "DIB" 

413 format_description = "Windows Bitmap" 

414 

415 def _open(self) -> None: 

416 self._bitmap() 

417 

418 

419# 

420# -------------------------------------------------------------------- 

421# Write BMP file 

422 

423 

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} 

431 

432 

433def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 

434 _save(im, fp, filename, False) 

435 

436 

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 

445 

446 info = im.encoderinfo 

447 

448 dpi = info.get("dpi", (96, 96)) 

449 

450 # 1 meter == 39.3701 inches 

451 ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi) 

452 

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] 

456 

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 

467 

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 ) 

481 

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 ) 

496 

497 fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) 

498 

499 if palette: 

500 fp.write(palette) 

501 

502 ImageFile._save(im, fp, [ImageFile._Tile("raw", args=(rawmode, stride, -1))]) 

503 

504 

505# 

506# -------------------------------------------------------------------- 

507# Registry 

508 

509 

510Image.register_open(BmpImageFile.format, BmpImageFile, _accept) 

511Image.register_save(BmpImageFile.format, _save) 

512 

513Image.register_extension(BmpImageFile.format, ".bmp") 

514 

515Image.register_mime(BmpImageFile.format, "image/bmp") 

516 

517Image.register_decoder("bmp_rle", BmpRleDecoder) 

518 

519Image.register_open(DibImageFile.format, DibImageFile, _dib_accept) 

520Image.register_save(DibImageFile.format, _dib_save) 

521 

522Image.register_extension(DibImageFile.format, ".dib") 

523 

524Image.register_mime(DibImageFile.format, "image/bmp")