Coverage for /usr/lib/python3/dist-packages/PIL/PngImagePlugin.py: 13%

875 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1# 

2# The Python Imaging Library. 

3# $Id$ 

4# 

5# PNG support code 

6# 

7# See "PNG (Portable Network Graphics) Specification, version 1.0; 

8# W3C Recommendation", 1996-10-01, Thomas Boutell (ed.). 

9# 

10# history: 

11# 1996-05-06 fl Created (couldn't resist it) 

12# 1996-12-14 fl Upgraded, added read and verify support (0.2) 

13# 1996-12-15 fl Separate PNG stream parser 

14# 1996-12-29 fl Added write support, added getchunks 

15# 1996-12-30 fl Eliminated circular references in decoder (0.3) 

16# 1998-07-12 fl Read/write 16-bit images as mode I (0.4) 

17# 2001-02-08 fl Added transparency support (from Zircon) (0.5) 

18# 2001-04-16 fl Don't close data source in "open" method (0.6) 

19# 2004-02-24 fl Don't even pretend to support interlaced files (0.7) 

20# 2004-08-31 fl Do basic sanity check on chunk identifiers (0.8) 

21# 2004-09-20 fl Added PngInfo chunk container 

22# 2004-12-18 fl Added DPI read support (based on code by Niki Spahiev) 

23# 2008-08-13 fl Added tRNS support for RGB images 

24# 2009-03-06 fl Support for preserving ICC profiles (by Florian Hoech) 

25# 2009-03-08 fl Added zTXT support (from Lowell Alleman) 

26# 2009-03-29 fl Read interlaced PNG files (from Conrado Porto Lopes Gouvua) 

27# 

28# Copyright (c) 1997-2009 by Secret Labs AB 

29# Copyright (c) 1996 by Fredrik Lundh 

30# 

31# See the README file for information on usage and redistribution. 

32# 

33from __future__ import annotations 

34 

35import itertools 

36import logging 

37import re 

38import struct 

39import warnings 

40import zlib 

41from enum import IntEnum 

42 

43from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence 

44from ._binary import i16be as i16 

45from ._binary import i32be as i32 

46from ._binary import o8 

47from ._binary import o16be as o16 

48from ._binary import o32be as o32 

49 

50logger = logging.getLogger(__name__) 

51 

52is_cid = re.compile(rb"\w\w\w\w").match 

53 

54 

55_MAGIC = b"\211PNG\r\n\032\n" 

56 

57 

58_MODES = { 

59 # supported bits/color combinations, and corresponding modes/rawmodes 

60 # Grayscale 

61 (1, 0): ("1", "1"), 

62 (2, 0): ("L", "L;2"), 

63 (4, 0): ("L", "L;4"), 

64 (8, 0): ("L", "L"), 

65 (16, 0): ("I", "I;16B"), 

66 # Truecolour 

67 (8, 2): ("RGB", "RGB"), 

68 (16, 2): ("RGB", "RGB;16B"), 

69 # Indexed-colour 

70 (1, 3): ("P", "P;1"), 

71 (2, 3): ("P", "P;2"), 

72 (4, 3): ("P", "P;4"), 

73 (8, 3): ("P", "P"), 

74 # Grayscale with alpha 

75 (8, 4): ("LA", "LA"), 

76 (16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available 

77 # Truecolour with alpha 

78 (8, 6): ("RGBA", "RGBA"), 

79 (16, 6): ("RGBA", "RGBA;16B"), 

80} 

81 

82 

83_simple_palette = re.compile(b"^\xff*\x00\xff*$") 

84 

85MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK 

86""" 

87Maximum decompressed size for a iTXt or zTXt chunk. 

88Eliminates decompression bombs where compressed chunks can expand 1000x. 

89See :ref:`Text in PNG File Format<png-text>`. 

90""" 

91MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK 

92""" 

93Set the maximum total text chunk size. 

94See :ref:`Text in PNG File Format<png-text>`. 

95""" 

96 

97 

98# APNG frame disposal modes 

99class Disposal(IntEnum): 

100 OP_NONE = 0 

101 """ 

102 No disposal is done on this frame before rendering the next frame. 

103 See :ref:`Saving APNG sequences<apng-saving>`. 

104 """ 

105 OP_BACKGROUND = 1 

106 """ 

107 This frame’s modified region is cleared to fully transparent black before rendering 

108 the next frame. 

109 See :ref:`Saving APNG sequences<apng-saving>`. 

110 """ 

111 OP_PREVIOUS = 2 

112 """ 

113 This frame’s modified region is reverted to the previous frame’s contents before 

114 rendering the next frame. 

115 See :ref:`Saving APNG sequences<apng-saving>`. 

116 """ 

117 

118 

119# APNG frame blend modes 

120class Blend(IntEnum): 

121 OP_SOURCE = 0 

122 """ 

123 All color components of this frame, including alpha, overwrite the previous output 

124 image contents. 

125 See :ref:`Saving APNG sequences<apng-saving>`. 

126 """ 

127 OP_OVER = 1 

128 """ 

129 This frame should be alpha composited with the previous output image contents. 

130 See :ref:`Saving APNG sequences<apng-saving>`. 

131 """ 

132 

133 

134def _safe_zlib_decompress(s): 

135 dobj = zlib.decompressobj() 

136 plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) 

137 if dobj.unconsumed_tail: 

138 msg = "Decompressed Data Too Large" 

139 raise ValueError(msg) 

140 return plaintext 

141 

142 

143def _crc32(data, seed=0): 

144 return zlib.crc32(data, seed) & 0xFFFFFFFF 

145 

146 

147# -------------------------------------------------------------------- 

148# Support classes. Suitable for PNG and related formats like MNG etc. 

149 

150 

151class ChunkStream: 

152 def __init__(self, fp): 

153 self.fp = fp 

154 self.queue = [] 

155 

156 def read(self): 

157 """Fetch a new chunk. Returns header information.""" 

158 cid = None 

159 

160 if self.queue: 

161 cid, pos, length = self.queue.pop() 

162 self.fp.seek(pos) 

163 else: 

164 s = self.fp.read(8) 

165 cid = s[4:] 

166 pos = self.fp.tell() 

167 length = i32(s) 

168 

169 if not is_cid(cid): 

170 if not ImageFile.LOAD_TRUNCATED_IMAGES: 

171 msg = f"broken PNG file (chunk {repr(cid)})" 

172 raise SyntaxError(msg) 

173 

174 return cid, pos, length 

175 

