Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pillow-10.4.0-py3.8-linux-x86_64.egg/PIL/PngImagePlugin.py: 12%

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

888 statements  

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 

42from typing import IO, TYPE_CHECKING, Any, NoReturn 

43 

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

45from ._binary import i16be as i16 

46from ._binary import i32be as i32 

47from ._binary import o8 

48from ._binary import o16be as o16 

49from ._binary import o32be as o32 

50 

51if TYPE_CHECKING: 

52 from . import _imaging 

53 

54logger = logging.getLogger(__name__) 

55 

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

57 

58 

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

60 

61 

62_MODES = { 

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

64 # Grayscale 

65 (1, 0): ("1", "1"), 

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

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

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

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

70 # Truecolour 

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

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

73 # Indexed-colour 

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

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

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

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

78 # Grayscale with alpha 

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

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

81 # Truecolour with alpha 

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

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

84} 

85 

86 

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

88 

89MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK 

90""" 

91Maximum decompressed size for a iTXt or zTXt chunk. 

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

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

94""" 

95MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK 

96""" 

97Set the maximum total text chunk size. 

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

99""" 

100 

101 

102# APNG frame disposal modes 

103class Disposal(IntEnum): 

104 OP_NONE = 0 

105 """ 

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

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

108 """ 

109 OP_BACKGROUND = 1 

110 """ 

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

112 the next frame. 

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

114 """ 

115 OP_PREVIOUS = 2 

116 """ 

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

118 rendering the next frame. 

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

120 """ 

121 

122 

123# APNG frame blend modes 

124class Blend(IntEnum): 

125 OP_SOURCE = 0 

126 """ 

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

128 image contents. 

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

130 """ 

131 OP_OVER = 1 

132 """ 

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

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

135 """ 

136 

137 

138def _safe_zlib_decompress(s): 

139 dobj = zlib.decompressobj() 

140 plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) 

141 if dobj.unconsumed_tail: 

142 msg = "Decompressed Data Too Large" 

143 raise ValueError(msg) 

144 return plaintext 

145 

146 

147def _crc32(data, seed=0): 

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

149 

150 

151# -------------------------------------------------------------------- 

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

153 

154 

155class ChunkStream: 

156 def __init__(self, fp: IO[bytes]) -> None: 

157 self.fp: IO[bytes] | None = fp 

158 self.queue: list[tuple[bytes, int, int]] | None = [] 

159 

160 def read(self) -> tuple[bytes, int, int]: 

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

162 cid = None 

163 

164 assert self.fp is not None 

165 if self.queue: 

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

167 self.fp.seek(pos) 

168 else: 

169 s = self.fp.read(8) 

170 cid = s[4:] 

171 pos = self.fp.tell() 

172 length = i32(s) 

173 

174 if not is_cid(cid): 

175 if not ImageFile.LOAD_TRUNCATED_IMAGES: 

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

177 raise SyntaxError(msg) 

178 

179 return cid, pos, length 

180 

181 def __enter__(self) -> ChunkStream: 

182 return self 

183 

184 def __exit__(self, *args: object) -> None: 

185 self.close() 

186 

187 def close(self) -> None: 

188 self.queue = self.fp = None 

189 

190 def push(self, cid: bytes, pos: int, length: int) -> None: 

191 assert self.queue is not None 

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

193 

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

195 """Call the appropriate chunk handler""" 

196 

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

198 return getattr(self, f"chunk_{cid.decode('ascii')}")(pos, length) 

199 

200 def crc(self, cid: bytes, data: bytes) -> None: 

201 """Read and verify checksum""" 

202 

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

204 # images 

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

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

207 self.crc_skip(cid, data) 

208 return 

209 

210 assert self.fp is not None 

211 try: 

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

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

214 if crc1 != crc2: 

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

216 raise SyntaxError(msg) 

217 except struct.error as e: 

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

219 raise SyntaxError(msg) from e 

220 

221 def crc_skip(self, cid: bytes, data: bytes) -> None: 

222 """Read checksum""" 

223 

224 assert self.fp is not None 

225 self.fp.read(4) 

226 

227 def verify(self, endchunk: bytes = b"IEND") -> list[bytes]: 

228 # Simple approach; just calculate checksum for all remaining 

229 # blocks. Must be called directly after open. 

230 

231 cids = [] 

232 

233 while True: 

234 try: 

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

236 except struct.error as e: 

237 msg = "truncated PNG file" 

