Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/pillow-11.0.0-py3.10-linux-x86_64.egg/PIL/BmpImagePlugin.py: 17%

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

240 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 

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