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

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

946 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 fractions import Fraction 

43from typing import IO, NamedTuple, cast 

44 

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

46from ._binary import i16be as i16 

47from ._binary import i32be as i32 

48from ._binary import o8 

49from ._binary import o16be as o16 

50from ._binary import o32be as o32 

51from ._deprecate import deprecate 

52from ._util import DeferredError 

53 

54TYPE_CHECKING = False 

55if TYPE_CHECKING: 

56 from collections.abc import Callable 

57 from typing import Any, NoReturn 

58 

59 from . import _imaging 

60 

61logger = logging.getLogger(__name__) 

62 

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

64 

65 

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

67 

68 

69_MODES = { 

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

71 # Grayscale 

72 (1, 0): ("1", "1"), 

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

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

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

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

77 # Truecolour 

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

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

80 # Indexed-colour 

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

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

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

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

85 # Grayscale with alpha 

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

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

88 # Truecolour with alpha 

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

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

91} 

92 

93 

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

95 

96MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK 

97""" 

98Maximum decompressed size for a iTXt or zTXt chunk. 

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

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

101""" 

102MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK 

103""" 

104Set the maximum total text chunk size. 

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

106""" 

107 

108 

109# APNG frame disposal modes 

110class Disposal(IntEnum): 

111 OP_NONE = 0 

112 """ 

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

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

115 """ 

116 OP_BACKGROUND = 1 

117 """ 

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

119 the next frame. 

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

121 """ 

122 OP_PREVIOUS = 2 

123 """ 

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

125 rendering the next frame. 

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

127 """ 

128 

129 

130# APNG frame blend modes 

131class Blend(IntEnum): 

132 OP_SOURCE = 0 

133 """ 

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

135 image contents. 

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

137 """ 

138 OP_OVER = 1 

139 """ 

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

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

142 """ 

143 

144 

145def _safe_zlib_decompress(s: bytes) -> bytes: 

146 dobj = zlib.decompressobj() 

147 plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) 

148 if dobj.unconsumed_tail: 

149 msg = "Decompressed data too large for PngImagePlugin.MAX_TEXT_CHUNK" 

150 raise ValueError(msg) 

151 return plaintext 

152 

153 

154def _crc32(data: bytes, seed: int = 0) -> int: 

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

156 

157 

158# -------------------------------------------------------------------- 

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

160 

161 

162class ChunkStream: 

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

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

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

166 

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

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

169 cid = None 

170 

171 assert self.fp is not None 

172 if self.queue: 

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

174 self.fp.seek(pos) 

175 else: 

176 s = self.fp.read(8) 

177 cid = s[4:] 

178 pos = self.fp.tell() 

179 length = i32(s) 

180 

181 if not is_cid(cid): 

182 if not ImageFile.LOAD_TRUNCATED_IMAGES: 

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

184 raise SyntaxError(msg) 

185 

186 return cid, pos, length 

187 

188 def __enter__(self) -> ChunkStream: 

189 return self 

190 

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

192 self.close() 

193 

194 def close(self) -> None: 

195 self.queue = self.fp = None 

196 

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

198 assert self.queue is not None 

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

200 

201 def call(self, cid: bytes, pos: int, length: int) -> bytes: 

202 """Call the appropriate chunk handler""" 

203 

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

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

206 

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

208 """Read and verify checksum""" 

209 

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

211 # images 

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

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

214 self.crc_skip(cid, data) 

215 return 

216 

217 assert self.fp is not None 

218 try: 

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

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

221 if crc1 != crc2: 

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

223 raise SyntaxError(msg) 

224 except struct.error as e: 

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

226 raise SyntaxError(msg) from e 

227 

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

229 """Read checksum""" 

230 

231 assert self.fp is not None 

232 self.fp.read(4) 

233 

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

235 # Simple approach; just calculate checksum for all remaining 

236 # blocks. Must be called directly after open. 

237 

238 cids = [] 

239 

240 assert self.fp is not None 

241 while True: 

242 try: 

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

244 except struct.error as e: 

245 msg = "truncated PNG file" 

246 raise OSError(msg) from e 

247 

248 if cid == endchunk: 

249 break 

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

251 cids.append(cid) 

252 

253 return cids 

254 

255 

256class iTXt(str): 

257 """ 

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

259 keeping their extra information 

260 

261 """ 

262 

263 lang: str | bytes | None 

264 tkey: str | bytes | None 

265 

266 @staticmethod 

267 def __new__( 

268 cls, text: str, lang: str | None = None, tkey: str | None = None 

269 ) -> iTXt: 

270 """ 

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

272 :param text: value for this key 

273 :param lang: language code 

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

275 """ 

