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

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

959 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 __enter__(self) -> PngStream: 

404 return self 

405 

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

407 self.text_memory += chunklen 

408 if self.text_memory > MAX_TEXT_MEMORY: 

409 msg = ( 

410 "Too much memory used in text chunks: " 

411 f"{self.text_memory}>MAX_TEXT_MEMORY" 

412 ) 

413 raise ValueError(msg) 

414 

415 def save_rewind(self) -> None: 

416 self.rewind_state = _RewindState( 

417 self.im_info.copy(), 

418 self.im_tile, 

419 self._seq_num, 

420 ) 

421 

422 def rewind(self) -> None: 

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

424 self.im_tile = self.rewind_state.tile 

425 self._seq_num = self.rewind_state.seq_num 

426 

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

428 # ICC profile 

429 assert self.fp is not None 

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

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

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

433 # Null separator 1 byte (null character) 

434 # Compression method 1 byte (0) 

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

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

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

438 comp_method = s[i + 1] 

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

440 if comp_method != 0: 

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

442 raise SyntaxError(msg) 

443 try: 

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

445 except ValueError: 

446 if ImageFile.LOAD_TRUNCATED_IMAGES: 

447 icc_profile = None 

448 else: 

449 raise 

450 except zlib.error: 

451 icc_profile = None # FIXME 

452 self.im_info["icc_profile"] = icc_profile 

453 return s 

454 

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

456 # image header 

457 assert self.fp is not None 

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

459 if length < 13: 

460 if ImageFile.LOAD_TRUNCATED_IMAGES: 

461 return s 

462 msg = "Truncated IHDR chunk" 

463 raise ValueError(msg) 

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

465 try: 

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

467 except KeyError: 

468 pass 

469 if s[12]: 

470 self.im_info["interlace"] = 1 

471 if s[11]: 

472 msg = "unknown filter category" 

473 raise SyntaxError(msg) 

474 return s 

475 

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

477 # image data 

478 if "bbox" in self.im_info: 

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

480 else: 

481 if self.im_n_frames is not None: 

482 self.im_info["default_image"] = True 

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

484 self.im_tile = tile 

485 self.im_idat = length 

486 msg = "image data found" 

487 raise EOFError(msg) 

488 

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

490 msg = "end of PNG image" 

491 raise EOFError(msg) 

492 

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

494 # palette 

495 assert self.fp is not None 

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

497 if self.im_mode == "P": 

498 self.im_palette = "RGB", s 

499 return s 

500 

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

502 # transparency 

503 assert self.fp is not None 

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

505 if self.im_mode == "P": 

506 if _simple_palette.match(s): 

507 # tRNS contains only one full-transparent entry, 

508 # other entries are full opaque 

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

510 if i >= 0: 

511 self.im_info["transparency"] = i 

512 else: 

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

514 # for each palette entry 

515 self.im_info["transparency"] = s 

516 elif self.im_mode == "1": 

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

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

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

520 elif self.im_mode == "RGB": 

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

522 return s 

523 

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

525 # gamma setting 

526 assert self.fp is not None 

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

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

529 return s 

530 

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

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

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

534 

535 assert self.fp is not None 

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

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

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

539 return s 

540 

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

542 # srgb rendering intent, 1 byte 

543 # 0 perceptual 

544 # 1 relative colorimetric 

545 # 2 saturation 

546 # 3 absolute colorimetric 

547 

548 assert self.fp is not None 

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

550 if length < 1: 

551 if ImageFile.LOAD_TRUNCATED_IMAGES: 

552 return s 

553 msg = "Truncated sRGB chunk" 

554 raise ValueError(msg) 

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

556 return s 

557 

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

559 # pixels per unit 

560 assert self.fp is not None 

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

562 if length < 9: 

563 if ImageFile.LOAD_TRUNCATED_IMAGES: 

564 return s 

565 msg = "Truncated pHYs chunk" 

566 raise ValueError(msg) 

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

568 unit = s[8] 

569 if unit == 1: # meter 

