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

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

242 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 read, seek = self.fp.read, self.fp.seek 

80 if header: 

81 seek(header) 

82 # read bmp header size @offset 14 (this is part of the header size) 

83 file_info: dict[str, bool | int | tuple[int, ...]] = { 

84 "header_size": i32(read(4)), 

85 "direction": -1, 

86 } 

87 

88 # -------------------- If requested, read header at a specific position 

89 # read the rest of the bmp header, without its size 

90 assert isinstance(file_info["header_size"], int) 

91 header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4) 

92 

93 # ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1 

94 # ----- This format has different offsets because of width/height types 

95 # 12: BITMAPCOREHEADER/OS21XBITMAPHEADER 

96 if file_info["header_size"] == 12: 

97 file_info["width"] = i16(header_data, 0) 

98 file_info["height"] = i16(header_data, 2) 

99 file_info["planes"] = i16(header_data, 4) 

100 file_info["bits"] = i16(header_data, 6) 

101 file_info["compression"] = self.COMPRESSIONS["RAW"] 

102 file_info["palette_padding"] = 3 

103 

104 # --------------------------------------------- Windows Bitmap v3 to v5 

105 # 40: BITMAPINFOHEADER 

106 # 52: BITMAPV2HEADER 

107 # 56: BITMAPV3HEADER 

108 # 64: BITMAPCOREHEADER2/OS22XBITMAPHEADER 

109 # 108: BITMAPV4HEADER 

110 # 124: BITMAPV5HEADER 

111 elif file_info["header_size"] in (40, 52, 56, 64, 108, 124): 

112 file_info["y_flip"] = header_data[7] == 0xFF 

113 file_info["direction"] = 1 if file_info["y_flip"] else -1 

114 file_info["width"] = i32(header_data, 0) 

115 file_info["height"] = ( 

116 i32(header_data, 4) 

117 if not file_info["y_flip"] 

118 else 2**32 - i32(header_data, 4) 

119 ) 

120 file_info["planes"] = i16(header_data, 8) 

121 file_info["bits"] = i16(header_data, 10) 

122 file_info["compression"] = i32(header_data, 12) 

123 # byte size of pixel data 

124 file_info["data_size"] = i32(header_data, 16) 

125 file_info["pixels_per_meter"] = ( 

126 i32(header_data, 20), 

127 i32(header_data, 24), 

128 ) 

129 file_info["colors"] = i32(header_data, 28) 

130 file_info["palette_padding"] = 4 

131 assert isinstance(file_info["pixels_per_meter"], tuple) 

132 self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"]) 

133 if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]: 

134 masks = ["r_mask", "g_mask", "b_mask"] 

135 if len(header_data) >= 48: 

136 if len(header_data) >= 52: 

137 masks.append("a_mask") 

138 else: 

139 file_info["a_mask"] = 0x0 

140 for idx, mask in enumerate(masks): 

141 file_info[mask] = i32(header_data, 36 + idx * 4) 

142 else: 

143 # 40 byte headers only have the three components in the 

144 # bitfields masks, ref: 

145 # https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx 

146 # See also 

147 # https://github.com/python-pillow/Pillow/issues/1293 

148 # There is a 4th component in the RGBQuad, in the alpha 

149 # location, but it is listed as a reserved component, 

150 # and it is not generally an alpha channel 

151 file_info["a_mask"] = 0x0 

152 for mask in masks: 

153 file_info[mask] = i32(read(4)) 

154 assert isinstance(file_info["r_mask"], int) 

155 assert isinstance(file_info["g_mask"], int) 

156 assert isinstance(file_info["b_mask"], int) 

157 assert isinstance(file_info["a_mask"], int) 

158 file_info["rgb_mask"] = ( 

159 file_info["r_mask"], 

160 file_info["g_mask"], 

161 file_info["b_mask"], 

162 ) 

