Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/BmpImagePlugin.py: 87%

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

247 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 

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 

51USE_RAW_ALPHA = False 

52 

53 

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

55 return prefix.startswith(b"BM") 

56 

57 

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

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

60 

61 

62# ============================================================================= 

63# Image plugin for the Windows BMP format. 

64# ============================================================================= 

65class BmpImageFile(ImageFile.ImageFile): 

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

67 

68 # ------------------------------------------------------------- Description 

69 format_description = "Windows Bitmap" 

70 format = "BMP" 

71 

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 

76 

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 } 

88 

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) 

93 

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 

104 

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) 

173 

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

179 

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

188 

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) 

194 

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) 

258 

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 ) 

274 

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 

280 

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 ) 

290 

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 ] 

308 

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) 

322 

323 

324class BmpRleDecoder(ImageFile.PyDecoder): 

325 _pulls_fd = True 

326 

327 def decode(self, buffer: Image.DecoderInput) -> 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 data_count = min(dest_length - len(data), byte[0]) 

375 if rle4: 

376 # 2 pixels per byte, padded up to a whole byte 

377 byte_count = (data_count + 1) // 2 

378 bytes_read = self.fd.read(byte_count) 

379 for i, byte_read in enumerate(bytes_read): 

380 data += o8(byte_read >> 4) 

381 if i * 2 + 1 < data_count: 

382 data += o8(byte_read & 0x0F) 

383 else: 

384 byte_count = data_count 

385 bytes_read = self.fd.read(byte_count) 

386 data += bytes_read 

387 if len(bytes_read) < byte_count or len(data) == dest_length: 

388 break 

389 x += byte[0] 

390 

391 # align to 16-bit word boundary 

392 if self.fd.tell() % 2 != 0: 

393 self.fd.seek(1, os.SEEK_CUR) 

394 rawmode = "L" if self.mode == "L" else "P" 

395 self.set_as_raw(bytes(data), rawmode, (0, self.args[-1])) 

396 return -1, 0 

397 

398 

399# ============================================================================= 

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

401# ============================================================================= 

402class DibImageFile(BmpImageFile): 

403 format = "DIB" 

404 format_description = "Windows Bitmap" 

405 

406 def _open(self) -> None: 

407 self._bitmap() 

408 

409 

410# 

411# -------------------------------------------------------------------- 

412# Write BMP file 

413 

414 

415SAVE = { 

416 "1": ("1", 1, 2), 

417 "L": ("L", 8, 256), 

418 "P": ("P", 8, 256), 

419 "RGB": ("BGR", 24, 0), 

420 "RGBA": ("BGRA", 32, 0), 

421} 

422 

423 

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

425 _save(im, fp, filename, False) 

426 

427 

428def _save( 

429 im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True 

430) -> None: 

431 try: 

432 rawmode, bits, colors = SAVE[im.mode] 

433 except KeyError as e: 

434 msg = f"cannot write mode {im.mode} as BMP" 

435 raise OSError(msg) from e 

436 

437 info = im.encoderinfo 

438 

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

440 

441 # 1 meter == 39.3701 inches 

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

443 

444 stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3) 

445 header = 40 # or 64 for OS/2 version 2 

446 image = stride * im.size[1] 

447 

448 if im.mode == "1": 

449 palette = b"".join(o8(i) * 3 + b"\x00" for i in (0, 255)) 

450 elif im.mode == "L": 

451 palette = b"".join(o8(i) * 3 + b"\x00" for i in range(256)) 

452 elif im.mode == "P": 

453 # Colors used should not be zero, as that is treated as the maximum 

454 palette = im.im.getpalette("RGB", "BGRX") or b"\x00\x00\x00\x00" 

455 colors = len(palette) // 4 

456 else: 

457 palette = None 

458 

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 ) 

472 

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 ) 

487 

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

489 

490 if palette: 

491 fp.write(palette) 

492 

493 ImageFile._save( 

494 im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))] 

495 ) 

496 

497 

498# 

499# -------------------------------------------------------------------- 

500# Registry 

501 

502 

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

504Image.register_save(BmpImageFile.format, _save) 

505 

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

507 

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

509 

510Image.register_decoder("bmp_rle", BmpRleDecoder) 

511 

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

513Image.register_save(DibImageFile.format, _dib_save) 

514 

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

516 

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