238 raise OSError(msg) from e 

239 

240 if cid == endchunk: 

241 break 

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

243 cids.append(cid) 

244 

245 return cids 

246 

247 

248class iTXt(str): 

249 """ 

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

251 keeping their extra information 

252 

253 """ 

254 

255 lang: str | bytes | None 

256 tkey: str | bytes | None 

257 

258 @staticmethod 

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

260 """ 

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

262 :param text: value for this key 

263 :param lang: language code 

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

265 """ 

266 

267 self = str.__new__(cls, text) 

268 self.lang = lang 

269 self.tkey = tkey 

270 return self 

271 

272 

273class PngInfo: 

274 """ 

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

276 

277 """ 

278 

279 def __init__(self) -> None: 

280 self.chunks: list[tuple[bytes, bytes, bool]] = [] 

281 

282 def add(self, cid: bytes, data: bytes, after_idat: bool = False) -> None: 

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

284 

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

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

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

288 should be written after IDAT 

289 

290 """ 

291 

292 self.chunks.append((cid, data, after_idat)) 

293 

294 def add_itxt( 

295 self, 

296 key: str | bytes, 

297 value: str | bytes, 

298 lang: str | bytes = "", 

299 tkey: str | bytes = "", 

300 zip: bool = False, 

301 ) -> None: 

302 """Appends an iTXt chunk. 

303 

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

305 :param value: value for this key 

306 :param lang: language code 

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

308 :param zip: compression flag 

309 

310 """ 

311 

312 if not isinstance(key, bytes): 

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

314 if not isinstance(value, bytes): 

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

316 if not isinstance(lang, bytes): 

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

318 if not isinstance(tkey, bytes): 

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

320 

321 if zip: 

322 self.add( 

323 b"iTXt", 

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

325 ) 

326 else: 

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

328 

329 def add_text( 

330 self, key: str | bytes, value: str | bytes | iTXt, zip: bool = False 

331 ) -> None: 

332 """Appends a text chunk. 

333 

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

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

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

337 :param zip: compression flag 

338 