176 def __enter__(self): 

177 return self 

178 

179 def __exit__(self, *args): 

180 self.close() 

181 

182 def close(self): 

183 self.queue = self.fp = None 

184 

185 def push(self, cid, pos, length): 

186 self.queue.append((cid, pos, length)) 

187 

188 def call(self, cid, pos, length): 

189 """Call the appropriate chunk handler""" 

190 

191 logger.debug("STREAM %r %s %s", cid, pos, length) 

192 return getattr(self, "chunk_" + cid.decode("ascii"))(pos, length) 

193 

194 def crc(self, cid, data): 

195 """Read and verify checksum""" 

196 

197 # Skip CRC checks for ancillary chunks if allowed to load truncated 

198 # images 

199 # 5th byte of first char is 1 [specs, section 5.4] 

200 if ImageFile.LOAD_TRUNCATED_IMAGES and (cid[0] >> 5 & 1): 

201 self.crc_skip(cid, data) 

202 return 

203 

204 try: 

205 crc1 = _crc32(data, _crc32(cid)) 

206 crc2 = i32(self.fp.read(4)) 

207 if crc1 != crc2: 

208 msg = f"broken PNG file (bad header checksum in {repr(cid)})" 

209 raise SyntaxError(msg) 

210 except struct.error as e: 

211 msg = f"broken PNG file (incomplete checksum in {repr(cid)})" 

212 raise SyntaxError(msg) from e 

213 

214 def crc_skip(self, cid, data): 

215 """Read checksum""" 

216 

217 self.fp.read(4) 

218 

219 def verify(self, endchunk=b"IEND"): 

220 # Simple approach; just calculate checksum for all remaining 

221 # blocks. Must be called directly after open. 

222 

223 cids = [] 

224 

225 while True: 

226 try: 

227 cid, pos, length = self.read() 

228 except struct.error as e: 

229 msg = "truncated PNG file" 

230 raise OSError(msg) from e 

231 

232 if cid == endchunk: 

233 break 

234 self.crc(cid, ImageFile._safe_read(self.fp, length)) 

235 cids.append(cid) 

236 

237 return cids 

238 

239 

240class iTXt(str): 

241 """ 

242 Subclass of string to allow iTXt chunks to look like strings while 

243 keeping their extra information 

244 

245 """ 

246 

247 @staticmethod 

248 def __new__(cls, text, lang=None, tkey=None): 

249 """ 

250 :param cls: the class to use when creating the instance 

251 :param text: value for this key 

252 :param lang: language code 

253 :param tkey: UTF-8 version of the key name 

254 """ 

255 

256 self = str.__new__(cls, text) 

257 self.lang = lang 

258 self.tkey = tkey 

259 return self 

260 

261 

262class PngInfo: 

263 """ 

264 PNG chunk container (for use with save(pnginfo=)) 

265 

266 """ 

267 

268 def __init__(self): 

269 self.chunks = [] 

270 

271 def add(self, cid, data, after_idat=False): 

272 """Appends an arbitrary chunk. Use with caution. 

273 

274 :param cid: a byte string, 4 bytes long. 

275 :param data: a byte string of the encoded data 

276 :param after_idat: for use with private chunks. Whether the chunk 

277 should be written after IDAT 

278 

279 """ 

280 

281 chunk = [cid, data] 

282 if after_idat: 

283 chunk.append(True) 

284 self.chunks.append(tuple(chunk)) 

285 

286 def add_itxt(self, key, value, lang="", tkey="", zip=False): 

287 """Appends an iTXt chunk. 

288 

289 :param key: latin-1 encodable text key name 

290 :param value: value for this key 

291 :param lang: language code 

292 :param tkey: UTF-8 version of the key name 

293 :param zip: compression flag 

294 

295 """ 

296 

297 if not isinstance(key, bytes): 

298 key = key.encode("latin-1", "strict") 

299 if not isinstance(value, bytes): 

300 value = value.encode("utf-8", "strict") 

301 if not isinstance(lang, bytes): 

302 lang = lang.encode("utf-8", "strict") 

303 if not isinstance(tkey, bytes): 

304 tkey = tkey.encode("utf-8", "strict") 

305 

306 if zip: 

307 self.add( 

308 b"iTXt", 

309 key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value), 

310 ) 

311 else: 

312 self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value) 

313 

314 def add_text(self, key, value, zip=False): 

315 """Appends a text chunk. 

316 

317 :param key: latin-1 encodable text key name 

318 :param value: value for this key, text or an 

319 :py:class:`PIL.PngImagePlugin.iTXt` instance 

320 :param zip: compression flag 

321 

322 """ 

323 if isinstance(value, iTXt): 

324 return self.add_itxt(key, value, value.lang, value.tkey, zip=zip) 

325 

326 # The tEXt chunk stores latin-1 text 

327 if not isinstance(value, bytes): 

328 try: 

329 value = value.encode("latin-1", "strict") 

330 except UnicodeError: 

331 return self.add_itxt(key, value, zip=zip) 

332 

333 if not isinstance(key, bytes): 

334 key = key.encode("latin-1", "strict") 

335 

336 if zip: 

337 self.add(b"zTXt", key + b"\0\0" + zlib.compress(value)) 

338 else: 

339 self.add(b"tEXt", key + b"\0" + value) 

340 

341 

342# -------------------------------------------------------------------- 

343# PNG image stream (IHDR/IEND) 

344 

345 

346class PngStream(ChunkStream): 

347 def __init__(self, fp): 

348 super().__init__(fp) 

349 

350 # local copies of Image attributes 

351 self.im_info = {} 

352 self.im_text = {} 

353 self.im_size = (0, 0) 

354 self.im_mode = None 

355 self.im_tile = None 

356 self.im_palette = None 

357 self.im_custom_mimetype = None 

358 self.im_n_frames = None 

359 self._seq_num = None 

360 self.rewind_state = None 

361 

362 self.text_memory = 0 

363 

364 def check_text_memory(self, chunklen): 

365 self.text_memory += chunklen 

366 if self.text_memory > MAX_TEXT_MEMORY: 

367 msg = ( 

368 "Too much memory used in text chunks: " 

369 f"{self.text_memory}>MAX_TEXT_MEMORY" 

370 ) 

371 raise ValueError(msg) 

372 

373 def save_rewind(self): 

374 self.rewind_state = { 

375 "info": self.im_info.copy(), 

376 "tile": self.im_tile, 

377 "seq_num": self._seq_num, 

378 } 

379 

380 def rewind(self): 