570 dpi = px * 0.0254, py * 0.0254 

571 self.im_info["dpi"] = dpi 

572 elif unit == 0: 

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

574 return s 

575 

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

577 # text 

578 assert self.fp is not None 

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

580 try: 

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

582 except ValueError: 

583 # fallback for broken tEXt tags 

584 k = s 

585 v = b"" 

586 if k: 

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

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

589 

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

591 self.im_text[k_str] = v_str 

592 self.check_text_memory(len(v_str)) 

593 

594 return s 

595 

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

597 # compressed text 

598 assert self.fp is not None 

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

600 try: 

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

602 except ValueError: 

603 k = s 

604 v = b"" 

605 if v: 

606 comp_method = v[0] 

607 else: 

608 comp_method = 0 

609 if comp_method != 0: 

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

611 raise SyntaxError(msg) 

612 try: 

613 v = _safe_zlib_decompress(v[1:]) 

614 except ValueError: 

615 if ImageFile.LOAD_TRUNCATED_IMAGES: 

616 v = b"" 

617 else: 

618 raise 

619 except zlib.error: 

620 v = b"" 

621 

622 if k: 

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

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

625 

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

627 self.check_text_memory(len(v_str)) 

628 

629 return s 

630 

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

632 # international text 

633 assert self.fp is not None 

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

635 try: 

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

637 except ValueError: 

638 return s 

639 if len(r) < 2: 

640 return s 

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

642 try: 

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

644 except ValueError: 

645 return s 

646 if cf != 0: 

647 if cm == 0: 

648 try: 

649 v = _safe_zlib_decompress(v) 

650 except ValueError: 

651 if ImageFile.LOAD_TRUNCATED_IMAGES: 

652 return s 

653 else: 

654 raise 

655 except zlib.error: 

656 return s 

657 else: 

658 return s 

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

660 self.im_info["xmp"] = v 

661 try: 

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

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

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

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

666 except UnicodeError: 

667 return s 

668 

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

670 self.check_text_memory(len(v_str)) 

671 

672 return s 

673 

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

675 assert self.fp is not None 

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

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

678 return s 

679 

680 # APNG chunks 

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

682 assert self.fp is not None 

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

684 if length < 8: 

685 if ImageFile.LOAD_TRUNCATED_IMAGES: 

686 return s 

687 msg = "APNG contains truncated acTL chunk" 

688 raise ValueError(msg) 

689 if self.im_n_frames is not None: 

690 self.im_n_frames = None 

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

692 return s 

693 n_frames = i32(s) 

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

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

696 return s 

697 self.im_n_frames = n_frames 

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

699 self.im_custom_mimetype = "image/apng" 

700 return s 

701 

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

703 assert self.fp is not None 

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

705 if length < 26: 

706 if ImageFile.LOAD_TRUNCATED_IMAGES: 

707 return s 

708 msg = "APNG contains truncated fcTL chunk" 

709 raise ValueError(msg) 

710 seq = i32(s) 

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

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

713 ): 

714 msg = "APNG contains frame sequence errors" 

715 raise SyntaxError(msg) 

716 self._seq_num = seq 

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

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

719 im_w, im_h = self.im_size 

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

721 msg = "APNG contains invalid frames" 

722 raise SyntaxError(msg) 

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

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

725 if delay_den == 0: 

726 delay_den = 100 

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

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

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

730 return s 

731 

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

733 assert self.fp is not None 

734 if length < 4: 

735 if ImageFile.LOAD_TRUNCATED_IMAGES: 

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

737 return s 

738 msg = "APNG contains truncated fDAT chunk" 

739 raise ValueError(msg) 

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

741 seq = i32(s) 

742 if self._seq_num != seq - 1: 

743 msg = "APNG contains frame sequence errors" 

744 raise SyntaxError(msg) 

745 self._seq_num = seq 

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

747 

748 

749# -------------------------------------------------------------------- 

750# PNG reader 

751 

752 

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

754 return prefix.startswith(_MAGIC) 

755 

756 

757## 