163 file_info["rgba_mask"] = ( 

164 file_info["r_mask"], 

165 file_info["g_mask"], 

166 file_info["b_mask"], 

167 file_info["a_mask"], 

168 ) 

169 else: 

170 msg = f"Unsupported BMP header type ({file_info['header_size']})" 

171 raise OSError(msg) 

172 

173 # ------------------ Special case : header is reported 40, which 

174 # ---------------------- is shorter than real size for bpp >= 16 

175 assert isinstance(file_info["width"], int) 

176 assert isinstance(file_info["height"], int) 

177 self._size = file_info["width"], file_info["height"] 

178 

179 # ------- If color count was not found in the header, compute from bits 

180 assert isinstance(file_info["bits"], int) 

181 file_info["colors"] = ( 

182 file_info["colors"] 

183 if file_info.get("colors", 0) 

184 else (1 << file_info["bits"]) 

185 ) 

186 assert isinstance(file_info["colors"], int) 

187 if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8: 

188 offset += 4 * file_info["colors"] 

189 

190 # ---------------------- Check bit depth for unusual unsupported values 

191 self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", "")) 

192 if not self.mode: 

193 msg = f"Unsupported BMP pixel depth ({file_info['bits']})" 

194 raise OSError(msg) 

195 

196 # ---------------- Process BMP with Bitfields compression (not palette) 

197 decoder_name = "raw" 

198 if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]: 

199 SUPPORTED: dict[int, list[tuple[int, ...]]] = { 

200 32: [ 

201 (0xFF0000, 0xFF00, 0xFF, 0x0), 

202 (0xFF000000, 0xFF0000, 0xFF00, 0x0), 

203 (0xFF000000, 0xFF00, 0xFF, 0x0), 

204 (0xFF000000, 0xFF0000, 0xFF00, 0xFF), 

205 (0xFF, 0xFF00, 0xFF0000, 0xFF000000), 

206 (0xFF0000, 0xFF00, 0xFF, 0xFF000000), 

207 (0xFF000000, 0xFF00, 0xFF, 0xFF0000), 

208 (0x0, 0x0, 0x0, 0x0), 

209 ], 

210 24: [(0xFF0000, 0xFF00, 0xFF)], 

211 16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)], 

212 } 

213 MASK_MODES = { 

214 (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX", 

215 (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR", 

216 (32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR", 

217 (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR", 

218 (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA", 

219 (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA", 

220 (32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR", 

221 (32, (0x0, 0x0, 0x0, 0x0)): "BGRA", 

222 (24, (0xFF0000, 0xFF00, 0xFF)): "BGR", 

223 (16, (0xF800, 0x7E0, 0x1F)): "BGR;16", 

224 (16, (0x7C00, 0x3E0, 0x1F)): "BGR;15", 

225 } 

226 if file_info["bits"] in SUPPORTED: 

227 if ( 

228 file_info["bits"] == 32 

229 and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]] 

230 ): 

231 assert isinstance(file_info["rgba_mask"], tuple) 

232 raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])] 

233 self._mode = "RGBA" if "A" in raw_mode else self.mode 

234 elif ( 

235 file_info["bits"] in (24, 16) 

236 and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]] 

237 ): 

238 assert isinstance(file_info["rgb_mask"], tuple) 

239 raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])] 

240 else: 

241 msg = "Unsupported BMP bitfields layout" 

242 raise OSError(msg) 

243 else: 

244 msg = "Unsupported BMP bitfields layout" 

245 raise OSError(msg) 

246 elif file_info["compression"] == self.COMPRESSIONS["RAW"]: 

247 if file_info["bits"] == 32 and ( 

248 header == 22 or USE_RAW_ALPHA # 32-bit .cur offset 

249 ): 

250 raw_mode, self._mode = "BGRA", "RGBA" 

251 elif file_info["compression"] in ( 

252 self.COMPRESSIONS["RLE8"], 

253 self.COMPRESSIONS["RLE4"], 

254 ): 