381 self.im_info = self.rewind_state["info"] 

382 self.im_tile = self.rewind_state["tile"] 

383 self._seq_num = self.rewind_state["seq_num"] 

384 

385 def chunk_iCCP(self, pos, length): 

386 # ICC profile 

387 s = ImageFile._safe_read(self.fp, length) 

388 # according to PNG spec, the iCCP chunk contains: 

389 # Profile name 1-79 bytes (character string) 

390 # Null separator 1 byte (null character) 

391 # Compression method 1 byte (0) 

392 # Compressed profile n bytes (zlib with deflate compression) 

393 i = s.find(b"\0") 

394 logger.debug("iCCP profile name %r", s[:i]) 

395 logger.debug("Compression method %s", s[i]) 

396 comp_method = s[i] 

397 if comp_method != 0: 

398 msg = f"Unknown compression method {comp_method} in iCCP chunk" 

399 raise SyntaxError(msg) 

400 try: 

401 icc_profile = _safe_zlib_decompress(s[i + 2 :]) 

402 except ValueError: 

403 if ImageFile.LOAD_TRUNCATED_IMAGES: 

404 icc_profile = None 

405 else: 

406 raise 

407 except zlib.error: 

408 icc_profile = None # FIXME 

409 self.im_info["icc_profile"] = icc_profile 

410 return s 

411 

412 def chunk_IHDR(self, pos, length): 

413 # image header 

414 s = ImageFile._safe_read(self.fp, length) 

415 if length < 13: 

416 if ImageFile.LOAD_TRUNCATED_IMAGES: 

417 return s 

418 msg = "Truncated IHDR chunk" 

419 raise ValueError(msg) 

420 self.im_size = i32(s, 0), i32(s, 4) 

421 try: 

422 self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])] 

423 except Exception: 

424 pass 

425 if s[12]: 

426 self.im_info["interlace"] = 1 

427 if s[11]: 

428 msg = "unknown filter category" 

429 raise SyntaxError(msg) 

430 return s 

431 

432 def chunk_IDAT(self, pos, length): 

433 # image data 

434 if "bbox" in self.im_info: 

435 tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] 

436 else: 

437 if self.im_n_frames is not None: 

438 self.im_info["default_image"] = True 

439 tile = [("zip", (0, 0) + self.im_size, pos, self.im_rawmode)] 

440 self.im_tile = tile 

441 self.im_idat = length 

442 msg = "image data found" 

443 raise EOFError(msg) 

444 

445 def chunk_IEND(self, pos, length): 

446 msg = "end of PNG image" 

447 raise EOFError(msg) 

448 

449 def chunk_PLTE(self, pos, length): 

450 # palette 

451 s = ImageFile._safe_read(self.fp, length) 

452 if self.im_mode == "P": 

453 self.im_palette = "RGB", s 

454 return s 

455 

456 def chunk_tRNS(self, pos, length): 

457 # transparency 

458 s = ImageFile._safe_read(self.fp, length) 

459 if self.im_mode == "P": 

460 if _simple_palette.match(s): 

461 # tRNS contains only one full-transparent entry, 

462 # other entries are full opaque 

463 i = s.find(b"\0") 

464 if i >= 0: 

465 self.im_info["transparency"] = i 

466 else: 

467 # otherwise, we have a byte string with one alpha value 

468 # for each palette entry 

469 self.im_info["transparency"] = s 

470 elif self.im_mode in ("1", "L", "I"): 

471 self.im_info["transparency"] = i16(s) 

472 elif self.im_mode == "RGB": 

473 self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4) 

474 return s 

475 

476 def chunk_gAMA(self, pos, length): 

477 # gamma setting 

478 s = ImageFile._safe_read(self.fp, length) 

479 self.im_info["gamma"] = i32(s) / 100000.0 

480 return s 

481 

482 def chunk_cHRM(self, pos, length): 

483 # chromaticity, 8 unsigned ints, actual value is scaled by 100,000 

484 # WP x,y, Red x,y, Green x,y Blue x,y 

485 

486 s = ImageFile._safe_read(self.fp, length) 

487 raw_vals = struct.unpack(">%dI" % (len(s) // 4), s) 

488 self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) 

489 return s 

490 

491 def chunk_sRGB(self, pos, length): 

492 # srgb rendering intent, 1 byte 

493 # 0 perceptual 

494 # 1 relative colorimetric 

495 # 2 saturation 

496 # 3 absolute colorimetric 

497 

498 s = ImageFile._safe_read(self.fp, length) 

499 if length < 1: 

500 if ImageFile.LOAD_TRUNCATED_IMAGES: 

501 return s 

502 msg = "Truncated sRGB chunk" 

503 raise ValueError(msg) 

504 self.im_info["srgb"] = s[0] 

505 return s 

506 

507 def chunk_pHYs(self, pos, length): 

508 # pixels per unit 

509 s = ImageFile._safe_read(self.fp, length) 

510 if length < 9: 

511 if ImageFile.LOAD_TRUNCATED_IMAGES: 

512 return s 

513 msg = "Truncated pHYs chunk" 

514 raise ValueError(msg) 

515 px, py = i32(s, 0), i32(s, 4) 

516 unit = s[8] 

517 if unit == 1: # meter 

518 dpi = px * 0.0254, py * 0.0254 

519 self.im_info["dpi"] = dpi 

520 elif unit == 0: 

521 self.im_info["aspect"] = px, py 

522 return s 

523 

524 def chunk_tEXt(self, pos, length): 

525 # text 

526 s = ImageFile._safe_read(self.fp, length) 

527 try: 

528 k, v = s.split(b"\0", 1) 

529 except ValueError: 

530 # fallback for broken tEXt tags 

531 k = s 

532 v = b"" 

533 if k: 

534 k = k.decode("latin-1", "strict") 

535 v_str = v.decode("latin-1", "replace") 

536 

537 self.im_info[k] = v if k == "exif" else v_str 

538 self.im_text[k] = v_str 

539 self.check_text_memory(len(v_str)) 

540 

541 return s 

542 

543 def chunk_zTXt(self, pos, length): 

544 # compressed text 

545 s = ImageFile._safe_read(self.fp, length) 

546 try: 

547 k, v = s.split(b"\0", 1) 

548 except ValueError: 

549 k = s 

550 v = b"" 

551 if v: 

552 comp_method = v[0] 

553 else: 

554 comp_method = 0 

555 if comp_method != 0: 

556 msg = f"Unknown compression method {comp_method} in zTXt chunk" 

557 raise SyntaxError(msg) 

558 try: 

559 v = _safe_zlib_decompress(v[1:]) 

560 except ValueError: 

561 if ImageFile.LOAD_TRUNCATED_IMAGES: 

562 v = b"" 

563 else: 

564 raise 

565 except zlib.error: 

566 v = b"" 

567 

568 if k: 

569 k = k.decode("latin-1", "strict") 

570 v = v.decode("latin-1", "replace") 

571 

572 self.im_info[k] = self.im_text[k] = v 

573 self.check_text_memory(len(v)) 

574 

575 return s 

576 

577 def chunk_iTXt(self, pos, length): 

578 # international text 

579 r = s = ImageFile._safe_read(self.fp, length) 

580 try: 

581 k, r = r.split(b"\0", 1) 

582 except ValueError: 

583 return s 

584 if len(r) < 2: 

585 return s 

586 cf, cm, r = r[0], r[1], r[2:] 

587 try: 

588 lang, tk, v = r.split(b"\0", 2) 

589 except ValueError: 

590 return s 

591 if cf != 0: 

592 if cm == 0: 

593 try: 

594 v = _safe_zlib_decompress(v) 

595 except ValueError: 

596 if ImageFile.LOAD_TRUNCATED_IMAGES: 

597 return s 

598 else: 

599 raise 

600 except zlib.error: 

601 return s 

602 else: 

603 return s 

604 try: 

605 k = k.decode("latin-1", "strict") 

606 lang = lang.decode("utf-8", "strict") 

607 tk = tk.decode("utf-8", "strict") 

608 v = v.decode("utf-8", "strict") 

609 except UnicodeError: 

610 return s 

611 

612 self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk) 