758# Image plugin for PNG images. 

759 

760 

761class PngImageFile(ImageFile.ImageFile): 

762 format = "PNG" 

763 format_description = "Portable network graphics" 

764 

765 def _open(self) -> None: 

766 assert self.fp is not None 

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

768 msg = "not a PNG file" 

769 raise SyntaxError(msg) 

770 self._fp = self.fp 

771 self.__frame = 0 

772 

773 # 

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

775 

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

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

778 

779 while True: 

780 # 

781 # get next chunk 

782 

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

784 

785 try: 

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

787 except EOFError: 

788 break 

789 except AttributeError: 

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

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

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

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

794 

795 self.png.crc(cid, s) 

796 

797 # 

798 # Copy relevant attributes from the PngStream. An alternative 

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

800 # directly, but that introduces circular references which are 

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

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

803 

804 self._mode = self.png.im_mode 

805 self._size = self.png.im_size 

806 self.info = self.png.im_info 

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

808 self.tile = self.png.im_tile 

809 self.custom_mimetype = self.png.im_custom_mimetype 

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

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

812 

813 if self.png.im_palette: 

814 rawmode, data = self.png.im_palette 

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

816 

817 if cid == b"fdAT": 

818 self.__prepare_idat = length - 4 

819 else: 

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

821 

822 if self.png.im_n_frames is not None: 

823 self._close_exclusive_fp_after_loading = False 

824 self.png.save_rewind() 

825 self.__rewind_idat = self.__prepare_idat 

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

827 if self.default_image: 

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

829 self.n_frames += 1 

830 self._seek(0) 

831 self.is_animated = self.n_frames > 1 

832 

833 @property 

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

835 # experimental 

836 if self._text is None: 

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

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

839 if self.is_animated: 

840 frame = self.__frame 

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

842 self.seek(self.n_frames - 1) 

843 self.load() 

844 if self.is_animated: 

845 self.seek(frame) 

846 assert self._text is not None 

847 return self._text 

848 

849 def verify(self) -> None: 

850 """Verify PNG file""" 

851 

852 if self.fp is None: 

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

854 raise RuntimeError(msg) 

855 

856 # back up to beginning of IDAT block 

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

858 

859 assert self.png is not None 

860 self.png.verify() 

861 self.png.close() 

862 

863 super().verify() 

864 

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

866 if not self._seek_check(frame): 

867 return 

868 if frame < self.__frame: 

869 self._seek(0, True) 

870 

871 last_frame = self.__frame 

872 try: 

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

874 self._seek(f) 

875 except EOFError as e: 

876 self.seek(last_frame) 

877 msg = "no more images in APNG file" 

878 raise EOFError(msg) from e 

879 

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

881 assert self.png is not None 

882 if isinstance(self._fp, DeferredError): 

883 raise self._fp.ex 

884 

885 self.dispose: _imaging.ImagingCore | None 

886 dispose_extent = None 

887 if frame == 0: 

888 if rewind: 

889 self._fp.seek(self.__rewind) 

890 self.png.rewind() 

891 self.__prepare_idat = self.__rewind_idat 

892 self._im = None 

893 self.info = self.png.im_info 

894 self.tile = self.png.im_tile 

895 self.fp = self._fp 

896 self._prev_im = None 

897 self.dispose = None 

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

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

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

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

902 self.__frame = 0 

903 else: 

904 if frame != self.__frame + 1: 

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

906 raise ValueError(msg) 

907 

908 # ensure previous frame was loaded 

909 self.load() 

910 

911 if self.dispose: 

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

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

914 

915 self.fp = self._fp 

916 

917 # advance to the next frame 

918 if self.__prepare_idat: 

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

920 self.__prepare_idat = 0 

921 frame_start = False 

922 while True: 

923 self.fp.read(4) # CRC 

924 

925 try: 

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

927 except (struct.error, SyntaxError): 

928 break 

929 

930 if cid == b"IEND": 

931 msg = "No more images in APNG file" 

932 raise EOFError(msg) 