255 decoder_name = "bmp_rle" 

256 else: 

257 msg = f"Unsupported BMP compression ({file_info['compression']})" 

258 raise OSError(msg) 

259 

260 # --------------- Once the header is processed, process the palette/LUT 

261 if self.mode == "P": # Paletted for 1, 4 and 8 bit images 

262 # ---------------------------------------------------- 1-bit images 

263 if not (0 < file_info["colors"] <= 65536): 

264 msg = f"Unsupported BMP Palette size ({file_info['colors']})" 

265 raise OSError(msg) 

266 else: 

267 assert isinstance(file_info["palette_padding"], int) 

268 padding = file_info["palette_padding"] 

269 palette = read(padding * file_info["colors"]) 

270 grayscale = True 

271 indices = ( 

272 (0, 255) 

273 if file_info["colors"] == 2 

274 else list(range(file_info["colors"])) 

275 ) 

276 

277 # ----------------- Check if grayscale and ignore palette if so 

278 for ind, val in enumerate(indices): 

279 rgb = palette[ind * padding : ind * padding + 3] 

280 if rgb != o8(val) * 3: 

281 grayscale = False 

282 

283 # ------- If all colors are gray, white or black, ditch palette 

284 if grayscale: 

285 self._mode = "1" if file_info["colors"] == 2 else "L" 

286 raw_mode = self.mode 

287 else: 

288 self._mode = "P" 

289 self.palette = ImagePalette.raw( 

290 "BGRX" if padding == 4 else "BGR", palette 

291 ) 

292 

293 # ---------------------------- Finally set the tile data for the plugin 

294 self.info["compression"] = file_info["compression"] 

295 args: list[Any] = [raw_mode] 

296 if decoder_name == "bmp_rle": 

297 args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"]) 

298 else: 

299 assert isinstance(file_info["width"], int) 

300 args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3)) 

301 args.append(file_info["direction"]) 

302 self.tile = [ 

303 ImageFile._Tile( 

304 decoder_name, 

305 (0, 0, file_info["width"], file_info["height"]), 

306 offset or self.fp.tell(), 

307 tuple(args), 

308 ) 

309 ] 

310 

311 def _open(self) -> None: 

312 """Open file, check magic number and read header""" 

313 # read 14 bytes: magic number, filesize, reserved, header final offset 

314 head_data = self.fp.read(14) 

315 # choke if the file does not have the required magic bytes 

316 if not _accept(head_data): 

317 msg = "Not a BMP file" 

318 raise SyntaxError(msg) 

319 # read the start position of the BMP image data (u32) 

320 offset = i32(head_data, 10) 

321 # load bitmap information (offset=raster info) 

322 self._bitmap(offset=offset) 

323 

324 

325class BmpRleDecoder(ImageFile.PyDecoder): 

326 _pulls_fd = True 

327 

328 def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: 

329 assert self.fd is not None 

330 rle4 = self.args[1] 

331 data = bytearray() 

332 x = 0 

333 dest_length = self.state.xsize * self.state.ysize 

334 while len(data) < dest_length: 

335 pixels = self.fd.read(1) 

336 byte = self.fd.read(1) 

337 if not pixels or not byte: 

338 break 

339 num_pixels = pixels[0] 

340 if num_pixels: 

341 # encoded mode 

342 if x + num_pixels > self.state.xsize: 

343 # Too much data for row 

344 num_pixels = max(0, self.state.xsize - x) 

345 if rle4: 

346 first_pixel = o8(byte[0] >> 4) 

347 second_pixel = o8(byte[0] & 0x0F) 

348 for index in range(num_pixels): 

349 if index % 2 == 0: 

350 data += first_pixel 

351 else: 

352 data += second_pixel 

353 else: 

354 data += byte * num_pixels 

355 x += num_pixels 

356 else: 

357 if byte[0] == 0: 

358 # end of line 