613 self.check_text_memory(len(v)) 

614 

615 return s 

616 

617 def chunk_eXIf(self, pos, length): 

618 s = ImageFile._safe_read(self.fp, length) 

619 self.im_info["exif"] = b"Exif\x00\x00" + s 

620 return s 

621 

622 # APNG chunks 

623 def chunk_acTL(self, pos, length): 

624 s = ImageFile._safe_read(self.fp, length) 

625 if length < 8: 

626 if ImageFile.LOAD_TRUNCATED_IMAGES: 

627 return s 

628 msg = "APNG contains truncated acTL chunk" 

629 raise ValueError(msg) 

630 if self.im_n_frames is not None: 

631 self.im_n_frames = None 

632 warnings.warn("Invalid APNG, will use default PNG image if possible") 

633 return s 

634 n_frames = i32(s) 

635 if n_frames == 0 or n_frames > 0x80000000: 

636 warnings.warn("Invalid APNG, will use default PNG image if possible") 

637 return s 

638 self.im_n_frames = n_frames 

639 self.im_info["loop"] = i32(s, 4) 

640 self.im_custom_mimetype = "image/apng" 

641 return s 

642 

643 def chunk_fcTL(self, pos, length): 

644 s = ImageFile._safe_read(self.fp, length) 

645 if length < 26: 

646 if ImageFile.LOAD_TRUNCATED_IMAGES: 

647 return s 

648 msg = "APNG contains truncated fcTL chunk" 

649 raise ValueError(msg) 

650 seq = i32(s) 

651 if (self._seq_num is None and seq != 0) or ( 

652 self._seq_num is not None and self._seq_num != seq - 1 

653 ): 

654 msg = "APNG contains frame sequence errors" 

655 raise SyntaxError(msg) 

656 self._seq_num = seq 

657 width, height = i32(s, 4), i32(s, 8) 

658 px, py = i32(s, 12), i32(s, 16) 

659 im_w, im_h = self.im_size 

660 if px + width > im_w or py + height > im_h: 

661 msg = "APNG contains invalid frames" 

662 raise SyntaxError(msg) 

663 self.im_info["bbox"] = (px, py, px + width, py + height) 

664 delay_num, delay_den = i16(s, 20), i16(s, 22) 

665 if delay_den == 0: 

666 delay_den = 100 

667 self.im_info["duration"] = float(delay_num) / float(delay_den) * 1000 

668 self.im_info["disposal"] = s[24] 

669 self.im_info["blend"] = s[25] 

670 return s 

671 

672 def chunk_fdAT(self, pos, length): 

673 if length < 4: 

674 if ImageFile.LOAD_TRUNCATED_IMAGES: 

675 s = ImageFile._safe_read(self.fp, length) 

676 return s 

677 msg = "APNG contains truncated fDAT chunk" 

678 raise ValueError(msg) 

679 s = ImageFile._safe_read(self.fp, 4) 

680 seq = i32(s) 

681 if self._seq_num != seq - 1: 

682 msg = "APNG contains frame sequence errors" 

683 raise SyntaxError(msg) 

684 self._seq_num = seq 

685 return self.chunk_IDAT(pos + 4, length - 4) 

686 

687 

688# -------------------------------------------------------------------- 

689# PNG reader 

690 

691 

692def _accept(prefix): 

693 return prefix[:8] == _MAGIC 

694 

695 

696## 

697# Image plugin for PNG images. 

698 

699 

700class PngImageFile(ImageFile.ImageFile): 

701 format = "PNG" 

702 format_description = "Portable network graphics" 

703 

704 def _open(self): 

705 if not _accept(self.fp.read(8)): 

706 msg = "not a PNG file" 

707 raise SyntaxError(msg) 

708 self._fp = self.fp 

709 self.__frame = 0 

710 

711 # 

712 # Parse headers up to the first IDAT or fDAT chunk 

713 

714 self.private_chunks = [] 

715 self.png = PngStream(self.fp) 

716 

717 while True: 

718 # 

719 # get next chunk 

720 

721 cid, pos, length = self.png.read() 

722 

723 try: 

724 s = self.png.call(cid, pos, length) 

725 except EOFError: 

726 break 

727 except AttributeError: 

728 logger.debug("%r %s %s (unknown)", cid, pos, length) 

729 s = ImageFile._safe_read(self.fp, length) 

730 if cid[1:2].islower(): 

731 self.private_chunks.append((cid, s)) 

732 

733 self.png.crc(cid, s) 

734 

735 # 

736 # Copy relevant attributes from the PngStream. An alternative 

737 # would be to let the PngStream class modify these attributes 

738 # directly, but that introduces circular references which are 

739 # difficult to break if things go wrong in the decoder... 

740 # (believe me, I've tried ;-) 

741 

742 self._mode = self.png.im_mode 

743 self._size = self.png.im_size 