933 if cid == b"fcTL": 

934 if frame_start: 

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

936 msg = "APNG missing frame data" 

937 raise SyntaxError(msg) 

938 frame_start = True 

939 

940 try: 

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

942 except UnicodeDecodeError: 

943 break 

944 except EOFError: 

945 if cid == b"fdAT": 

946 length -= 4 

947 if frame_start: 

948 self.__prepare_idat = length 

949 break 

950 ImageFile._safe_read(self.fp, length) 

951 except AttributeError: 

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

953 ImageFile._safe_read(self.fp, length) 

954 

955 self.__frame = frame 

956 self.tile = self.png.im_tile 

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

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

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

960 

961 if not self.tile: 

962 msg = "image not found in APNG frame" 

963 raise EOFError(msg) 

964 if dispose_extent: 

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

966 

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

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

969 self.dispose_op = Disposal.OP_BACKGROUND 

970 

971 self.dispose = None 

972 if self.dispose_op == Disposal.OP_PREVIOUS: 

973 if self._prev_im: 

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

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

976 elif self.dispose_op == Disposal.OP_BACKGROUND: 

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

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

979 

980 def tell(self) -> int: 

981 return self.__frame 

982 

983 def load_prepare(self) -> None: 

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

985 

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

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

988 

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

990 ImageFile.ImageFile.load_prepare(self) 

991 

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

993 """internal: read more image data""" 

994 

995 assert self.png is not None 

996 assert self.fp is not None 

997 while self.__idat == 0: 

998 # end of chunk, skip forward to next one 

999 

1000 self.fp.read(4) # CRC 

1001 

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

1003 

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

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

1006 return b"" 

1007 

1008 if cid == b"fdAT": 

1009 try: 

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

1011 except EOFError: 

1012 pass 

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

1014 else: 

1015 self.__idat = length # empty chunks are allowed 

1016 

1017 # read more data from this chunk 

1018 if read_bytes <= 0: 

1019 read_bytes = self.__idat 

1020 else: 

1021 read_bytes = min(read_bytes, self.__idat) 

1022 

1023 self.__idat = self.__idat - read_bytes 

1024 

1025 return self.fp.read(read_bytes) 

1026 

1027 def load_end(self) -> None: 

1028 """internal: finished reading image data""" 

1029 assert self.png is not None 

1030 assert self.fp is not None 

1031 if self.__idat != 0: 

1032 self.fp.read(self.__idat) 

1033 while True: 

1034 self.fp.read(4) # CRC 

1035 

1036 try: 

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

1038 except (struct.error, SyntaxError): 

1039 break 

1040 

1041 if cid == b"IEND": 

1042 break 

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

1044 # start of the next frame, stop reading 

1045 self.__prepare_idat = 0 

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

1047 break 

1048 

1049 try: 

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

1051 except UnicodeDecodeError: 

1052 break 

1053 except EOFError: 

1054 if cid == b"fdAT": 

1055 length -= 4 

1056 try: 

1057 ImageFile._safe_read(self.fp, length) 

1058 except OSError as e: 

1059 if ImageFile.LOAD_TRUNCATED_IMAGES: 

1060 break 

1061 else: 

1062 raise e 

1063 except AttributeError: 

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

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

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

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

1068 self._text = self.png.im_text 

1069 if not self.is_animated: 

1070 self.png.close() 

1071 self.png = None 

1072 else: 

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

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

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

1076 mask = updated.convert_transparent( 

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

1078 ) 

1079 else: 

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

1081 t = self.info["transparency"] 

1082 if isinstance(t, bytes): 

1083 updated.putpalettealphas(t) 

1084 elif isinstance(t, int): 

1085 updated.putpalettealpha(t) 

1086 mask = updated.convert("RGBA") 

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

1088 self.im = self._prev_im 

1089 

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

1091 if "exif" not in self.info: 

1092 self.load() 

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

1094 return None 

1095 return self.getexif()._get_merged_dict() 

1096 

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

1098 if "exif" not in self.info: 

1099 self.load() 