359 while len(data) % self.state.xsize != 0: 

360 data += b"\x00" 

361 x = 0 

362 elif byte[0] == 1: 

363 # end of bitmap 

364 break 

365 elif byte[0] == 2: 

366 # delta 

367 bytes_read = self.fd.read(2) 

368 if len(bytes_read) < 2: 

369 break 

370 right, up = self.fd.read(2) 

371 data += b"\x00" * (right + up * self.state.xsize) 

372 x = len(data) % self.state.xsize 

373 else: 

374 # absolute mode 

375 if rle4: 

376 # 2 pixels per byte 

377 byte_count = byte[0] // 2 

378 bytes_read = self.fd.read(byte_count) 

379 for byte_read in bytes_read: 

380 data += o8(byte_read >> 4) 

381 data += o8(byte_read & 0x0F) 

382 else: 

383 byte_count = byte[0] 

384 bytes_read = self.fd.read(byte_count) 

385 data += bytes_read 

386 if len(bytes_read) < byte_count: 

387 break 

388 x += byte[0] 

389 

390 # align to 16-bit word boundary 

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

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

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

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

395 return -1, 0 

396 

397 

398# ============================================================================= 

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

400# ============================================================================= 

401class DibImageFile(BmpImageFile): 

402 format = "DIB" 

403 format_description = "Windows Bitmap" 

404 

405 def _open(self) -> None: 

406 self._bitmap() 

407 

408 

409# 

410# -------------------------------------------------------------------- 

411# Write BMP file 

412 

413 

414SAVE = { 

415 "1": ("1", 1, 2), 

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

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

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

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

420} 

421 

422 

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

424 _save(im, fp, filename, False) 

425 

426 

427def _save( 

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

429) -> None: 

430 try: 

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

432 except KeyError as e: 

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

434 raise OSError(msg) from e 

435 

436 info = im.encoderinfo 

437 

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

439 

440 # 1 meter == 39.3701 inches 

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

442 

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

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

445 image = stride * im.size[1] 

446 

447 if im.mode == "1": 

448 palette = b"".join(o8(i) * 4 for i in (0, 255)) 

449 elif im.mode == "L": 

450 palette = b"".join(o8(i) * 4 for i in range(256)) 

451 elif im.mode == "P": 

452 palette = im.im.getpalette("RGB", "BGRX") 

453 colors = len(palette) // 4 

454 else: 

455 palette = None 

456 

457 # bitmap header 

458 if bitmap_header: 

459 offset = 14 + header + colors * 4 

460 file_size = offset + image 

461 if file_size > 2**32 - 1: 

462 msg = "File size is too large for the BMP format" 

463 raise ValueError(msg) 

464 fp.write( 

465 b"BM" # file type (magic) 

466 + o32(file_size) # file size 

467 + o32(0) # reserved 

468 + o32(offset) # image data offset 

469 ) 

470 

471 # bitmap info header 

472 fp.write( 

473 o32(header) # info header size 

474 + o32(im.size[0]) # width 

475 + o32(im.size[1]) # height 

476 + o16(1) # planes 

477 + o16(bits) # depth 

478 + o32(0) # compression (0=uncompressed) 

479 + o32(image) # size of bitmap 

480 + o32(ppm[0]) # resolution 

481 + o32(ppm[1]) # resolution 

482 + o32(colors) # colors used 

483 + o32(colors) # colors important 

484 ) 

485 

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

487 

488 if palette: 

489 fp.write(palette) 

490 

491 ImageFile._save( 

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

493 ) 

494 

495 

496# 

497# -------------------------------------------------------------------- 

498# Registry 

499 

500 

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

502Image.register_save(BmpImageFile.format, _save) 

503 

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

505 

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

507 

508Image.register_decoder("bmp_rle", BmpRleDecoder) 

509 

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

511Image.register_save(DibImageFile.format, _dib_save) 

512 

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

514 

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