276 

277 self = str.__new__(cls, text) 

278 self.lang = lang 

279 self.tkey = tkey 

280 return self 

281 

282 

283class PngInfo: 

284 """ 

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

286 

287 """ 

288 

289 def __init__(self) -> None: 

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

291 

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

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

294 

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

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

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

298 should be written after IDAT 

299 

300 """ 

301 

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

303 

304 def add_itxt( 

305 self, 

306 key: str | bytes, 

307 value: str | bytes, 

308 lang: str | bytes = "", 

309 tkey: str | bytes = "", 

310 zip: bool = False, 

311 ) -> None: 

312 """Appends an iTXt chunk. 

313 

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

315 :param value: value for this key 

316 :param lang: language code 

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

318 :param zip: compression flag 

319 

320 """ 

321 

322 if not isinstance(key, bytes): 

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

324 if not isinstance(value, bytes): 

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

326 if not isinstance(lang, bytes): 

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

328 if not isinstance(tkey, bytes): 

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

330 

331 if zip: 

332 self.add( 

333 b"iTXt", 

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

335 ) 

336 else: 

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

338 

339 def add_text( 

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

341 ) -> None: 

342 """Appends a text chunk. 

343 

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

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

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

347 :param zip: compression flag 

348 

349 """ 

350 if isinstance(value, iTXt): 

351 return self.add_itxt( 

352 key, 

353 value, 

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

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

356 zip=zip, 

357 ) 

358 

359 # The tEXt chunk stores latin-1 text 

360 if not isinstance(value, bytes): 

361 try: 

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

363 except UnicodeError: 

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

365 

366 if not isinstance(key, bytes): 

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

368 

369 if zip: 

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

371 else: 

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

373 

374 

375# -------------------------------------------------------------------- 

376# PNG image stream (IHDR/IEND) 

377 

378 

379class _RewindState(NamedTuple): 

380 info: dict[str | tuple[int, int], Any] 

381 tile: list[ImageFile._Tile] 

382 seq_num: int | None 

383 

384 

385class PngStream(ChunkStream): 

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

387 super().__init__(fp) 

388 

389 # local copies of Image attributes 

390 self.im_info: dict[str | tuple[int, int], Any] = {} 

391 self.im_text: dict[str, str | iTXt] = {} 

392 self.im_size = (0, 0) 

393 self.im_mode = "" 

394 self.im_tile: list[ImageFile._Tile] = [] 

395 self.im_palette: tuple[str, bytes] | None = None 

396 self.im_custom_mimetype: str | None = None 

397 self.im_n_frames: int | None = None 

398 self._seq_num: int | None = None 

399 self.rewind_state = _RewindState({}, [], None) 

400 

401 self.text_memory = 0 

402 

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

404 self.text_memory += chunklen 

405 if self.text_memory > MAX_TEXT_MEMORY: 

406 msg = ( 

407 "Too much memory used in text chunks: " 

408 f"{self.text_memory}>MAX_TEXT_MEMORY" 

409 ) 

410 raise ValueError(msg) 

411 

412 def save_rewind(self) -> None: 

413 self.rewind_state = _RewindState( 

414 self.im_info.copy(), 

415 self.im_tile, 

416 self._seq_num, 

417 ) 

418 

419 def rewind(self) -> None: 

420 self.im_info = self.rewind_state.info.copy() 

421 self.im_tile = self.rewind_state.tile 

422 self._seq_num = self.rewind_state.seq_num 

423 

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

425 # ICC profile 

426 assert self.fp is not None 

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

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

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

430 # Null separator 1 byte (null character) 

431 # Compression method 1 byte (0) 

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

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

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

435 comp_method = s[i + 1] 

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

437 if comp_method != 0: 

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

439 raise SyntaxError(msg) 

440 try: 

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

442 except ValueError: 

443 if ImageFile.LOAD_TRUNCATED_IMAGES: 

444 icc_profile = None 

445 else: 

446 raise 

447 except zlib.error: 

448 icc_profile = None # FIXME 

449 self.im_info["icc_profile"] = icc_profile 

450 return s 

451 

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

453 # image header 

454 assert self.fp is not None 

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

456 if length < 13: 

457 if ImageFile.LOAD_TRUNCATED_IMAGES: 

458 return s 

459 msg = "Truncated IHDR chunk" 

460 raise ValueError(msg) 

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

462 try: 

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

464 except Exception: 

465 pass 

466 if s[12]: 

467 self.im_info["interlace"] = 1 

468 if s[11]: 

469 msg = "unknown filter category" 

470 raise SyntaxError(msg) 

471 return s 

472 

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

474 # image data 

475 if "bbox" in self.im_info: 

476 tile = [ImageFile._Tile("zip", self.im_info["bbox"], pos, self.im_rawmode)] 

477 else: 

478 if self.im_n_frames is not None: 

479 self.im_info["default_image"] = True 

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

481 self.im_tile = tile 

482 self.im_idat = length 

483 msg = "image data found" 

484 raise EOFError(msg) 

485 

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

487 msg = "end of PNG image" 

488 raise EOFError(msg) 

489 

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

491 # palette 

492 assert self.fp is not None 

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

494 if self.im_mode == "P": 

495 self.im_palette = "RGB", s 

496 return s 

497 

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

499 # transparency 

500 assert self.fp is not None 

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

502 if self.im_mode == "P": 

503 if _simple_palette.match(s): 

504 # tRNS contains only one full-transparent entry, 

505 # other entries are full opaque 

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

507 if i >= 0: 

508 self.im_info["transparency"] = i 

509 else: 

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

511 # for each palette entry 

512 self.im_info["transparency"] = s 

513 elif self.im_mode == "1": 

514 self.im_info["transparency"] = 255 if i16(s) else 0 

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

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

517 elif self.im_mode == "RGB": 

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

519 return s 

520 

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

522 # gamma setting 

523 assert self.fp is not None 

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

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

526 return s 

527 

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

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

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

531 

532 assert self.fp is not None 

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

534 raw_vals = struct.unpack(f">{len(s) // 4}I", s) 

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

536 return s 

537 

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

539 # srgb rendering intent, 1 byte 

540 # 0 perceptual 

541 # 1 relative colorimetric 

542 # 2 saturation 

543 # 3 absolute colorimetric 

544 

545 assert self.fp is not None 

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

547 if length < 1: 

548 if ImageFile.LOAD_TRUNCATED_IMAGES: 

549 return s 

550 msg = "Truncated sRGB chunk" 

551 raise ValueError(msg) 

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

553 return s 

554 

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

556 # pixels per unit 

557 assert self.fp is not None 

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

559 if length < 9: 

560 if ImageFile.LOAD_TRUNCATED_IMAGES: 

561 return s 

562 msg = "Truncated pHYs chunk" 

563 raise ValueError(msg) 

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

565 unit = s[8] 

566 if unit == 1: # meter 

567 dpi = px * 0.0254, py * 0.0254 

568 self.im_info["dpi"] = dpi 

569 elif unit == 0: 

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

571 return s 

572 

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

574 # text 

575 assert self.fp is not None 

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

577 try: 

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

579 except ValueError: 

580 # fallback for broken tEXt tags 

581 k = s 

582 v = b"" 

583 if k: 

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

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

586 

587 self.im_info[k_str] = v if k == b"exif" else v_str 

588 self.im_text[k_str] = v_str 

589 self.check_text_memory(len(v_str)) 

590 

591 return s 

592 

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

594 # compressed text 

595 assert self.fp is not None 

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

597 try: 

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

599 except ValueError: 

600 k = s 

601 v = b"" 

602 if v: 

603 comp_method = v[0] 

604 else: 

605 comp_method = 0 

606 if comp_method != 0: 

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

608 raise SyntaxError(msg) 

609 try: 

610 v = _safe_zlib_decompress(v[1:]) 

611 except ValueError: 

612 if ImageFile.LOAD_TRUNCATED_IMAGES: 

613 v = b"" 

614 else: 

615 raise 

616 except zlib.error: 

617 v = b"" 

618 

619 if k: 

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

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

622 

623 self.im_info[k_str] = self.im_text[k_str] = v_str 

624 self.check_text_memory(len(v_str)) 

625 

626 return s 

627 

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

629 # international text 

630 assert self.fp is not None 

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

632 try: 

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

634 except ValueError: 

635 return s 

636 if len(r) < 2: 

637 return s 

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

639 try: 

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

641 except ValueError: 

642 return s 

643 if cf != 0: 

644 if cm == 0: 

645 try: 

646 v = _safe_zlib_decompress(v) 

647 except ValueError: 

648 if ImageFile.LOAD_TRUNCATED_IMAGES: 

649 return s 

650 else: 

651 raise 

652 except zlib.error: 

653 return s 

654 else: 

655 return s 

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

657 self.im_info["xmp"] = v 

658 try: 

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

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

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

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

663 except UnicodeError: 

664 return s 

665 

666 self.im_info[k_str] = self.im_text[k_str] = iTXt(v_str, lang_str, tk_str) 

667 self.check_text_memory(len(v_str)) 

668 

669 return s 

670 

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

672 assert self.fp is not None 

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

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

675 return s 

676 

677 # APNG chunks 

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

679 assert self.fp is not None 

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

681 if length < 8: 

682 if ImageFile.LOAD_TRUNCATED_IMAGES: 

683 return s 

684 msg = "APNG contains truncated acTL chunk" 

685 raise ValueError(msg) 

686 if self.im_n_frames is not None: 

687 self.im_n_frames = None 

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

689 return s 

690 n_frames = i32(s) 

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

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

693 return s 

694 self.im_n_frames = n_frames 

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

696 self.im_custom_mimetype = "image/apng" 

697 return s 

698 

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

700 assert self.fp is not None 

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

702 if length < 26: 

703 if ImageFile.LOAD_TRUNCATED_IMAGES: 

704 return s 

705 msg = "APNG contains truncated fcTL chunk" 

706 raise ValueError(msg) 

707 seq = i32(s) 

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

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

710 ): 

711 msg = "APNG contains frame sequence errors" 

712 raise SyntaxError(msg) 

713 self._seq_num = seq 

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

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

716 im_w, im_h = self.im_size 

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

718 msg = "APNG contains invalid frames" 

719 raise SyntaxError(msg) 

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

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

722 if delay_den == 0: 

723 delay_den = 100 

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

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

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

727 return s 

728 

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

730 assert self.fp is not None 

731 if length < 4: 

732 if ImageFile.LOAD_TRUNCATED_IMAGES: 

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

734 return s 

735 msg = "APNG contains truncated fDAT chunk" 

736 raise ValueError(msg) 

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

738 seq = i32(s) 

739 if self._seq_num != seq - 1: 

740 msg = "APNG contains frame sequence errors" 

741 raise SyntaxError(msg) 

742 self._seq_num = seq 

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

744 

745 

746# -------------------------------------------------------------------- 

747# PNG reader 

748 

749 

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

751 return prefix.startswith(_MAGIC) 

752 

753 

754## 

755# Image plugin for PNG images. 

756 

757 

758class PngImageFile(ImageFile.ImageFile): 

759 format = "PNG" 

760 format_description = "Portable network graphics" 

761 

762 def _open(self) -> None: 

763 assert self.fp is not None 

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

765 msg = "not a PNG file" 

766 raise SyntaxError(msg) 

767 self._fp = self.fp 

768 self.__frame = 0 

769 

770 # 

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

772 

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

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

775 

776 while True: 

777 # 

778 # get next chunk 

779 

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

781 

782 try: 

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

784 except EOFError: 

785 break 

786 except AttributeError: 

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

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

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

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

791 

792 self.png.crc(cid, s) 

793 

794 # 

795 # Copy relevant attributes from the PngStream. An alternative 

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

797 # directly, but that introduces circular references which are 

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

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

800 

801 self._mode = self.png.im_mode 

802 self._size = self.png.im_size 

803 self.info = self.png.im_info 

804 self._text: dict[str, str | iTXt] | None = None 

805 self.tile = self.png.im_tile 

806 self.custom_mimetype = self.png.im_custom_mimetype 

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

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

809 

810 if self.png.im_palette: 

811 rawmode, data = self.png.im_palette 

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

813 

814 if cid == b"fdAT": 

815 self.__prepare_idat = length - 4 

816 else: 

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

818 

819 if self.png.im_n_frames is not None: 

820 self._close_exclusive_fp_after_loading = False 

821 self.png.save_rewind() 

822 self.__rewind_idat = self.__prepare_idat 

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

824 if self.default_image: 

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

826 self.n_frames += 1 

827 self._seek(0) 

828 self.is_animated = self.n_frames > 1 

829 

830 @property 

831 def text(self) -> dict[str, str | iTXt]: 

832 # experimental 

833 if self._text is None: 

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

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

836 if self.is_animated: 

837 frame = self.__frame 

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

839 self.seek(self.n_frames - 1) 

840 self.load() 

841 if self.is_animated: 

842 self.seek(frame) 

843 assert self._text is not None 

844 return self._text 

845 

846 def verify(self) -> None: 

847 """Verify PNG file""" 

848 

849 if self.fp is None: 

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

851 raise RuntimeError(msg) 

852 

853 # back up to beginning of IDAT block 

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

855 

856 assert self.png is not None 

857 self.png.verify() 

858 self.png.close() 

859 

860 super().verify() 

861 

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

863 if not self._seek_check(frame): 

864 return 

865 if frame < self.__frame: 

866 self._seek(0, True) 

867 

868 last_frame = self.__frame 

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

870 try: 

871 self._seek(f) 

872 except EOFError as e: 

873 self.seek(last_frame) 

874 msg = "no more images in APNG file" 

875 raise EOFError(msg) from e 

876 

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

878 assert self.png is not None 

879 if isinstance(self._fp, DeferredError): 

880 raise self._fp.ex 

881 

882 self.dispose: _imaging.ImagingCore | None 

883 dispose_extent = None 

884 if frame == 0: 

885 if rewind: 

886 self._fp.seek(self.__rewind) 

887 self.png.rewind() 

888 self.__prepare_idat = self.__rewind_idat 

889 self._im = None 

890 self.info = self.png.im_info 

891 self.tile = self.png.im_tile 

892 self.fp = self._fp 

893 self._prev_im = None 

894 self.dispose = None 

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

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

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

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

899 self.__frame = 0 

900 else: 

901 if frame != self.__frame + 1: 

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

903 raise ValueError(msg) 

904 

905 # ensure previous frame was loaded 

906 self.load() 

907 

908 if self.dispose: 

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

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

911 

912 self.fp = self._fp 

913 

914 # advance to the next frame 

915 if self.__prepare_idat: 

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

917 self.__prepare_idat = 0 

918 frame_start = False 

919 while True: 

920 self.fp.read(4) # CRC 

921 

922 try: 

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

924 except (struct.error, SyntaxError): 

925 break 

926 

927 if cid == b"IEND": 

928 msg = "No more images in APNG file" 

929 raise EOFError(msg) 

930 if cid == b"fcTL": 

931 if frame_start: 

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

933 msg = "APNG missing frame data" 

934 raise SyntaxError(msg) 

935 frame_start = True 

936 

937 try: 

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

939 except UnicodeDecodeError: 

940 break 

941 except EOFError: 

942 if cid == b"fdAT": 

943 length -= 4 

944 if frame_start: 

945 self.__prepare_idat = length 

946 break 

947 ImageFile._safe_read(self.fp, length) 

948 except AttributeError: 

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

950 ImageFile._safe_read(self.fp, length) 

951 

952 self.__frame = frame 

953 self.tile = self.png.im_tile 

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

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

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

957 

958 if not self.tile: 

959 msg = "image not found in APNG frame" 

960 raise EOFError(msg) 

961 if dispose_extent: 

962 self.dispose_extent: tuple[float, float, float, float] = dispose_extent 

963 

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

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

966 self.dispose_op = Disposal.OP_BACKGROUND 

967 

968 self.dispose = None 

969 if self.dispose_op == Disposal.OP_PREVIOUS: 

970 if self._prev_im: 

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

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

973 elif self.dispose_op == Disposal.OP_BACKGROUND: 

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

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

976 

977 def tell(self) -> int: 

978 return self.__frame 

979 

980 def load_prepare(self) -> None: 

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

982 

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

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

985 

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

987 ImageFile.ImageFile.load_prepare(self) 

988 

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

990 """internal: read more image data""" 

991 

992 assert self.png is not None 

993 assert self.fp is not None 

994 while self.__idat == 0: 

995 # end of chunk, skip forward to next one 

996 

997 self.fp.read(4) # CRC 

998 

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

1000 

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

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

1003 return b"" 

1004 

1005 if cid == b"fdAT": 

1006 try: 

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

1008 except EOFError: 

1009 pass 

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

1011 else: 

1012 self.__idat = length # empty chunks are allowed 

1013 

1014 # read more data from this chunk 

1015 if read_bytes <= 0: 

1016 read_bytes = self.__idat 

1017 else: 

1018 read_bytes = min(read_bytes, self.__idat) 

1019 

1020 self.__idat = self.__idat - read_bytes 

1021 

1022 return self.fp.read(read_bytes) 

1023 

1024 def load_end(self) -> None: 

1025 """internal: finished reading image data""" 

1026 assert self.png is not None 

1027 assert self.fp is not None 

1028 if self.__idat != 0: 

1029 self.fp.read(self.__idat) 

1030 while True: 

1031 self.fp.read(4) # CRC 

1032 

1033 try: 

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

1035 except (struct.error, SyntaxError): 

1036 break 

1037 

1038 if cid == b"IEND": 

1039 break 

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

1041 # start of the next frame, stop reading 

1042 self.__prepare_idat = 0 

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

1044 break 

1045 

1046 try: 

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

1048 except UnicodeDecodeError: 

1049 break 

1050 except EOFError: 

1051 if cid == b"fdAT": 

1052 length -= 4 

1053 try: 

1054 ImageFile._safe_read(self.fp, length) 

1055 except OSError as e: 

1056 if ImageFile.LOAD_TRUNCATED_IMAGES: 

1057 break 

1058 else: 

1059 raise e 

1060 except AttributeError: 

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

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

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

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

1065 self._text = self.png.im_text 

1066 if not self.is_animated: 

1067 self.png.close() 

1068 self.png = None 

1069 else: 

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

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

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

1073 mask = updated.convert_transparent( 

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

1075 ) 

1076 else: 

1077 if self.im.mode == "P" and "transparency" in self.info: 

1078 t = self.info["transparency"] 

1079 if isinstance(t, bytes): 

1080 updated.putpalettealphas(t) 

1081 elif isinstance(t, int): 

1082 updated.putpalettealpha(t) 

1083 mask = updated.convert("RGBA") 

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

1085 self.im = self._prev_im 

1086 

1087 def _getexif(self) -> dict[int, Any] | None: 

1088 if "exif" not in self.info: 

1089 self.load() 

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

1091 return None 

1092 return self.getexif()._get_merged_dict() 

1093 

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

1095 if "exif" not in self.info: 

1096 self.load() 

1097 

1098 return super().getexif() 

1099 

1100 

1101# -------------------------------------------------------------------- 

1102# PNG writer 

1103 

1104_OUTMODES = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1121} 

1122 

1123 

1124def putchunk(fp: IO[bytes], cid: bytes, *data: bytes) -> None: 

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

1126 

1127 byte_data = b"".join(data) 

1128 

1129 fp.write(o32(len(byte_data)) + cid) 

1130 fp.write(byte_data) 

1131 crc = _crc32(byte_data, _crc32(cid)) 

1132 fp.write(o32(crc)) 

1133 

1134 

1135class _idat: 

1136 # wrap output from the encoder in IDAT chunks 

1137 

1138 def __init__(self, fp: IO[bytes], chunk: Callable[..., None]) -> None: 

1139 self.fp = fp 

1140 self.chunk = chunk 

1141 

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

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

1144 

1145 

1146class _fdat: 

1147 # wrap encoder output in fdAT chunks 

1148 

1149 def __init__(self, fp: IO[bytes], chunk: Callable[..., None], seq_num: int) -> None: 

1150 self.fp = fp 

1151 self.chunk = chunk 

1152 self.seq_num = seq_num 

1153 

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

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

1156 self.seq_num += 1 

1157 

1158 

1159def _apply_encoderinfo(im: Image.Image, encoderinfo: dict[str, Any]) -> None: 

1160 im.encoderconfig = ( 

1161 encoderinfo.get("optimize", False), 

1162 encoderinfo.get("compress_level", -1), 

1163 encoderinfo.get("compress_type", -1), 

1164 encoderinfo.get("dictionary", b""), 

1165 ) 

1166 

1167 

1168class _Frame(NamedTuple): 

1169 im: Image.Image 

1170 bbox: tuple[int, int, int, int] | None 

1171 encoderinfo: dict[str, Any] 

1172 

1173 

1174def _write_multiple_frames( 

1175 im: Image.Image, 

1176 fp: IO[bytes], 

1177 chunk: Callable[..., None], 

1178 mode: str, 

1179 rawmode: str, 

1180 default_image: Image.Image | None, 

1181 append_images: list[Image.Image], 

1182) -> Image.Image | None: 

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

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

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

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

1187 

1188 if default_image: 

1189 chain = itertools.chain(append_images) 

1190 else: 

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

1192 

1193 im_frames: list[_Frame] = [] 

1194 frame_count = 0 

1195 for im_seq in chain: 

1196 for im_frame in ImageSequence.Iterator(im_seq): 

1197 if im_frame.mode == mode: 

1198 im_frame = im_frame.copy() 

1199 else: 

1200 im_frame = im_frame.convert(mode) 

1201 encoderinfo = im.encoderinfo.copy() 

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

1203 encoderinfo["duration"] = duration[frame_count] 

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

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

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

1207 encoderinfo["disposal"] = disposal[frame_count] 

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

1209 encoderinfo["blend"] = blend[frame_count] 

1210 frame_count += 1 

1211 

1212 if im_frames: 

1213 previous = im_frames[-1] 

1214 prev_disposal = previous.encoderinfo.get("disposal") 

1215 prev_blend = previous.encoderinfo.get("blend") 

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

1217 prev_disposal = Disposal.OP_BACKGROUND 

1218 

1219 if prev_disposal == Disposal.OP_BACKGROUND: 

1220 base_im = previous.im.copy() 

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

1222 bbox = previous.bbox 

1223 if bbox: 

1224 dispose = dispose.crop(bbox) 

1225 else: 

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

1227 base_im.paste(dispose, bbox) 

1228 elif prev_disposal == Disposal.OP_PREVIOUS: 

1229 base_im = im_frames[-2].im 

1230 else: 

1231 base_im = previous.im 

1232 delta = ImageChops.subtract_modulo( 

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

1234 ) 

1235 bbox = delta.getbbox(alpha_only=False) 

1236 if ( 

1237 not bbox 

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

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

1240 and "duration" in encoderinfo 

1241 ): 

1242 previous.encoderinfo["duration"] += encoderinfo["duration"] 

1243 continue 

1244 else: 

1245 bbox = None 

1246 im_frames.append(_Frame(im_frame, bbox, encoderinfo)) 

1247 

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

1249 return im_frames[0].im 

1250 

1251 # animation control 

1252 chunk( 

1253 fp, 

1254 b"acTL", 

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

1256 o32(loop), # 4: num_plays 

1257 ) 

1258 

1259 # default image IDAT (if it exists) 

1260 if default_image: 

1261 default_im = im if im.mode == mode else im.convert(mode) 

1262 _apply_encoderinfo(default_im, im.encoderinfo) 

1263 ImageFile._save( 

1264 default_im, 

1265 cast(IO[bytes], _idat(fp, chunk)), 

1266 [ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)], 

1267 ) 

1268 

1269 seq_num = 0 

1270 for frame, frame_data in enumerate(im_frames): 

1271 im_frame = frame_data.im 

1272 if not frame_data.bbox: 

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

1274 else: 

1275 bbox = frame_data.bbox 

1276 im_frame = im_frame.crop(bbox) 

1277 size = im_frame.size 

1278 encoderinfo = frame_data.encoderinfo 

1279 frame_duration = encoderinfo.get("duration", 0) 

1280 delay = Fraction(frame_duration / 1000).limit_denominator(65535) 

1281 if delay.numerator > 65535: 

1282 msg = "cannot write duration" 

1283 raise ValueError(msg) 

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

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

1286 # frame control 

1287 chunk( 

1288 fp, 

1289 b"fcTL", 

1290 o32(seq_num), # sequence_number 

1291 o32(size[0]), # width 

1292 o32(size[1]), # height 

1293 o32(bbox[0]), # x_offset 

1294 o32(bbox[1]), # y_offset 

1295 o16(delay.numerator), # delay_numerator 

1296 o16(delay.denominator), # delay_denominator 

1297 o8(frame_disposal), # dispose_op 

1298 o8(frame_blend), # blend_op 

1299 ) 

1300 seq_num += 1 

1301 # frame data 

1302 _apply_encoderinfo(im_frame, im.encoderinfo) 

1303 if frame == 0 and not default_image: 

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

1305 ImageFile._save( 

1306 im_frame, 

1307 cast(IO[bytes], _idat(fp, chunk)), 

1308 [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], 

1309 ) 

1310 else: 

1311 fdat_chunks = _fdat(fp, chunk, seq_num) 

1312 ImageFile._save( 

1313 im_frame, 

1314 cast(IO[bytes], fdat_chunks), 

1315 [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], 

1316 ) 

1317 seq_num = fdat_chunks.seq_num 

1318 return None 

1319 

1320 

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

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

1323 

1324 

1325def _save( 

1326 im: Image.Image, 

1327 fp: IO[bytes], 

1328 filename: str | bytes, 

1329 chunk: Callable[..., None] = putchunk, 

1330 save_all: bool = False, 

1331) -> None: 

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

1333 

1334 if save_all: 

1335 default_image = im.encoderinfo.get( 

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

1337 ) 

1338 modes = set() 

1339 sizes = set() 

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

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

1342 for im_frame in ImageSequence.Iterator(im_seq): 

1343 modes.add(im_frame.mode) 

1344 sizes.add(im_frame.size) 

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

1346 if mode in modes: 

1347 break 

1348 else: 

1349 mode = modes.pop() 

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

1351 else: 

1352 size = im.size 

1353 mode = im.mode 

1354 

1355 outmode = mode 

1356 if mode == "P": 

1357 # 

1358 # attempt to minimize storage requirements for palette images 

1359 if "bits" in im.encoderinfo: 

1360 # number of bits specified by user 

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

1362 else: 

1363 # check palette contents 

1364 if im.palette: 

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

1366 else: 

1367 colors = 256 

1368 

1369 if colors <= 16: 

1370 if colors <= 2: 

1371 bits = 1 

1372 elif colors <= 4: 

1373 bits = 2 

1374 else: 

1375 bits = 4 

1376 outmode += f";{bits}" 

1377 

1378 # get the corresponding PNG mode 

1379 try: 

1380 rawmode, bit_depth, color_type = _OUTMODES[outmode] 

1381 except KeyError as e: 

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

1383 raise OSError(msg) from e 

1384 if outmode == "I": 

1385 deprecate("Saving I mode images as PNG", 13, stacklevel=4) 

1386 

1387 # 

1388 # write minimal PNG file 

1389 

1390 fp.write(_MAGIC) 

1391 

1392 chunk( 

1393 fp, 

1394 b"IHDR", 

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

1396 o32(size[1]), 

1397 bit_depth, 

1398 color_type, 

1399 b"\0", # 10: compression 

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

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

1402 ) 

1403 

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

1405 

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

1407 if icc: 

1408 # ICC profile 

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

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

1411 # Null separator 1 byte (null character) 

1412 # Compression method 1 byte (0) 

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

1414 name = b"ICC Profile" 

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

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

1417 

1418 # You must either have sRGB or iCCP. 

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

1420 chunks.remove(b"sRGB") 

1421 

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

1423 if info: 

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

1425 for info_chunk in info.chunks: 

1426 cid, data = info_chunk[:2] 

1427 if cid in chunks: 

1428 chunks.remove(cid) 

1429 chunk(fp, cid, data) 

1430 elif cid in chunks_multiple_allowed: 

1431 chunk(fp, cid, data) 

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

1433 # Private chunk 

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

1435 if not after_idat: 

1436 chunk(fp, cid, data) 

1437 

1438 if im.mode == "P": 

1439 palette_byte_number = colors * 3 

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

1441 while len(palette_bytes) < palette_byte_number: 

1442 palette_bytes += b"\0" 

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

1444 

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

1446 

1447 if transparency or transparency == 0: 

1448 if im.mode == "P": 

1449 # limit to actual palette size 

1450 alpha_bytes = colors 

1451 if isinstance(transparency, bytes): 

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

1453 else: 

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

1455 alpha = b"\xff" * transparency + b"\0" 

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

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

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

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

1460 elif im.mode == "RGB": 

1461 red, green, blue = transparency 

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

1463 else: 

1464 if "transparency" in im.encoderinfo: 

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

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

1467 msg = "cannot use transparency for this mode" 

1468 raise OSError(msg) 

1469 else: 

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

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

1472 alpha_bytes = colors 

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

1474 

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

1476 if dpi: 

1477 chunk( 

1478 fp, 

1479 b"pHYs", 

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

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

1482 b"\x01", 

1483 ) 

1484 

1485 if info: 

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

1487 for info_chunk in info.chunks: 

1488 cid, data = info_chunk[:2] 

1489 if cid in chunks: 

1490 chunks.remove(cid) 

1491 chunk(fp, cid, data) 

1492 

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

1494 if exif: 

1495 if isinstance(exif, Image.Exif): 

1496 exif = exif.tobytes(8) 

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

1498 exif = exif[6:] 

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

1500 

1501 single_im: Image.Image | None = im 

1502 if save_all: 

1503 single_im = _write_multiple_frames( 

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

1505 ) 

1506 if single_im: 

1507 _apply_encoderinfo(single_im, im.encoderinfo) 

1508 ImageFile._save( 

1509 single_im, 

1510 cast(IO[bytes], _idat(fp, chunk)), 

1511 [ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)], 

1512 ) 

1513 

1514 if info: 

1515 for info_chunk in info.chunks: 

1516 cid, data = info_chunk[:2] 

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

1518 # Private chunk 

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

1520 if after_idat: 

1521 chunk(fp, cid, data) 

1522 

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

1524 

1525 if hasattr(fp, "flush"): 

1526 fp.flush() 

1527 

1528 

1529# -------------------------------------------------------------------- 

1530# PNG chunk converter 

1531 

1532 

1533def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes]]: 

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

1535 from io import BytesIO 

1536 

1537 chunks = [] 

1538 

1539 def append(fp: IO[bytes], cid: bytes, *data: bytes) -> None: 

1540 byte_data = b"".join(data) 

1541 crc = o32(_crc32(byte_data, _crc32(cid))) 

1542 chunks.append((cid, byte_data, crc)) 

1543 

1544 fp = BytesIO() 

1545 

1546 try: 

1547 im.encoderinfo = params 

1548 _save(im, fp, "", append) 

1549 finally: 

1550 del im.encoderinfo 

1551 

1552 return chunks 

1553 

1554 

1555# -------------------------------------------------------------------- 

1556# Registry 

1557 

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

1559Image.register_save(PngImageFile.format, _save) 

1560Image.register_save_all(PngImageFile.format, _save_all) 

1561 

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

1563 

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