1100 

1101 return super().getexif() 

1102 

1103 

1104# -------------------------------------------------------------------- 

1105# PNG writer 

1106 

1107_OUTMODES = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1124} 

1125 

1126 

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

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

1129 

1130 byte_data = b"".join(data) 

1131 

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

1133 fp.write(byte_data) 

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

1135 fp.write(o32(crc)) 

1136 

1137 

1138class _idat: 

1139 # wrap output from the encoder in IDAT chunks 

1140 

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

1142 self.fp = fp 

1143 self.chunk = chunk 

1144 

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

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

1147 

1148 

1149class _fdat: 

1150 # wrap encoder output in fdAT chunks 

1151 

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

1153 self.fp = fp 

1154 self.chunk = chunk 

1155 self.seq_num = seq_num 

1156 

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

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

1159 self.seq_num += 1 

1160 

1161 

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

1163 im.encoderconfig = ( 

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

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

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

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

1168 ) 

1169 

1170 

1171class _Frame(NamedTuple): 

1172 im: Image.Image 

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

1174 encoderinfo: dict[str, Any] 

1175 

1176 

1177def _write_multiple_frames( 

1178 im: Image.Image, 

1179 fp: IO[bytes], 

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

1181 mode: str, 

1182 rawmode: str, 

1183 default_image: Image.Image | None, 

1184 append_images: list[Image.Image], 

1185) -> Image.Image | None: 

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

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

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

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

1190 

1191 if default_image: 

1192 chain = itertools.chain(append_images) 

1193 else: 

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

1195 

1196 im_frames: list[_Frame] = [] 

1197 frame_count = 0 

1198 for im_seq in chain: 

1199 for im_frame in ImageSequence.Iterator(im_seq): 

1200 if im_frame.mode == mode: 

1201 im_frame = im_frame.copy() 

1202 else: 

1203 im_frame = im_frame.convert(mode) 

1204 encoderinfo = im.encoderinfo.copy() 

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

1206 encoderinfo["duration"] = duration[frame_count] 

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

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

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

1210 encoderinfo["disposal"] = disposal[frame_count] 

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

1212 encoderinfo["blend"] = blend[frame_count] 

1213 frame_count += 1 

1214 

1215 if im_frames: 

1216 previous = im_frames[-1] 

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

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

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

1220 prev_disposal = Disposal.OP_BACKGROUND 

1221 

1222 if prev_disposal == Disposal.OP_BACKGROUND: 

1223 base_im = previous.im.copy() 

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

1225 bbox = previous.bbox 

1226 if bbox: 

1227 dispose = dispose.crop(bbox) 

1228 else: 

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

1230 base_im.paste(dispose, bbox) 

1231 elif prev_disposal == Disposal.OP_PREVIOUS: 

1232 base_im = im_frames[-2].im 

1233 else: 

1234 base_im = previous.im 

1235 delta = ImageChops.subtract_modulo( 

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

1237 ) 

1238 bbox = delta.getbbox(alpha_only=False) 

1239 if ( 

1240 not bbox 

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

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

1243 and "duration" in encoderinfo 

1244 ): 

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

1246 continue 

1247 else: 

1248 bbox = None 

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

1250 

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

1252 return im_frames[0].im 

1253 

1254 # animation control 

1255 chunk( 

1256 fp, 

1257 b"acTL", 

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

1259 o32(loop), # 4: num_plays 

1260 ) 

1261 

1262 # default image IDAT (if it exists) 

1263 if default_image: 

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

1265 _apply_encoderinfo(default_im, im.encoderinfo) 

1266 ImageFile._save( 

1267 default_im, 

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

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

1270 ) 

1271 

1272 seq_num = 0 

1273 for frame, frame_data in enumerate(im_frames): 

1274 im_frame = frame_data.im 

1275 if not frame_data.bbox: 

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

1277 else: 

1278 bbox = frame_data.bbox 

1279 im_frame = im_frame.crop(bbox) 

1280 size = im_frame.size 

1281 encoderinfo = frame_data.encoderinfo 

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

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