339 """ 

340 if isinstance(value, iTXt): 

341 return self.add_itxt( 

342 key, 

343 value, 

344 value.lang if value.lang is not None else b"", 

345 value.tkey if value.tkey is not None else b"", 

346 zip=zip, 

347 ) 

348 

349 # The tEXt chunk stores latin-1 text 

350 if not isinstance(value, bytes): 

351 try: 

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

353 except UnicodeError: 

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

355 

356 if not isinstance(key, bytes): 

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

358 

359 if zip: 

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

361 else: 

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

363 

364 

365# -------------------------------------------------------------------- 

366# PNG image stream (IHDR/IEND) 

367 

368 

369class PngStream(ChunkStream): 

370 def __init__(self, fp): 

371 super().__init__(fp) 

372 

373 # local copies of Image attributes 

374 self.im_info = {} 

375 self.im_text = {} 

376 self.im_size = (0, 0) 

377 self.im_mode = None 

378 self.im_tile = None 

379 self.im_palette = None 

380 self.im_custom_mimetype = None 

381 self.im_n_frames = None 

382 self._seq_num = None 

383 self.rewind_state = None 

384 

385 self.text_memory = 0 

386 

387 def check_text_memory(self, chunklen: int) -> None: 

388 self.text_memory += chunklen 

389 if self.text_memory > MAX_TEXT_MEMORY: 

390 msg = ( 

391 "Too much memory used in text chunks: " 

392 f"{self.text_memory}>MAX_TEXT_MEMORY" 

393 ) 

394 raise ValueError(msg) 

395 

396 def save_rewind(self) -> None: 

397 self.rewind_state = { 

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

399 "tile": self.im_tile, 

400 "seq_num": self._seq_num, 

401 } 

402 

403 def rewind(self) -> None: 

404 self.im_info = self.rewind_state["info"].copy() 

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

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

407 

408 def chunk_iCCP(self, pos: int, length: int) -> bytes: 

409 # ICC profile 

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

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

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

413 # Null separator 1 byte (null character) 

414 # Compression method 1 byte (0) 

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

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

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

418 comp_method = s[i + 1] 

419 logger.debug("Compression method %s", comp_method) 

420 if comp_method != 0: 

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

422 raise SyntaxError(msg) 

423 try: 

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

425 except ValueError: 

426 if ImageFile.LOAD_TRUNCATED_IMAGES: 

427 icc_profile = None 

428 else: 

429 raise 

430 except zlib.error: 

431 icc_profile = None # FIXME 

432 self.im_info["icc_profile"] = icc_profile 

433 return s 

434 

435 def chunk_IHDR(self, pos: int, length: int) -> bytes: 

436 # image header 

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

438 if length < 13: 

439 if ImageFile.LOAD_TRUNCATED_IMAGES: 

440 return s 

441 msg = "Truncated IHDR chunk" 

442 raise ValueError(msg) 

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

444 try: 

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

446 except Exception: 

447 pass 

448 if s[12]: 

449 self.im_info["interlace"] = 1 

450 if s[11]: 

451 msg = "unknown filter category" 

452 raise SyntaxError(msg) 

453 return s 

454 

455 def chunk_IDAT(self, pos: int, length: int) -> NoReturn: 

456 # image data 

457 if "bbox" in self.im_info: 

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

459 else: 

460 if self.im_n_frames is not None: 

461 self.im_info["default_image"] = True 

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

463 self.im_tile = tile 

464 self.im_idat = length 

465 msg = "image data found" 

466 raise EOFError(msg) 

467 

468 def chunk_IEND(self, pos: int, length: int) -> NoReturn: 

469 msg = "end of PNG image" 

470 raise EOFError(msg) 

471 

472 def chunk_PLTE(self, pos: int, length: int) -> bytes: 

473 # palette 

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

475 if self.im_mode == "P": 

476 self.im_palette = "RGB", s 

477 return s 

478 

479 def chunk_tRNS(self, pos: int, length: int) -> bytes: 

480 # transparency 

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

482 if self.im_mode == "P": 

483 if _simple_palette.match(s): 

484 # tRNS contains only one full-transparent entry, 

485 # other entries are full opaque 

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

487 if i >= 0: 

488 self.im_info["transparency"] = i 

489 else: 

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

491 # for each palette entry 

492 self.im_info["transparency"] = s 

493 elif self.im_mode in ("1", "L", "I;16"): 

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

495 elif self.im_mode == "RGB": 

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

497 return s 

498 

499 def chunk_gAMA(self, pos: int, length: int) -> bytes: 

500 # gamma setting 

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

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

503 return s 

504 

505 def chunk_cHRM(self, pos: int, length: int) -> bytes: 

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

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

508 

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

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

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

512 return s 

513 

514 def chunk_sRGB(self, pos: int, length: int) -> bytes: 

515 # srgb rendering intent, 1 byte 

516 # 0 perceptual 

517 # 1 relative colorimetric 

518 # 2 saturation 

519 # 3 absolute colorimetric 

520 

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

522 if length < 1: 

523 if ImageFile.LOAD_TRUNCATED_IMAGES: 

524 return s 

525 msg = "Truncated sRGB chunk" 

526 raise ValueError(msg) 

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

528 return s 

529 

530 def chunk_pHYs(self, pos: int, length: int) -> bytes: 

531 # pixels per unit 

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

533 if length < 9: 

534 if ImageFile.LOAD_TRUNCATED_IMAGES: 

535 return s 

536 msg = "Truncated pHYs chunk" 

537 raise ValueError(msg) 

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

539 unit = s[8] 

540 if unit == 1: # meter 

541 dpi = px * 0.0254, py * 0.0254 

542 self.im_info["dpi"] = dpi 

543 elif unit == 0: 

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

545 return s 

546 

547 def chunk_tEXt(self, pos: int, length: int) -> bytes: 

548 # text 

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

550 try: 

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

552 except ValueError: 

553 # fallback for broken tEXt tags 

554 k = s 

555 v = b"" 

556 if k: 

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

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

559 

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

561 self.im_text[k] = v_str 

562 self.check_text_memory(len(v_str)) 

563 

564 return s 

565 

566 def chunk_zTXt(self, pos: int, length: int) -> bytes: 

567 # compressed text 

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

569 try: 

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

571 except ValueError: 

572 k = s 

573 v = b"" 

574 if v: 

575 comp_method = v[0] 

576 else: 

577 comp_method = 0 

578 if comp_method != 0: 

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

580 raise SyntaxError(msg) 

581 try: 

582 v = _safe_zlib_decompress(v[1:]) 

583 except ValueError: 

584 if ImageFile.LOAD_TRUNCATED_IMAGES: 

585 v = b"" 

586 else: 

587 raise 

588 except zlib.error: 

589 v = b"" 

590 

591 if k: 

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

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

594 

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

596 self.check_text_memory(len(v)) 

597 

598 return s 

599 

600 def chunk_iTXt(self, pos: int, length: int) -> bytes: 

601 # international text 

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

603 try: 

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

605 except ValueError: 

606 return s 

607 if len(r) < 2: 

608 return s 

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

610 try: 

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

612 except ValueError: 

613 return s 

614 if cf != 0: 

615 if cm == 0: 

616 try: 

617 v = _safe_zlib_decompress(v) 

618 except ValueError: 

619 if ImageFile.LOAD_TRUNCATED_IMAGES: 

620 return s 

621 else: 

622 raise 

623 except zlib.error: 

624 return s 

625 else: 

626 return s 

627 if k == b"XML:com.adobe.xmp": 

628 self.im_info["xmp"] = v 

629 try: 

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

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

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

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

634 except UnicodeError: 

635 return s 

636 

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

638 self.check_text_memory(len(v)) 

639 

640 return s 

641 

642 def chunk_eXIf(self, pos: int, length: int) -> bytes: 

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

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

645 return s 

646 

647 # APNG chunks 

648 def chunk_acTL(self, pos: int, length: int) -> bytes: 

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

650 if length < 8: 

651 if ImageFile.LOAD_TRUNCATED_IMAGES: 

652 return s 

653 msg = "APNG contains truncated acTL chunk" 

654 raise ValueError(msg) 

655 if self.im_n_frames is not None: 

656 self.im_n_frames = None 

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

658 return s 

659 n_frames = i32(s) 

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

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

662 return s 

663 self.im_n_frames = n_frames 

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

665 self.im_custom_mimetype = "image/apng" 

666 return s 

667 

668 def chunk_fcTL(self, pos: int, length: int) -> bytes: 

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

670 if length < 26: 

671 if ImageFile.LOAD_TRUNCATED_IMAGES: 

672 return s 

673 msg = "APNG contains truncated fcTL chunk" 

674 raise ValueError(msg) 

675 seq = i32(s) 

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

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

678 ): 

679 msg = "APNG contains frame sequence errors" 

680 raise SyntaxError(msg) 

681 self._seq_num = seq 

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

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

684 im_w, im_h = self.im_size 

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

686 msg = "APNG contains invalid frames" 

687 raise SyntaxError(msg) 

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

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

690 if delay_den == 0: 

691 delay_den = 100 

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

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

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

695 return s 

696 

697 def chunk_fdAT(self, pos: int, length: int) -> bytes: 

698 if length < 4: 

699 if ImageFile.LOAD_TRUNCATED_IMAGES: 

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

701 return s 

702 msg = "APNG contains truncated fDAT chunk" 

703 raise ValueError(msg) 

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

705 seq = i32(s) 

706 if self._seq_num != seq - 1: 

707 msg = "APNG contains frame sequence errors" 

708 raise SyntaxError(msg) 

709 self._seq_num = seq 

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

711 

712 

713# -------------------------------------------------------------------- 

714# PNG reader 

715 

716 

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

718 return prefix[:8] == _MAGIC 

719 

720 

721## 

722# Image plugin for PNG images. 

723 

724 

725class PngImageFile(ImageFile.ImageFile): 

726 format = "PNG" 

727 format_description = "Portable network graphics" 

728 

729 def _open(self) -> None: 

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

731 msg = "not a PNG file" 

732 raise SyntaxError(msg) 

733 self._fp = self.fp 

734 self.__frame = 0 

735 

736 # 

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

738 

739 self.private_chunks: list[tuple[bytes, bytes] | tuple[bytes, bytes, bool]] = [] 

740 self.png: PngStream | None = PngStream(self.fp) 

741 

742 while True: 

743 # 

744 # get next chunk 

745 

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

747 

748 try: 

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

750 except EOFError: 

751 break 

752 except AttributeError: 

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

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

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

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

757 

758 self.png.crc(cid, s) 

759 

760 # 

761 # Copy relevant attributes from the PngStream. An alternative 

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

763 # directly, but that introduces circular references which are 

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

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

766 

767 self._mode = self.png.im_mode 

768 self._size = self.png.im_size 

769 self.info = self.png.im_info 

770 self._text = None 

771 self.tile = self.png.im_tile 

772 self.custom_mimetype = self.png.im_custom_mimetype 

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

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

775 

776 if self.png.im_palette: 

777 rawmode, data = self.png.im_palette 

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

779 

780 if cid == b"fdAT": 

781 self.__prepare_idat = length - 4 

782 else: 

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

784 

785 if self.png.im_n_frames is not None: 

786 self._close_exclusive_fp_after_loading = False 

787 self.png.save_rewind() 

788 self.__rewind_idat = self.__prepare_idat 

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

790 if self.default_image: 

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

792 self.n_frames += 1 

793 self._seek(0) 

794 self.is_animated = self.n_frames > 1 

795 

796 @property 

797 def text(self): 

798 # experimental 

799 if self._text is None: 

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

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

802 if self.is_animated: 

803 frame = self.__frame 

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

805 self.seek(self.n_frames - 1) 

806 self.load() 

807 if self.is_animated: 

808 self.seek(frame) 

809 return self._text 

810 

811 def verify(self) -> None: 

812 """Verify PNG file""" 

813 

814 if self.fp is None: 

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

816 raise RuntimeError(msg) 

817 

818 # back up to beginning of IDAT block 

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

820 

821 assert self.png is not None 

822 self.png.verify() 

823 self.png.close() 

824 

825 if self._exclusive_fp: 

826 self.fp.close() 

827 self.fp = None 

828 

829 def seek(self, frame: int) -> None: 

830 if not self._seek_check(frame): 

831 return 

832 if frame < self.__frame: 

833 self._seek(0, True) 

834 

835 last_frame = self.__frame 

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

837 try: 

838 self._seek(f) 

839 except EOFError as e: 

840 self.seek(last_frame) 

841 msg = "no more images in APNG file" 

842 raise EOFError(msg) from e 

843 

844 def _seek(self, frame: int, rewind: bool = False) -> None: 

845 assert self.png is not None 

846 

847 self.dispose: _imaging.ImagingCore | None 

848 if frame == 0: 

849 if rewind: 

850 self._fp.seek(self.__rewind) 

851 self.png.rewind() 

852 self.__prepare_idat = self.__rewind_idat 

853 self.im = None 

854 if self.pyaccess: 

855 self.pyaccess = None 

856 self.info = self.png.im_info 

857 self.tile = self.png.im_tile 

858 self.fp = self._fp 

859 self._prev_im = None 

860 self.dispose = None 

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

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

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

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

865 self.__frame = 0 

866 else: 

867 if frame != self.__frame + 1: 

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

869 raise ValueError(msg) 

870 

871 # ensure previous frame was loaded 

872 self.load() 

873 

874 if self.dispose: 

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

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

877 

878 self.fp = self._fp 

879 

880 # advance to the next frame 

881 if self.__prepare_idat: 

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

883 self.__prepare_idat = 0 

884 frame_start = False 

885 while True: 

886 self.fp.read(4) # CRC 

887 

888 try: 

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

890 except (struct.error, SyntaxError): 

891 break 

892 

893 if cid == b"IEND": 

894 msg = "No more images in APNG file" 

895 raise EOFError(msg) 

896 if cid == b"fcTL": 

897 if frame_start: 

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

899 msg = "APNG missing frame data" 

900 raise SyntaxError(msg) 

901 frame_start = True 

902 

903 try: 

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

905 except UnicodeDecodeError: 

906 break 

907 except EOFError: 

908 if cid == b"fdAT": 

909 length -= 4 

910 if frame_start: 

911 self.__prepare_idat = length 

912 break 

913 ImageFile._safe_read(self.fp, length) 

914 except AttributeError: 

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

916 ImageFile._safe_read(self.fp, length) 

917 

918 self.__frame = frame 

919 self.tile = self.png.im_tile 

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

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

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

923 

924 if not self.tile: 

925 msg = "image not found in APNG frame" 

926 raise EOFError(msg) 

927 

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

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

930 self.dispose_op = Disposal.OP_BACKGROUND 

931 

932 self.dispose = None 

933 if self.dispose_op == Disposal.OP_PREVIOUS: 

934 if self._prev_im: 

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

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

937 elif self.dispose_op == Disposal.OP_BACKGROUND: 

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

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

940 

941 def tell(self) -> int: 

942 return self.__frame 

943 

944 def load_prepare(self) -> None: 

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

946 

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

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

949 

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

951 ImageFile.ImageFile.load_prepare(self) 

952 

953 def load_read(self, read_bytes: int) -> bytes: 

954 """internal: read more image data""" 

955 

956 assert self.png is not None 

957 while self.__idat == 0: 

958 # end of chunk, skip forward to next one 

959 

960 self.fp.read(4) # CRC 

961 

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

963 

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

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

966 return b"" 

967 

968 if cid == b"fdAT": 

969 try: 

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

971 except EOFError: 

972 pass 

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

974 else: 

975 self.__idat = length # empty chunks are allowed 

976 

977 # read more data from this chunk 

978 if read_bytes <= 0: 

979 read_bytes = self.__idat 

980 else: 

981 read_bytes = min(read_bytes, self.__idat) 

982 

983 self.__idat = self.__idat - read_bytes 

984 

985 return self.fp.read(read_bytes) 

986 

987 def load_end(self) -> None: 

988 """internal: finished reading image data""" 

989 assert self.png is not None 

990 if self.__idat != 0: 

991 self.fp.read(self.__idat) 

992 while True: 

993 self.fp.read(4) # CRC 

994 

995 try: 

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

997 except (struct.error, SyntaxError): 

998 break 

999 

1000 if cid == b"IEND": 

1001 break 

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

1003 # start of the next frame, stop reading 

1004 self.__prepare_idat = 0 

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

1006 break 

1007 

1008 try: 

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

1010 except UnicodeDecodeError: 

1011 break 

1012 except EOFError: 

1013 if cid == b"fdAT": 

1014 length -= 4 

1015 try: 

1016 ImageFile._safe_read(self.fp, length) 

1017 except OSError as e: 

1018 if ImageFile.LOAD_TRUNCATED_IMAGES: 

1019 break 

1020 else: 

1021 raise e 

1022 except AttributeError: 

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

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

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

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

1027 self._text = self.png.im_text 

1028 if not self.is_animated: 

1029 self.png.close() 

1030 self.png = None 

1031 else: 

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

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

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

1035 mask = updated.convert_transparent( 

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

1037 ) 

1038 else: 

1039 mask = updated.convert("RGBA") 

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

1041 self.im = self._prev_im 

1042 if self.pyaccess: 

1043 self.pyaccess = None 

1044 

1045 def _getexif(self) -> dict[str, Any] | None: 

1046 if "exif" not in self.info: 

1047 self.load() 

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

1049 return None 

1050 return self.getexif()._get_merged_dict() 

1051 

1052 def getexif(self) -> Image.Exif: 

1053 if "exif" not in self.info: 

1054 self.load() 

1055 

1056 return super().getexif() 

1057 

1058 

1059# -------------------------------------------------------------------- 

1060# PNG writer 

1061 

1062_OUTMODES = { 

1063 # supported PIL modes, and corresponding rawmode, bit depth and color type 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1079} 

1080 

1081 

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

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

1084 

1085 data = b"".join(data) 

1086 

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

1088 fp.write(data) 

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

1090 fp.write(o32(crc)) 

1091 

1092 

1093class _idat: 

1094 # wrap output from the encoder in IDAT chunks 

1095 

1096 def __init__(self, fp, chunk): 

1097 self.fp = fp 

1098 self.chunk = chunk 

1099 

1100 def write(self, data: bytes) -> None: 

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

1102 

1103 

1104class _fdat: 

1105 # wrap encoder output in fdAT chunks 

1106 

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

1108 self.fp = fp 

1109 self.chunk = chunk 

1110 self.seq_num = seq_num 

1111 

1112 def write(self, data: bytes) -> None: 

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

1114 self.seq_num += 1 

1115 

1116 

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

1118 duration = im.encoderinfo.get("duration") 

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

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

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

1122 

1123 if default_image: 

1124 chain = itertools.chain(append_images) 

1125 else: 

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

1127 

1128 im_frames = [] 

1129 frame_count = 0 

1130 for im_seq in chain: 

1131 for im_frame in ImageSequence.Iterator(im_seq): 

1132 if im_frame.mode == mode: 

1133 im_frame = im_frame.copy() 

1134 else: 

1135 im_frame = im_frame.convert(mode) 

1136 encoderinfo = im.encoderinfo.copy() 

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

1138 encoderinfo["duration"] = duration[frame_count] 

1139 elif duration is None and "duration" in im_frame.info: 

1140 encoderinfo["duration"] = im_frame.info["duration"] 

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

1142 encoderinfo["disposal"] = disposal[frame_count] 

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

1144 encoderinfo["blend"] = blend[frame_count] 

1145 frame_count += 1 

1146 

1147 if im_frames: 

1148 previous = im_frames[-1] 

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

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

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

1152 prev_disposal = Disposal.OP_BACKGROUND 

1153 

1154 if prev_disposal == Disposal.OP_BACKGROUND: 

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

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

1157 bbox = previous["bbox"] 

1158 if bbox: 

1159 dispose = dispose.crop(bbox) 

1160 else: 

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

1162 base_im.paste(dispose, bbox) 

1163 elif prev_disposal == Disposal.OP_PREVIOUS: 

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

1165 else: 

1166 base_im = previous["im"] 

1167 delta = ImageChops.subtract_modulo( 

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

1169 ) 

1170 bbox = delta.getbbox(alpha_only=False) 

1171 if ( 

1172 not bbox 

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

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

1175 and "duration" in encoderinfo 

1176 ): 

1177 previous["encoderinfo"]["duration"] += encoderinfo["duration"] 

1178 continue 

1179 else: 

1180 bbox = None 

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

1182 

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

1184 return im_frames[0]["im"] 

1185 

1186 # animation control 

1187 chunk( 

1188 fp, 

1189 b"acTL", 

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

1191 o32(loop), # 4: num_plays 

1192 ) 

1193 

1194 # default image IDAT (if it exists) 

1195 if default_image: 

1196 if im.mode != mode: 

1197 im = im.convert(mode) 

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

1199 

1200 seq_num = 0 

1201 for frame, frame_data in enumerate(im_frames): 

1202 im_frame = frame_data["im"] 

1203 if not frame_data["bbox"]: 

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

1205 else: 

1206 bbox = frame_data["bbox"] 

1207 im_frame = im_frame.crop(bbox) 

1208 size = im_frame.size 

1209 encoderinfo = frame_data["encoderinfo"] 

1210 frame_duration = int(round(encoderinfo.get("duration", 0))) 

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

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

1213 # frame control 

1214 chunk( 

1215 fp, 

1216 b"fcTL", 

1217 o32(seq_num), # sequence_number 

1218 o32(size[0]), # width 

1219 o32(size[1]), # height 

1220 o32(bbox[0]), # x_offset 

1221 o32(bbox[1]), # y_offset 

1222 o16(frame_duration), # delay_numerator 

1223 o16(1000), # delay_denominator 

1224 o8(frame_disposal), # dispose_op 

1225 o8(frame_blend), # blend_op 

1226 ) 

1227 seq_num += 1 

1228 # frame data 

1229 if frame == 0 and not default_image: 

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

1231 ImageFile._save( 

1232 im_frame, 

1233 _idat(fp, chunk), 

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

1235 ) 

1236 else: 

1237 fdat_chunks = _fdat(fp, chunk, seq_num) 

1238 ImageFile._save( 

1239 im_frame, 

1240 fdat_chunks, 

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

1242 ) 

1243 seq_num = fdat_chunks.seq_num 

1244 

1245 

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

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

1248 

1249 

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

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

1252 

1253 if save_all: 

1254 default_image = im.encoderinfo.get( 

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

1256 ) 

1257 modes = set() 

1258 sizes = set() 

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

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

1261 for im_frame in ImageSequence.Iterator(im_seq): 

1262 modes.add(im_frame.mode) 

1263 sizes.add(im_frame.size) 

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

1265 if mode in modes: 

1266 break 

1267 else: 

1268 mode = modes.pop() 

1269 size = tuple(max(frame_size[i] for frame_size in sizes) for i in range(2)) 

1270 else: 

1271 size = im.size 

1272 mode = im.mode 

1273 

1274 outmode = mode 

1275 if mode == "P": 

1276 # 

1277 # attempt to minimize storage requirements for palette images 

1278 if "bits" in im.encoderinfo: 

1279 # number of bits specified by user 

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

1281 else: 

1282 # check palette contents 

1283 if im.palette: 

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

1285 else: 

1286 colors = 256 

1287 

1288 if colors <= 16: 

1289 if colors <= 2: 

1290 bits = 1 

1291 elif colors <= 4: 

1292 bits = 2 

1293 else: 

1294 bits = 4 

1295 outmode += f";{bits}" 

1296 

1297 # encoder options 

1298 im.encoderconfig = ( 

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

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

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

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

1303 ) 

1304 

1305 # get the corresponding PNG mode 

1306 try: 

1307 rawmode, bit_depth, color_type = _OUTMODES[outmode] 

1308 except KeyError as e: 

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

1310 raise OSError(msg) from e 

1311 

1312 # 

1313 # write minimal PNG file 

1314 

1315 fp.write(_MAGIC) 

1316 

1317 chunk( 

1318 fp, 

1319 b"IHDR", 

1320 o32(size[0]), # 0: size 

1321 o32(size[1]), 

1322 bit_depth, 

1323 color_type, 

1324 b"\0", # 10: compression 

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

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

1327 ) 

1328 

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

1330 

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

1332 if icc: 

1333 # ICC profile 

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

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

1336 # Null separator 1 byte (null character) 

1337 # Compression method 1 byte (0) 

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

1339 name = b"ICC Profile" 

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

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

1342 

1343 # You must either have sRGB or iCCP. 

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

1345 chunks.remove(b"sRGB") 

1346 

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

1348 if info: 

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

1350 for info_chunk in info.chunks: 

1351 cid, data = info_chunk[:2] 

1352 if cid in chunks: 

1353 chunks.remove(cid) 

1354 chunk(fp, cid, data) 

1355 elif cid in chunks_multiple_allowed: 

1356 chunk(fp, cid, data) 

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

1358 # Private chunk 

1359 after_idat = len(info_chunk) == 3 and info_chunk[2] 

1360 if not after_idat: 

1361 chunk(fp, cid, data) 

1362 

1363 if im.mode == "P": 

1364 palette_byte_number = colors * 3 

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

1366 while len(palette_bytes) < palette_byte_number: 

1367 palette_bytes += b"\0" 

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

1369 

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

1371 

1372 if transparency or transparency == 0: 

1373 if im.mode == "P": 

1374 # limit to actual palette size 

1375 alpha_bytes = colors 

1376 if isinstance(transparency, bytes): 

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

1378 else: 

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

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

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

1382 elif im.mode in ("1", "L", "I", "I;16"): 

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

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

1385 elif im.mode == "RGB": 

1386 red, green, blue = transparency 

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

1388 else: 

1389 if "transparency" in im.encoderinfo: 

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

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

1392 msg = "cannot use transparency for this mode" 

1393 raise OSError(msg) 

1394 else: 

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

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

1397 alpha_bytes = colors 

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

1399 

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

1401 if dpi: 

1402 chunk( 

1403 fp, 

1404 b"pHYs", 

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

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

1407 b"\x01", 

1408 ) 

1409 

1410 if info: 

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

1412 for info_chunk in info.chunks: 

1413 cid, data = info_chunk[:2] 

1414 if cid in chunks: 

1415 chunks.remove(cid) 

1416 chunk(fp, cid, data) 

1417 

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

1419 if exif: 

1420 if isinstance(exif, Image.Exif): 

1421 exif = exif.tobytes(8) 

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

1423 exif = exif[6:] 

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

1425 

1426 if save_all: 

1427 im = _write_multiple_frames( 

1428 im, fp, chunk, mode, rawmode, default_image, append_images 

1429 ) 

1430 if im: 

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

1432 

1433 if info: 

1434 for info_chunk in info.chunks: 

1435 cid, data = info_chunk[:2] 

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

1437 # Private chunk 

1438 after_idat = len(info_chunk) == 3 and info_chunk[2] 

1439 if after_idat: 

1440 chunk(fp, cid, data) 

1441 

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

1443 

1444 if hasattr(fp, "flush"): 

1445 fp.flush() 

1446 

1447 

1448# -------------------------------------------------------------------- 

1449# PNG chunk converter 

1450 

1451 

1452def getchunks(im, **params): 

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

1454 

1455 class collector: 

1456 data = [] 

1457 

1458 def write(self, data: bytes) -> None: 

1459 pass 

1460 

1461 def append(self, chunk: bytes) -> None: 

1462 self.data.append(chunk) 

1463 

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

1465 data = b"".join(data) 

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

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

1468 

1469 fp = collector() 

1470 

1471 try: 

1472 im.encoderinfo = params 

1473 _save(im, fp, None, append) 

1474 finally: 

1475 del im.encoderinfo 

1476 

1477 return fp.data 

1478 

1479 

1480# -------------------------------------------------------------------- 

1481# Registry 

1482 

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

1484Image.register_save(PngImageFile.format, _save) 

1485Image.register_save_all(PngImageFile.format, _save_all) 

1486 

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

1488 

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