744 self.info = self.png.im_info 

745 self._text = None 

746 self.tile = self.png.im_tile 

747 self.custom_mimetype = self.png.im_custom_mimetype 

748 self.n_frames = self.png.im_n_frames or 1 

749 self.default_image = self.info.get("default_image", False) 

750 

751 if self.png.im_palette: 

752 rawmode, data = self.png.im_palette 

753 self.palette = ImagePalette.raw(rawmode, data) 

754 

755 if cid == b"fdAT": 

756 self.__prepare_idat = length - 4 

757 else: 

758 self.__prepare_idat = length # used by load_prepare() 

759 

760 if self.png.im_n_frames is not None: 

761 self._close_exclusive_fp_after_loading = False 

762 self.png.save_rewind() 

763 self.__rewind_idat = self.__prepare_idat 

764 self.__rewind = self._fp.tell() 

765 if self.default_image: 

766 # IDAT chunk contains default image and not first animation frame 

767 self.n_frames += 1 

768 self._seek(0) 

769 self.is_animated = self.n_frames > 1 

770 

771 @property 

772 def text(self): 

773 # experimental 

774 if self._text is None: 

775 # iTxt, tEXt and zTXt chunks may appear at the end of the file 

776 # So load the file to ensure that they are read 

777 if self.is_animated: 

778 frame = self.__frame 

779 # for APNG, seek to the final frame before loading 

780 self.seek(self.n_frames - 1) 

781 self.load() 

782 if self.is_animated: 

783 self.seek(frame) 

784 return self._text 

785 

786 def verify(self): 

787 """Verify PNG file""" 

788 

789 if self.fp is None: 

790 msg = "verify must be called directly after open" 

791 raise RuntimeError(msg) 

792 

793 # back up to beginning of IDAT block 

794 self.fp.seek(self.tile[0][2] - 8) 

795 

796 self.png.verify() 

797 self.png.close() 

798 

799 if self._exclusive_fp: 

800 self.fp.close() 

801 self.fp = None 

802 

803 def seek(self, frame): 

804 if not self._seek_check(frame): 

805 return 

806 if frame < self.__frame: 

807 self._seek(0, True) 

808 

809 last_frame = self.__frame 

810 for f in range(self.__frame + 1, frame + 1): 

811 try: 

812 self._seek(f) 

813 except EOFError as e: 

814 self.seek(last_frame) 

815 msg = "no more images in APNG file" 

816 raise EOFError(msg) from e 

817 

818 def _seek(self, frame, rewind=False): 

819 if frame == 0: 

820 if rewind: 

821 self._fp.seek(self.__rewind) 

822 self.png.rewind() 

823 self.__prepare_idat = self.__rewind_idat 

824 self.im = None 

825 if self.pyaccess: 

826 self.pyaccess = None 

827 self.info = self.png.im_info 

828 self.tile = self.png.im_tile 

829 self.fp = self._fp 

830 self._prev_im = None 

831 self.dispose = None 

832 self.default_image = self.info.get("default_image", False) 

833 self.dispose_op = self.info.get("disposal") 

834 self.blend_op = self.info.get("blend") 

835 self.dispose_extent = self.info.get("bbox") 

836 self.__frame = 0 

837 else: 

838 if frame != self.__frame + 1: 

839 msg = f"cannot seek to frame {frame}" 

840 raise ValueError(msg) 

841 

842 # ensure previous frame was loaded 

843 self.load() 

844 

845 if self.dispose: 

846 self.im.paste(self.dispose, self.dispose_extent) 

847 self._prev_im = self.im.copy() 

848 

849 self.fp = self._fp 

850 

851 # advance to the next frame 

852 if self.__prepare_idat: 

853 ImageFile._safe_read(self.fp, self.__prepare_idat) 

854 self.__prepare_idat = 0 

855 frame_start = False 

856 while True: 

857 self.fp.read(4) # CRC 

858 

859 try: 

860 cid, pos, length = self.png.read() 

861 except (struct.error, SyntaxError): 

862 break 

863 

864 if cid == b"IEND": 

865 msg = "No more images in APNG file" 

866 raise EOFError(msg) 

867 if cid == b"fcTL": 

868 if frame_start: 

869 # there must be at least one fdAT chunk between fcTL chunks 

870 msg = "APNG missing frame data" 

871 raise SyntaxError(msg) 

872 frame_start = True 

873 

874 try: 

875 self.png.call(cid, pos, length) 

876 except UnicodeDecodeError: 

877 break 

878 except EOFError: 

879 if cid == b"fdAT": 

880 length -= 4 

881 if frame_start: 

882 self.__prepare_idat = length 

883 break 

884 ImageFile._safe_read(self.fp, length) 

885 except AttributeError: 

886 logger.debug("%r %s %s (unknown)", cid, pos, length) 

887 ImageFile._safe_read(self.fp, length) 

888 

889 self.__frame = frame 

890 self.tile = self.png.im_tile 

891 self.dispose_op = self.info.get("disposal") 

892 self.blend_op = self.info.get("blend") 

893 self.dispose_extent = self.info.get("bbox") 

894 

895 if not self.tile: 

896 msg = "image not found in APNG frame" 

897 raise EOFError(msg) 

898 

899 # setup frame disposal (actual disposal done when needed in the next _seek()) 

900 if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS: 

901 self.dispose_op = Disposal.OP_BACKGROUND 

902 

903 if self.dispose_op == Disposal.OP_PREVIOUS: 

904 self.dispose = self._prev_im.copy() 

905 self.dispose = self._crop(self.dispose, self.dispose_extent) 

906 elif self.dispose_op == Disposal.OP_BACKGROUND: 

907 self.dispose = Image.core.fill(self.mode, self.size) 

908 self.dispose = self._crop(self.dispose, self.dispose_extent) 

909 else: 

910 self.dispose = None 

911 

912 def tell(self): 

913 return self.__frame 

914 

915 def load_prepare(self): 

916 """internal: prepare to read PNG file""" 

917 

918 if self.info.get("interlace"): 

919 self.decoderconfig = self.decoderconfig + (1,) 

920 

921 self.__idat = self.__prepare_idat # used by load_read() 

922 ImageFile.ImageFile.load_prepare(self) 

923 

924 def load_read(self, read_bytes): 

925 """internal: read more image data""" 

926 

927 while self.__idat == 0: 

928 # end of chunk, skip forward to next one 

929 

930 self.fp.read(4) # CRC 

931 

932 cid, pos, length = self.png.read() 