1284 if delay.numerator > 65535: 

1285 msg = "cannot write duration" 

1286 raise ValueError(msg) 

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

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

1289 # frame control 

1290 chunk( 

1291 fp, 

1292 b"fcTL", 

1293 o32(seq_num), # sequence_number 

1294 o32(size[0]), # width 

1295 o32(size[1]), # height 

1296 o32(bbox[0]), # x_offset 

1297 o32(bbox[1]), # y_offset 

1298 o16(delay.numerator), # delay_numerator 

1299 o16(delay.denominator), # delay_denominator 

1300 o8(frame_disposal), # dispose_op 

1301 o8(frame_blend), # blend_op 

1302 ) 

1303 seq_num += 1 

1304 # frame data 

1305 _apply_encoderinfo(im_frame, im.encoderinfo) 

1306 if frame == 0 and not default_image: 

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

1308 ImageFile._save( 

1309 im_frame, 

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

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

1312 ) 

1313 else: 

1314 fdat_chunks = _fdat(fp, chunk, seq_num) 

1315 ImageFile._save( 

1316 im_frame, 

1317 cast(IO[bytes], fdat_chunks), 

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

1319 ) 

1320 seq_num = fdat_chunks.seq_num 

1321 return None 

1322 

1323 

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

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

1326 

1327 

1328def _save( 

1329 im: Image.Image, 

1330 fp: IO[bytes], 

1331 filename: str | bytes, 

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

1333 save_all: bool = False, 

1334) -> None: 

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

1336 

1337 if save_all: 

1338 default_image = im.encoderinfo.get( 

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

1340 ) 

1341 modes = set() 

1342 sizes = set() 

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

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

1345 for im_frame in ImageSequence.Iterator(im_seq): 

1346 modes.add(im_frame.mode) 

1347 sizes.add(im_frame.size) 

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

1349 if mode in modes: 

1350 break 

1351 else: 

1352 mode = modes.pop() 

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

1354 else: 

1355 size = im.size 

1356 mode = im.mode 

1357 

1358 outmode = mode 

1359 palette = [] 

1360 if im.palette: 

1361 palette = im.getpalette() or [] 

1362 if mode == "P": 

1363 # 

1364 # attempt to minimize storage requirements for palette images 

1365 if "bits" in im.encoderinfo: 

1366 # number of bits specified by user 

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

1368 else: 

1369 # check palette contents 

1370 if im.palette: 