933 

934 if cid not in [b"IDAT", b"DDAT", b"fdAT"]: 

935 self.png.push(cid, pos, length) 

936 return b"" 

937 

938 if cid == b"fdAT": 

939 try: 

940 self.png.call(cid, pos, length) 

941 except EOFError: 

942 pass 

943 self.__idat = length - 4 # sequence_num has already been read 

944 else: 

945 self.__idat = length # empty chunks are allowed 

946 

947 # read more data from this chunk 

948 if read_bytes <= 0: 

949 read_bytes = self.__idat 

950 else: 

951 read_bytes = min(read_bytes, self.__idat) 

952 

953 self.__idat = self.__idat - read_bytes 

954 

955 return self.fp.read(read_bytes) 

956 

957 def load_end(self): 

958 """internal: finished reading image data""" 

959 if self.__idat != 0: 

960 self.fp.read(self.__idat) 

961 while True: 

962 self.fp.read(4) # CRC 

963 

964 try: 

965 cid, pos, length = self.png.read() 

966 except (struct.error, SyntaxError): 

967 break 

968 

969 if cid == b"IEND": 

970 break 

971 elif cid == b"fcTL" and self.is_animated: 

972 # start of the next frame, stop reading 

973 self.__prepare_idat = 0 

974 self.png.push(cid, pos, length) 

975 break 

976 

977 try: 

978 self.png.call(cid, pos, length) 

979 except UnicodeDecodeError: 

980 break 

981 except EOFError: 

982 if cid == b"fdAT": 

983 length -= 4 

984 ImageFile._safe_read(self.fp, length) 

985 except AttributeError: 

986 logger.debug("%r %s %s (unknown)", cid, pos, length) 

987 s = ImageFile._safe_read(self.fp, length) 

988 if cid[1:2].islower(): 

989 self.private_chunks.append((cid, s, True)) 

990 self._text = self.png.im_text 

991 if not self.is_animated: 

992 self.png.close() 

993 self.png = None 

994 else: 

995 if self._prev_im and self.blend_op == Blend.OP_OVER: 

996 updated = self._crop(self.im, self.dispose_extent) 

997 if self.im.mode == "RGB" and "transparency" in self.info: 

998 mask = updated.convert_transparent( 

999 "RGBA", self.info["transparency"] 

1000 ) 

1001 else: 

1002 mask = updated.convert("RGBA") 

1003 self._prev_im.paste(updated, self.dispose_extent, mask) 

1004 self.im = self._prev_im 

1005 if self.pyaccess: 

1006 self.pyaccess = None 

1007 

1008 def _getexif(self): 

1009 if "exif" not in self.info: 

1010 self.load() 

1011 if "exif" not in self.info and "Raw profile type exif" not in self.info: 

1012 return None 

1013 return self.getexif()._get_merged_dict() 

1014 

1015 def getexif(self): 

1016 if "exif" not in self.info: 

1017 self.load() 

1018 

1019 return super().getexif() 

1020 

1021 def getxmp(self): 

1022 """ 

1023 Returns a dictionary containing the XMP tags. 

1024 Requires defusedxml to be installed. 

1025 

1026 :returns: XMP tags in a dictionary. 

1027 """ 

1028 return ( 

1029 self._getxmp(self.info["XML:com.adobe.xmp"]) 

1030 if "XML:com.adobe.xmp" in self.info 

1031 else {} 

1032 ) 

1033 

1034 

1035# -------------------------------------------------------------------- 

1036# PNG writer 

1037 

1038_OUTMODES = { 

1039 # supported PIL modes, and corresponding rawmodes/bits/color combinations 

1040 "1": ("1", b"\x01\x00"), 

1041 "L;1": ("L;1", b"\x01\x00"), 

1042 "L;2": ("L;2", b"\x02\x00"), 

1043 "L;4": ("L;4", b"\x04\x00"), 

1044 "L": ("L", b"\x08\x00"), 

1045 "LA": ("LA", b"\x08\x04"), 

1046 "I": ("I;16B", b"\x10\x00"), 

1047 "I;16": ("I;16B", b"\x10\x00"), 

1048 "I;16B": ("I;16B", b"\x10\x00"), 

1049 "P;1": ("P;1", b"\x01\x03"), 

1050 "P;2": ("P;2", b"\x02\x03"), 

1051 "P;4": ("P;4", b"\x04\x03"), 

1052 "P": ("P", b"\x08\x03"), 

1053 "RGB": ("RGB", b"\x08\x02"), 

1054 "RGBA": ("RGBA", b"\x08\x06"), 

1055} 

1056 

1057 

1058def putchunk(fp, cid, *data): 

1059 """Write a PNG chunk (including CRC field)""" 

1060 

1061 data = b"".join(data) 

1062 

1063 fp.write(o32(len(data)) + cid) 

1064 fp.write(data) 

1065 crc = _crc32(data, _crc32(cid)) 

1066 fp.write(o32(crc)) 

1067 

1068 

1069class _idat: 

1070 # wrap output from the encoder in IDAT chunks 

1071 

1072 def __init__(self, fp, chunk): 

1073 self.fp = fp 

1074 self.chunk = chunk 

1075 

1076 def write(self, data): 

1077 self.chunk(self.fp, b"IDAT", data) 

1078 

1079 

1080class _fdat: 

1081 # wrap encoder output in fdAT chunks 

1082 

1083 def __init__(self, fp, chunk, seq_num): 

1084 self.fp = fp 

1085 self.chunk = chunk 

1086 self.seq_num = seq_num 

1087 

1088 def write(self, data): 

1089 self.chunk(self.fp, b"fdAT", o32(self.seq_num), data) 

1090 self.seq_num += 1 

1091 

1092 

1093def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images): 

1094 duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) 

1095 loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) 

1096 disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) 

1097 blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE)) 

1098 

1099 if default_image: 

1100 chain = itertools.chain(append_images) 

1101 else: 

1102 chain = itertools.chain([im], append_images) 

1103 

1104 im_frames = [] 

1105 frame_count = 0 

1106 for im_seq in chain: 

1107 for im_frame in ImageSequence.Iterator(im_seq): 

1108 if im_frame.mode == rawmode: 

1109 im_frame = im_frame.copy() 

1110 else: 

1111 im_frame = im_frame.convert(rawmode) 

1112 encoderinfo = im.encoderinfo.copy() 

1113 if isinstance(duration, (list, tuple)): 

1114 encoderinfo["duration"] = duration[frame_count] 

1115 if isinstance(disposal, (list, tuple)): 

1116 encoderinfo["disposal"] = disposal[frame_count] 

1117 if isinstance(blend, (list, tuple)): 

1118 encoderinfo["blend"] = blend[frame_count] 

1119 frame_count += 1 

1120 

1121 if im_frames: 

1122 previous = im_frames[-1] 

1123 prev_disposal = previous["encoderinfo"].get("disposal") 

1124 prev_blend = previous["encoderinfo"].get("blend") 

1125 if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2: 

1126 prev_disposal = Disposal.OP_BACKGROUND 

1127 

1128 if prev_disposal == Disposal.OP_BACKGROUND: 

1129 base_im = previous["im"].copy() 

1130 dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0)) 

1131 bbox = previous["bbox"] 

1132 if bbox: 

1133 dispose = dispose.crop(bbox) 

1134 else: 

1135 bbox = (0, 0) + im.size 

1136 base_im.paste(dispose, bbox) 

1137 elif prev_disposal == Disposal.OP_PREVIOUS: 

1138 base_im = im_frames[-2]["im"] 

1139 else: 

1140 base_im = previous["im"] 

1141 delta = ImageChops.subtract_modulo( 

1142 im_frame.convert("RGBA"), base_im.convert("RGBA") 

1143 ) 

1144 bbox = delta.getbbox(alpha_only=False) 

1145 if ( 

1146 not bbox 

1147 and prev_disposal == encoderinfo.get("disposal") 

1148 and prev_blend == encoderinfo.get("blend") 

1149 ): 

1150 previous["encoderinfo"]["duration"] += encoderinfo.get( 

1151 "duration", duration 

1152 ) 

1153 continue 

1154 else: 

1155 bbox = None 

1156 if "duration" not in encoderinfo: 

1157 encoderinfo["duration"] = duration 

1158 im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) 

1159 

1160 if len(im_frames) == 1 and not default_image: 

1161 return im_frames[0]["im"] 

1162 

1163 # animation control 

1164 chunk( 

1165 fp, 

1166 b"acTL", 

1167 o32(len(im_frames)), # 0: num_frames 

1168 o32(loop), # 4: num_plays 

1169 ) 

1170 

1171 # default image IDAT (if it exists) 

1172 if default_image: 

1173 if im.mode != rawmode: 

1174 im = im.convert(rawmode) 

1175 ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) 

1176 

1177 seq_num = 0 

1178 for frame, frame_data in enumerate(im_frames): 

1179 im_frame = frame_data["im"] 

1180 if not frame_data["bbox"]: 

1181 bbox = (0, 0) + im_frame.size 

1182 else: 

1183 bbox = frame_data["bbox"] 

1184 im_frame = im_frame.crop(bbox) 

1185 size = im_frame.size 

1186 encoderinfo = frame_data["encoderinfo"] 

1187 frame_duration = int(round(encoderinfo["duration"])) 

1188 frame_disposal = encoderinfo.get("disposal", disposal) 

1189 frame_blend = encoderinfo.get("blend", blend) 

1190 # frame control 

1191 chunk( 

1192 fp, 

1193 b"fcTL", 

1194 o32(seq_num), # sequence_number 

1195 o32(size[0]), # width 

1196 o32(size[1]), # height 

1197 o32(bbox[0]), # x_offset 

1198 o32(bbox[1]), # y_offset 

1199 o16(frame_duration), # delay_numerator 

1200 o16(1000), # delay_denominator 

1201 o8(frame_disposal), # dispose_op 

1202 o8(frame_blend), # blend_op 

1203 ) 

1204 seq_num += 1 

1205 # frame data 

1206 if frame == 0 and not default_image: 

1207 # first frame must be in IDAT chunks for backwards compatibility 

1208 ImageFile._save( 

1209 im_frame, 

1210 _idat(fp, chunk), 

1211 [("zip", (0, 0) + im_frame.size, 0, rawmode)], 

1212 ) 

1213 else: 

1214 fdat_chunks = _fdat(fp, chunk, seq_num) 

1215 ImageFile._save( 

1216 im_frame, 

1217 fdat_chunks, 

1218 [("zip", (0, 0) + im_frame.size, 0, rawmode)], 

1219 ) 

1220 seq_num = fdat_chunks.seq_num 

1221 

1222 

1223def _save_all(im, fp, filename): 

1224 _save(im, fp, filename, save_all=True) 

1225 

1226 

1227def _save(im, fp, filename, chunk=putchunk, save_all=False): 

1228 # save an image to disk (called by the save method) 

1229 

1230 if save_all: 

1231 default_image = im.encoderinfo.get( 

1232 "default_image", im.info.get("default_image") 

1233 ) 

1234 modes = set() 

1235 append_images = im.encoderinfo.get("append_images", []) 

1236 for im_seq in itertools.chain([im], append_images): 

1237 for im_frame in ImageSequence.Iterator(im_seq): 

1238 modes.add(im_frame.mode) 

1239 for mode in ("RGBA", "RGB", "P"): 

1240 if mode in modes: 

1241 break 

1242 else: 

1243 mode = modes.pop() 

1244 else: 

1245 mode = im.mode 

1246 

1247 if mode == "P": 

1248 # 

1249 # attempt to minimize storage requirements for palette images 

1250 if "bits" in im.encoderinfo: 

1251 # number of bits specified by user 

1252 colors = min(1 << im.encoderinfo["bits"], 256) 

1253 else: 

1254 # check palette contents 

1255 if im.palette: 

1256 colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1) 

1257 else: 

1258 colors = 256 

1259 

1260 if colors <= 16: 

1261 if colors <= 2: 

1262 bits = 1 

1263 elif colors <= 4: 

1264 bits = 2 

1265 else: 

1266 bits = 4 

1267 mode = f"{mode};{bits}" 

1268 

1269 # encoder options 

1270 im.encoderconfig = ( 

1271 im.encoderinfo.get("optimize", False), 

1272 im.encoderinfo.get("compress_level", -1), 

1273 im.encoderinfo.get("compress_type", -1), 

1274 im.encoderinfo.get("dictionary", b""), 

1275 ) 

1276 

1277 # get the corresponding PNG mode 

1278 try: 

1279 rawmode, mode = _OUTMODES[mode] 

1280 except KeyError as e: 

1281 msg = f"cannot write mode {mode} as PNG" 

1282 raise OSError(msg) from e 

1283 

1284 # 

1285 # write minimal PNG file 

1286 

1287 fp.write(_MAGIC) 