1371 colors = max(min(len(palette) // 3, 256), 1) 

1372 else: 

1373 colors = 256 

1374 

1375 if colors <= 16: 

1376 if colors <= 2: 

1377 bits = 1 

1378 elif colors <= 4: 

1379 bits = 2 

1380 else: 

1381 bits = 4 

1382 outmode += f";{bits}" 

1383 

1384 # get the corresponding PNG mode 

1385 try: 

1386 rawmode, bit_depth, color_type = _OUTMODES[outmode] 

1387 except KeyError as e: 

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

1389 raise OSError(msg) from e 

1390 if outmode == "I": 

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

1392 

1393 # 

1394 # write minimal PNG file 

1395 

1396 fp.write(_MAGIC) 

1397 

1398 chunk( 

1399 fp, 

1400 b"IHDR", 

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

1402 o32(size[1]), 

1403 bit_depth, 

1404 color_type, 

1405 b"\0", # 10: compression 

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

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

1408 ) 

1409 

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

1411 

1412 if icc := im.encoderinfo.get("icc_profile", im.info.get("icc_profile")): 

1413 # ICC profile 

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

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

1416 # Null separator 1 byte (null character) 

1417 # Compression method 1 byte (0) 

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

1419 name = b"ICC Profile" 

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

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

1422 

1423 # You must either have sRGB or iCCP. 

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

1425 chunks.remove(b"sRGB") 

1426 

1427 if info := im.encoderinfo.get("pnginfo"): 

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

1429 for info_chunk in info.chunks: 

1430 cid, data = info_chunk[:2] 

1431 if cid in chunks: 

1432 chunks.remove(cid) 

1433 chunk(fp, cid, data) 

1434 elif cid in chunks_multiple_allowed: 

1435 chunk(fp, cid, data) 

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

1437 # Private chunk 

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

1439 if not after_idat: 

1440 chunk(fp, cid, data) 

1441 

1442 if im.mode == "P": 

1443 palette_byte_number = colors * 3 

1444 palette_bytes = bytes(palette[:palette_byte_number]) 

1445 while len(palette_bytes) < palette_byte_number: 

1446 palette_bytes += b"\0" 

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

1448 

1449 transparency = im.encoderinfo.get("transparency", im.info.get("transparency")) 

1450 

1451 if transparency is not None: 

1452 if im.mode == "P": 

1453 # limit to actual palette size 

1454 alpha_bytes = colors 

1455 if isinstance(transparency, bytes): 

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

1457 elif isinstance(transparency, int): 

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

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

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

1461 else: 

1462 msg = "transparency for P must be an integer or bytes" 

1463 raise ValueError(msg) 

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

1465 if isinstance(transparency, int): 

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

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

1468 else: 

1469 msg = f"transparency for {im.mode} must be an integer" 

1470 raise ValueError(msg) 

1471 elif im.mode == "RGB": 

1472 if not isinstance(transparency, (list, tuple)): 

1473 msg = "transparency for RGB must be list or tuple" 

1474 raise ValueError(msg) 

1475 elif len(transparency) != 3: 

1476 msg = "transparency for RGB must have length 3" 

1477 raise ValueError(msg) 

1478 else: 

1479 red, green, blue = transparency 

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

1481 elif im.encoderinfo.get("transparency") is not None: 

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

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

1484 msg = "cannot use transparency for this mode" 

1485 raise OSError(msg) 

1486 elif im.mode == "P" and im.im.getpalettemode() == "RGBA": 

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

1488 alpha_bytes = colors 

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

1490 

1491 if dpi := im.encoderinfo.get("dpi"): 

1492 chunk( 

1493 fp, 

1494 b"pHYs", 

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

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

1497 b"\x01", 

1498 ) 

1499 

1500 if info: 

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

1502 for info_chunk in info.chunks: 

1503 cid, data = info_chunk[:2] 

1504 if cid in chunks: 

1505 chunks.remove(cid) 

1506 chunk(fp, cid, data) 

1507 

1508 if exif := im.encoderinfo.get("exif"): 

1509 if isinstance(exif, Image.Exif): 

1510 exif = exif.tobytes(8) 

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

1512 exif = exif[6:] 

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

1514 

1515 single_im: Image.Image | None = im 

1516 if save_all: 

1517 single_im = _write_multiple_frames( 

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

1519 ) 

1520 if single_im: 

1521 _apply_encoderinfo(single_im, im.encoderinfo) 

1522 ImageFile._save( 

1523 single_im, 

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

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

1526 ) 

1527 

1528 if info: 

1529 for info_chunk in info.chunks: 

1530 cid, data = info_chunk[:2] 

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

1532 # Private chunk 

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

1534 if after_idat: 

1535 chunk(fp, cid, data) 

1536 

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

1538 

1539 if hasattr(fp, "flush"): 

1540 fp.flush() 

1541 

1542 

1543# -------------------------------------------------------------------- 

1544# PNG chunk converter 

1545 

1546 

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

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

1549 from io import BytesIO 

1550 

1551 chunks = [] 

1552 

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

1554 byte_data = b"".join(data) 

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

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

1557 

1558 fp = BytesIO() 

1559 

1560 try: 

1561 im.encoderinfo = params 

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

1563 finally: 

1564 del im.encoderinfo 

1565 

1566 return chunks 

1567 

1568 

1569# -------------------------------------------------------------------- 

1570# Registry 

1571 

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

1573Image.register_save(PngImageFile.format, _save) 

1574Image.register_save_all(PngImageFile.format, _save_all) 

1575 

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

1577 

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