1288 

1289 chunk( 

1290 fp, 

1291 b"IHDR", 

1292 o32(im.size[0]), # 0: size 

1293 o32(im.size[1]), 

1294 mode, # 8: depth/type 

1295 b"\0", # 10: compression 

1296 b"\0", # 11: filter category 

1297 b"\0", # 12: interlace flag 

1298 ) 

1299 

1300 chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"] 

1301 

1302 icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) 

1303 if icc: 

1304 # ICC profile 

1305 # according to PNG spec, the iCCP chunk contains: 

1306 # Profile name 1-79 bytes (character string) 

1307 # Null separator 1 byte (null character) 

1308 # Compression method 1 byte (0) 

1309 # Compressed profile n bytes (zlib with deflate compression) 

1310 name = b"ICC Profile" 

1311 data = name + b"\0\0" + zlib.compress(icc) 

1312 chunk(fp, b"iCCP", data) 

1313 

1314 # You must either have sRGB or iCCP. 

1315 # Disallow sRGB chunks when an iCCP-chunk has been emitted. 

1316 chunks.remove(b"sRGB") 

1317 

1318 info = im.encoderinfo.get("pnginfo") 

1319 if info: 

1320 chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"] 

1321 for info_chunk in info.chunks: 

1322 cid, data = info_chunk[:2] 

1323 if cid in chunks: 

1324 chunks.remove(cid) 

1325 chunk(fp, cid, data) 

1326 elif cid in chunks_multiple_allowed: 

1327 chunk(fp, cid, data) 

1328 elif cid[1:2].islower(): 

1329 # Private chunk 

1330 after_idat = info_chunk[2:3] 

1331 if not after_idat: 

1332 chunk(fp, cid, data) 

1333 

1334 if im.mode == "P": 

1335 palette_byte_number = colors * 3 

1336 palette_bytes = im.im.getpalette("RGB")[:palette_byte_number] 

1337 while len(palette_bytes) < palette_byte_number: 

1338 palette_bytes += b"\0" 

1339 chunk(fp, b"PLTE", palette_bytes) 

1340 

1341 transparency = im.encoderinfo.get("transparency", im.info.get("transparency", None)) 

1342 

1343 if transparency or transparency == 0: 

1344 if im.mode == "P": 

1345 # limit to actual palette size 

1346 alpha_bytes = colors 

1347 if isinstance(transparency, bytes): 

1348 chunk(fp, b"tRNS", transparency[:alpha_bytes]) 

1349 else: 

1350 transparency = max(0, min(255, transparency)) 

1351 alpha = b"\xFF" * transparency + b"\0" 

1352 chunk(fp, b"tRNS", alpha[:alpha_bytes]) 

1353 elif im.mode in ("1", "L", "I"): 

1354 transparency = max(0, min(65535, transparency)) 

1355 chunk(fp, b"tRNS", o16(transparency)) 

1356 elif im.mode == "RGB": 

1357 red, green, blue = transparency 

1358 chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue)) 

1359 else: 

1360 if "transparency" in im.encoderinfo: 

1361 # don't bother with transparency if it's an RGBA 

1362 # and it's in the info dict. It's probably just stale. 

1363 msg = "cannot use transparency for this mode" 

1364 raise OSError(msg) 

1365 else: 

1366 if im.mode == "P" and im.im.getpalettemode() == "RGBA": 

1367 alpha = im.im.getpalette("RGBA", "A") 

1368 alpha_bytes = colors 

1369 chunk(fp, b"tRNS", alpha[:alpha_bytes]) 

1370 

1371 dpi = im.encoderinfo.get("dpi") 

1372 if dpi: 

1373 chunk( 

1374 fp, 

1375 b"pHYs", 

1376 o32(int(dpi[0] / 0.0254 + 0.5)), 

1377 o32(int(dpi[1] / 0.0254 + 0.5)), 

1378 b"\x01", 

1379 ) 

1380 

1381 if info: 

1382 chunks = [b"bKGD", b"hIST"] 

1383 for info_chunk in info.chunks: 

1384 cid, data = info_chunk[:2] 

1385 if cid in chunks: 

1386 chunks.remove(cid) 

1387 chunk(fp, cid, data) 

1388 

1389 exif = im.encoderinfo.get("exif") 

1390 if exif: 

1391 if isinstance(exif, Image.Exif): 

1392 exif = exif.tobytes(8) 

1393 if exif.startswith(b"Exif\x00\x00"): 

1394 exif = exif[6:] 

1395 chunk(fp, b"eXIf", exif) 

1396 

1397 if save_all: 

1398 im = _write_multiple_frames( 

1399 im, fp, chunk, rawmode, default_image, append_images 

1400 ) 

1401 if im: 

1402 ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) 

1403 

1404 if info: 

1405 for info_chunk in info.chunks: 

1406 cid, data = info_chunk[:2] 

1407 if cid[1:2].islower(): 

1408 # Private chunk 

1409 after_idat = info_chunk[2:3] 

1410 if after_idat: 

1411 chunk(fp, cid, data) 

1412 

1413 chunk(fp, b"IEND", b"") 

1414 

1415 if hasattr(fp, "flush"): 

1416 fp.flush() 

1417 

1418 

1419# -------------------------------------------------------------------- 

1420# PNG chunk converter 

1421 

1422 

1423def getchunks(im, **params): 

1424 """Return a list of PNG chunks representing this image.""" 

1425 

1426 class collector: 

1427 data = [] 

1428 

1429 def write(self, data): 

1430 pass 

1431 

1432 def append(self, chunk): 

1433 self.data.append(chunk) 

1434 

1435 def append(fp, cid, *data): 

1436 data = b"".join(data) 

1437 crc = o32(_crc32(data, _crc32(cid))) 

1438 fp.append((cid, data, crc)) 

1439 

1440 fp = collector() 

1441 

1442 try: 

1443 im.encoderinfo = params 

1444 _save(im, fp, None, append) 

1445 finally: 

1446 del im.encoderinfo 

1447 

1448 return fp.data 

1449 

1450 

1451# -------------------------------------------------------------------- 

1452# Registry 

1453 

1454Image.register_open(PngImageFile.format, PngImageFile, _accept) 

1455Image.register_save(PngImageFile.format, _save) 

1456Image.register_save_all(PngImageFile.format, _save_all) 

1457 

1458Image.register_extensions(PngImageFile.format, [".png", ".apng"]) 

1459 

1460Image.register_mime(PngImageFile.format, "image/png")