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

954 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 ._util import DeferredError 

52 

53TYPE_CHECKING = False 

54if TYPE_CHECKING: 

55 from collections.abc import Callable 

56 from typing import Any, NoReturn, Self 

57 

58 from . import _imaging 

59 

60logger = logging.getLogger(__name__) 

61 

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

63 

64 

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

66 

67 

68_MODES = { 

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

70 # Grayscale 

71 (1, 0): ("1", "1"), 

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

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

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

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

76 # Truecolour 

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

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

79 # Indexed-colour 

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

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

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

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

84 # Grayscale with alpha 

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

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

87 # Truecolour with alpha 

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

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

90} 

91 

92 

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

94 

95MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK 

96""" 

97Maximum decompressed size for a iTXt or zTXt chunk. 

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

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

100""" 

101MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK 

102""" 

103Set the maximum total text chunk size. 

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

105""" 

106 

107 

108# APNG frame disposal modes 

109class Disposal(IntEnum): 

110 OP_NONE = 0 

111 """ 

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

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

114 """ 

115 OP_BACKGROUND = 1 

116 """ 

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

118 the next frame. 

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

120 """ 

121 OP_PREVIOUS = 2 

122 """ 

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

124 rendering the next frame. 

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

126 """ 

127 

128 

129# APNG frame blend modes 

130class Blend(IntEnum): 

131 OP_SOURCE = 0 

132 """ 

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

134 image contents. 

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

136 """ 

137 OP_OVER = 1 

138 """ 

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

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

141 """ 

142 

143 

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

145 dobj = zlib.decompressobj() 

146 plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) 

147 if dobj.unconsumed_tail: 

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

149 raise ValueError(msg) 

150 return plaintext 

151 

152 

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

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

155 

156 

157# -------------------------------------------------------------------- 

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

159 

160 

161class ChunkStream: 

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

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

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

165 

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

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

168 cid = None 

169 

170 assert self.fp is not None 

171 if self.queue: 

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

173 self.fp.seek(pos) 

174 else: 

175 s = self.fp.read(8) 

176 cid = s[4:] 

177 pos = self.fp.tell() 

178 length = i32(s) 

179 

180 if not is_cid(cid): 

181 if not ImageFile.LOAD_TRUNCATED_IMAGES: 

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

183 raise SyntaxError(msg) 

184 

185 return cid, pos, length 

186 

187 def __enter__(self) -> Self: 

188 return self 

189 

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

191 self.close() 

192 

193 def close(self) -> None: 

194 self.queue = self.fp = None 

195 

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

197 assert self.queue is not None 

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

199 

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

201 """Call the appropriate chunk handler""" 

202 

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

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

205 

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

207 """Read and verify checksum""" 

208 

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

210 # images 

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

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

213 self.crc_skip(cid, data) 

214 return 

215 

216 assert self.fp is not None 

217 try: 

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

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

220 if crc1 != crc2: 

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

222 raise SyntaxError(msg) 

223 except struct.error as e: 

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

225 raise SyntaxError(msg) from e 

226 

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

228 """Read checksum""" 

229 

230 assert self.fp is not None 

231 self.fp.read(4) 

232 

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

234 # Simple approach; just calculate checksum for all remaining 

235 # blocks. Must be called directly after open. 

236 

237 cids = [] 

238 

239 assert self.fp is not None 

240 while True: 

241 try: 

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

243 except struct.error as e: 

244 msg = "truncated PNG file" 

245 raise OSError(msg) from e 

246 

247 if cid == endchunk: 

248 break 

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

250 cids.append(cid) 

251 

252 return cids 

253 

254 

255class iTXt(str): 

256 """ 

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

258 keeping their extra information 

259 

260 """ 

261 

262 lang: str | bytes | None 

263 tkey: str | bytes | None 

264 

265 @staticmethod 

266 def __new__( 

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

268 ) -> Self: 

269 """ 

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

271 :param text: value for this key 

272 :param lang: language code 

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

274 """ 

275 

276 self = str.__new__(cls, text) 

277 self.lang = lang 

278 self.tkey = tkey 

279 return self 

280 

281 

282class PngInfo: 

283 """ 

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

285 

286 """ 

287 

288 def __init__(self) -> None: 

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

290 

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

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

293 

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

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

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

297 should be written after IDAT 

298 

299 """ 

300 

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

302 

303 def add_itxt( 

304 self, 

305 key: str | bytes, 

306 value: str | bytes, 

307 lang: str | bytes = "", 

308 tkey: str | bytes = "", 

309 zip: bool = False, 

310 ) -> None: 

311 """Appends an iTXt chunk. 

312 

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

314 :param value: value for this key 

315 :param lang: language code 

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

317 :param zip: compression flag 

318 

319 """ 

320 

321 if not isinstance(key, bytes): 

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

323 if not isinstance(value, bytes): 

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

325 if not isinstance(lang, bytes): 

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

327 if not isinstance(tkey, bytes): 

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

329 

330 if zip: 

331 self.add( 

332 b"iTXt", 

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

334 ) 

335 else: 

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

337 

338 def add_text( 

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

340 ) -> None: 

341 """Appends a text chunk. 

342 

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

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

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

346 :param zip: compression flag 

347 

348 """ 

349 if isinstance(value, iTXt): 

350 return self.add_itxt( 

351 key, 

352 value, 

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

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

355 zip=zip, 

356 ) 

357 

358 # The tEXt chunk stores latin-1 text 

359 if not isinstance(value, bytes): 

360 try: 

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

362 except UnicodeError: 

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

364 

365 if not isinstance(key, bytes): 

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

367 

368 if zip: 

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

370 else: 

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

372 

373 

374# -------------------------------------------------------------------- 

375# PNG image stream (IHDR/IEND) 

376 

377 

378class _RewindState(NamedTuple): 

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

380 tile: list[ImageFile._Tile] 

381 seq_num: int | None 

382 

383 

384class PngStream(ChunkStream): 

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

386 super().__init__(fp) 

387 

388 # local copies of Image attributes 

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

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

391 self.im_size = (0, 0) 

392 self.im_mode = "" 

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

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

395 self.im_custom_mimetype: str | None = None 

396 self.im_n_frames: int | None = None 

397 self._seq_num: int | None = None 

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

399 

400 self.text_memory = 0 

401 

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

403 self.text_memory += chunklen 

404 if self.text_memory > MAX_TEXT_MEMORY: 

405 msg = ( 

406 "Too much memory used in text chunks: " 

407 f"{self.text_memory}>MAX_TEXT_MEMORY" 

408 ) 

409 raise ValueError(msg) 

410 

411 def save_rewind(self) -> None: 

412 self.rewind_state = _RewindState( 

413 self.im_info.copy(), 

414 self.im_tile, 

415 self._seq_num, 

416 ) 

417 

418 def rewind(self) -> None: 

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

420 self.im_tile = self.rewind_state.tile 

421 self._seq_num = self.rewind_state.seq_num 

422 

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

424 # ICC profile 

425 assert self.fp is not None 

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

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

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

429 # Null separator 1 byte (null character) 

430 # Compression method 1 byte (0) 

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

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

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

434 comp_method = s[i + 1] 

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

436 if comp_method != 0: 

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

438 raise SyntaxError(msg) 

439 try: 

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

441 except ValueError: 

442 if ImageFile.LOAD_TRUNCATED_IMAGES: 

443 icc_profile = None 

444 else: 

445 raise 

446 except zlib.error: 

447 icc_profile = None # FIXME 

448 self.im_info["icc_profile"] = icc_profile 

449 return s 

450 

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

452 # image header 

453 assert self.fp is not None 

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

455 if length < 13: 

456 if ImageFile.LOAD_TRUNCATED_IMAGES: 

457 return s 

458 msg = "Truncated IHDR chunk" 

459 raise ValueError(msg) 

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

461 try: 

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

463 except KeyError: 

464 pass 

465 if s[12]: 

466 self.im_info["interlace"] = 1 

467 if s[11]: 

468 msg = "unknown filter category" 

469 raise SyntaxError(msg) 

470 return s 

471 

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

473 # image data 

474 if "bbox" in self.im_info: 

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

476 else: 

477 if self.im_n_frames is not None: 

478 self.im_info["default_image"] = True 

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

480 self.im_tile = tile 

481 self.im_idat = length 

482 msg = "image data found" 

483 raise EOFError(msg) 

484 

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

486 msg = "end of PNG image" 

487 raise EOFError(msg) 

488 

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

490 # palette 

491 assert self.fp is not None 

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

493 if self.im_mode == "P": 

494 self.im_palette = "RGB", s 

495 return s 

496 

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

498 # transparency 

499 assert self.fp is not None 

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

501 if self.im_mode == "P": 

502 if _simple_palette.match(s): 

503 # tRNS contains only one full-transparent entry, 

504 # other entries are full opaque 

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

506 if i >= 0: 

507 self.im_info["transparency"] = i 

508 else: 

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

510 # for each palette entry 

511 self.im_info["transparency"] = s 

512 elif self.im_mode == "1": 

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

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

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

516 elif self.im_mode == "RGB": 

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

518 return s 

519 

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

521 # gamma setting 

522 assert self.fp is not None 

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

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

525 return s 

526 

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

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

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

530 

531 assert self.fp is not None 

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

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

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

535 return s 

536 

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

538 # srgb rendering intent, 1 byte 

539 # 0 perceptual 

540 # 1 relative colorimetric 

541 # 2 saturation 

542 # 3 absolute colorimetric 

543 

544 assert self.fp is not None 

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

546 if length < 1: 

547 if ImageFile.LOAD_TRUNCATED_IMAGES: 

548 return s 

549 msg = "Truncated sRGB chunk" 

550 raise ValueError(msg) 

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

552 return s 

553 

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

555 # pixels per unit 

556 assert self.fp is not None 

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

558 if length < 9: 

559 if ImageFile.LOAD_TRUNCATED_IMAGES: 

560 return s 

561 msg = "Truncated pHYs chunk" 

562 raise ValueError(msg) 

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

564 unit = s[8] 

565 if unit == 1: # meter 

566 dpi = px * 0.0254, py * 0.0254 

567 self.im_info["dpi"] = dpi 

568 elif unit == 0: 

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

570 return s 

571 

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

573 # text 

574 assert self.fp is not None 

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

576 try: 

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

578 except ValueError: 

579 # fallback for broken tEXt tags 

580 k = s 

581 v = b"" 

582 if k: 

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

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

585 

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

587 self.im_text[k_str] = v_str 

588 self.check_text_memory(len(v_str)) 

589 

590 return s 

591 

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

593 # compressed text 

594 assert self.fp is not None 

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

596 try: 

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

598 except ValueError: 

599 k = s 

600 v = b"" 

601 if v: 

602 comp_method = v[0] 

603 else: 

604 comp_method = 0 

605 if comp_method != 0: 

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

607 raise SyntaxError(msg) 

608 try: 

609 v = _safe_zlib_decompress(v[1:]) 

610 except ValueError: 

611 if ImageFile.LOAD_TRUNCATED_IMAGES: 

612 v = b"" 

613 else: 

614 raise 

615 except zlib.error: 

616 v = b"" 

617 

618 if k: 

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

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

621 

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

623 self.check_text_memory(len(v_str)) 

624 

625 return s 

626 

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

628 # international text 

629 assert self.fp is not None 

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

631 try: 

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

633 except ValueError: 

634 return s 

635 if len(r) < 2: 

636 return s 

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

638 try: 

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

640 except ValueError: 

641 return s 

642 if cf != 0: 

643 if cm == 0: 

644 try: 

645 v = _safe_zlib_decompress(v) 

646 except ValueError: 

647 if ImageFile.LOAD_TRUNCATED_IMAGES: 

648 return s 

649 else: 

650 raise 

651 except zlib.error: 

652 return s 

653 else: 

654 return s 

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

656 self.im_info["xmp"] = v 

657 try: 

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

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

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

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

662 except UnicodeError: 

663 return s 

664 

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

666 self.check_text_memory(len(v_str)) 

667 

668 return s 

669 

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

671 assert self.fp is not None 

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

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

674 return s 

675 

676 # APNG chunks 

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

678 assert self.fp is not None 

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

680 if length < 8: 

681 if ImageFile.LOAD_TRUNCATED_IMAGES: 

682 return s 

683 msg = "APNG contains truncated acTL chunk" 

684 raise ValueError(msg) 

685 if self.im_n_frames is not None: 

686 self.im_n_frames = None 

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

688 return s 

689 n_frames = i32(s) 

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

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

692 return s 

693 self.im_n_frames = n_frames 

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

695 self.im_custom_mimetype = "image/apng" 

696 return s 

697 

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

699 assert self.fp is not None 

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

701 if length < 26: 

702 if ImageFile.LOAD_TRUNCATED_IMAGES: 

703 return s 

704 msg = "APNG contains truncated fcTL chunk" 

705 raise ValueError(msg) 

706 seq = i32(s) 

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

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

709 ): 

710 msg = "APNG contains frame sequence errors" 

711 raise SyntaxError(msg) 

712 self._seq_num = seq 

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

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

715 im_w, im_h = self.im_size 

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

717 msg = "APNG contains invalid frames" 

718 raise SyntaxError(msg) 

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

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

721 if delay_den == 0: 

722 delay_den = 100 

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

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

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

726 return s 

727 

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

729 assert self.fp is not None 

730 if length < 4: 

731 if ImageFile.LOAD_TRUNCATED_IMAGES: 

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

733 return s 

734 msg = "APNG contains truncated fDAT chunk" 

735 raise ValueError(msg) 

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

737 seq = i32(s) 

738 if self._seq_num != seq - 1: 

739 msg = "APNG contains frame sequence errors" 

740 raise SyntaxError(msg) 

741 self._seq_num = seq 

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

743 

744 

745# -------------------------------------------------------------------- 

746# PNG reader 

747 

748 

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

750 return prefix.startswith(_MAGIC) 

751 

752 

753## 

754# Image plugin for PNG images. 

755 

756 

757class PngImageFile(ImageFile.ImageFile): 

758 format = "PNG" 

759 format_description = "Portable network graphics" 

760 

761 def _open(self) -> None: 

762 assert self.fp is not None 

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

764 msg = "not a PNG file" 

765 raise SyntaxError(msg) 

766 self._fp = self.fp 

767 self.__frame = 0 

768 

769 # 

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

771 

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

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

774 

775 while True: 

776 # 

777 # get next chunk 

778 

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

780 

781 try: 

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

783 except EOFError: 

784 break 

785 except AttributeError: 

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

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

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

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

790 

791 self.png.crc(cid, s) 

792 

793 # 

794 # Copy relevant attributes from the PngStream. An alternative 

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

796 # directly, but that introduces circular references which are 

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

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

799 

800 self._mode = self.png.im_mode 

801 self._size = self.png.im_size 

802 self.info = self.png.im_info 

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

804 self.tile = self.png.im_tile 

805 self.custom_mimetype = self.png.im_custom_mimetype 

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

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

808 

809 if self.png.im_palette: 

810 rawmode, data = self.png.im_palette 

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

812 

813 if cid == b"fdAT": 

814 self.__prepare_idat = length - 4 

815 else: 

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

817 

818 if self.png.im_n_frames is not None: 

819 self._close_exclusive_fp_after_loading = False 

820 self.png.save_rewind() 

821 self.__rewind_idat = self.__prepare_idat 

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

823 if self.default_image: 

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

825 self.n_frames += 1 

826 self._seek(0) 

827 self.is_animated = self.n_frames > 1 

828 

829 @property 

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

831 # experimental 

832 if self._text is None: 

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

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

835 if self.is_animated: 

836 frame = self.__frame 

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

838 self.seek(self.n_frames - 1) 

839 self.load() 

840 if self.is_animated: 

841 self.seek(frame) 

842 assert self._text is not None 

843 return self._text 

844 

845 def verify(self) -> None: 

846 """Verify PNG file""" 

847 

848 if self.fp is None: 

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

850 raise RuntimeError(msg) 

851 

852 # back up to beginning of IDAT block 

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

854 

855 assert self.png is not None 

856 self.png.verify() 

857 self.png.close() 

858 

859 super().verify() 

860 

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

862 if not self._seek_check(frame): 

863 return 

864 if frame < self.__frame: 

865 self._seek(0, True) 

866 

867 last_frame = self.__frame 

868 try: 

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

870 self._seek(f) 

871 except EOFError as e: 

872 self.seek(last_frame) 

873 msg = "no more images in APNG file" 

874 raise EOFError(msg) from e 

875 

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

877 assert self.png is not None 

878 if isinstance(self._fp, DeferredError): 

879 raise self._fp.ex 

880 

881 self.dispose: _imaging.ImagingCore | None 

882 dispose_extent = None 

883 if frame == 0: 

884 if rewind: 

885 self._fp.seek(self.__rewind) 

886 self.png.rewind() 

887 self.__prepare_idat = self.__rewind_idat 

888 self._im = None 

889 self.info = self.png.im_info 

890 self.tile = self.png.im_tile 

891 self.fp = self._fp 

892 self._prev_im = None 

893 self.dispose = None 

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

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

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

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

898 self.__frame = 0 

899 else: 

900 if frame != self.__frame + 1: 

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

902 raise ValueError(msg) 

903 

904 # ensure previous frame was loaded 

905 self.load() 

906 

907 if self.dispose: 

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

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

910 

911 self.fp = self._fp 

912 

913 # advance to the next frame 

914 if self.__prepare_idat: 

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

916 self.__prepare_idat = 0 

917 frame_start = False 

918 while True: 

919 self.fp.read(4) # CRC 

920 

921 try: 

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

923 except (struct.error, SyntaxError): 

924 break 

925 

926 if cid == b"IEND": 

927 msg = "No more images in APNG file" 

928 raise EOFError(msg) 

929 if cid == b"fcTL": 

930 if frame_start: 

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

932 msg = "APNG missing frame data" 

933 raise SyntaxError(msg) 

934 frame_start = True 

935 

936 try: 

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

938 except UnicodeDecodeError: 

939 break 

940 except EOFError: 

941 if cid == b"fdAT": 

942 length -= 4 

943 if frame_start: 

944 self.__prepare_idat = length 

945 break 

946 ImageFile._safe_read(self.fp, length) 

947 except AttributeError: 

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

949 ImageFile._safe_read(self.fp, length) 

950 

951 self.__frame = frame 

952 self.tile = self.png.im_tile 

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

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

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

956 

957 if not self.tile: 

958 msg = "image not found in APNG frame" 

959 raise EOFError(msg) 

960 if dispose_extent: 

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

962 

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

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

965 self.dispose_op = Disposal.OP_BACKGROUND 

966 

967 self.dispose = None 

968 if self.dispose_op == Disposal.OP_PREVIOUS: 

969 if self._prev_im: 

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

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

972 elif self.dispose_op == Disposal.OP_BACKGROUND: 

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

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

975 

976 def tell(self) -> int: 

977 return self.__frame 

978 

979 def load_prepare(self) -> None: 

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

981 

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

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

984 

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

986 ImageFile.ImageFile.load_prepare(self) 

987 

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

989 """internal: read more image data""" 

990 

991 assert self.png is not None 

992 assert self.fp is not None 

993 while self.__idat == 0: 

994 # end of chunk, skip forward to next one 

995 

996 self.fp.read(4) # CRC 

997 

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

999 

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

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

1002 return b"" 

1003 

1004 if cid == b"fdAT": 

1005 try: 

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

1007 except EOFError: 

1008 pass 

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

1010 else: 

1011 self.__idat = length # empty chunks are allowed 

1012 

1013 # read more data from this chunk 

1014 if read_bytes <= 0: 

1015 read_bytes = self.__idat 

1016 else: 

1017 read_bytes = min(read_bytes, self.__idat) 

1018 

1019 self.__idat = self.__idat - read_bytes 

1020 

1021 return self.fp.read(read_bytes) 

1022 

1023 def load_end(self) -> None: 

1024 """internal: finished reading image data""" 

1025 assert self.png is not None 

1026 assert self.fp is not None 

1027 if self.__idat != 0: 

1028 self.fp.read(self.__idat) 

1029 while True: 

1030 self.fp.read(4) # CRC 

1031 

1032 try: 

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

1034 except (struct.error, SyntaxError): 

1035 break 

1036 

1037 if cid == b"IEND": 

1038 break 

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

1040 # start of the next frame, stop reading 

1041 self.__prepare_idat = 0 

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

1043 break 

1044 

1045 try: 

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

1047 except UnicodeDecodeError: 

1048 break 

1049 except EOFError: 

1050 if cid == b"fdAT": 

1051 length -= 4 

1052 try: 

1053 ImageFile._safe_read(self.fp, length) 

1054 except OSError as e: 

1055 if ImageFile.LOAD_TRUNCATED_IMAGES: 

1056 break 

1057 else: 

1058 raise e 

1059 except AttributeError: 

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

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

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

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

1064 self._text = self.png.im_text 

1065 if not self.is_animated: 

1066 self.png.close() 

1067 self.png = None 

1068 else: 

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

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

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

1072 mask = updated.convert_transparent( 

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

1074 ) 

1075 else: 

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

1077 t = self.info["transparency"] 

1078 if isinstance(t, bytes): 

1079 updated.putpalettealphas(t) 

1080 elif isinstance(t, int): 

1081 updated.putpalettealpha(t) 

1082 mask = updated.convert("RGBA") 

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

1084 self.im = self._prev_im 

1085 

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

1087 if "exif" not in self.info: 

1088 self.load() 

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

1090 return None 

1091 return self.getexif()._get_merged_dict() 

1092 

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

1094 if "exif" not in self.info: 

1095 self.load() 

1096 

1097 return super().getexif() 

1098 

1099 

1100# -------------------------------------------------------------------- 

1101# PNG writer 

1102 

1103_OUTMODES = { 

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

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

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

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

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

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

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

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

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

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

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

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

1116} 

1117 

1118 

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

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

1121 

1122 byte_data = b"".join(data) 

1123 

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

1125 fp.write(byte_data) 

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

1127 fp.write(o32(crc)) 

1128 

1129 

1130class _idat: 

1131 # wrap output from the encoder in IDAT chunks 

1132 

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

1134 self.fp = fp 

1135 self.chunk = chunk 

1136 

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

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

1139 

1140 

1141class _fdat: 

1142 # wrap encoder output in fdAT chunks 

1143 

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

1145 self.fp = fp 

1146 self.chunk = chunk 

1147 self.seq_num = seq_num 

1148 

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

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

1151 self.seq_num += 1 

1152 

1153 

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

1155 im.encoderconfig = ( 

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

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

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

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

1160 ) 

1161 

1162 

1163class _Frame(NamedTuple): 

1164 im: Image.Image 

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

1166 encoderinfo: dict[str, Any] 

1167 

1168 

1169def _write_multiple_frames( 

1170 im: Image.Image, 

1171 fp: IO[bytes], 

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

1173 mode: str, 

1174 rawmode: str, 

1175 default_image: Image.Image | None, 

1176 append_images: list[Image.Image], 

1177) -> Image.Image | None: 

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

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

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

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

1182 

1183 if default_image: 

1184 chain = itertools.chain(append_images) 

1185 else: 

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

1187 

1188 im_frames: list[_Frame] = [] 

1189 frame_count = 0 

1190 for im_seq in chain: 

1191 for im_frame in ImageSequence.Iterator(im_seq): 

1192 if im_frame.mode == mode: 

1193 im_frame = im_frame.copy() 

1194 else: 

1195 im_frame = im_frame.convert(mode) 

1196 encoderinfo = im.encoderinfo.copy() 

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

1198 encoderinfo["duration"] = duration[frame_count] 

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

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

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

1202 encoderinfo["disposal"] = disposal[frame_count] 

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

1204 encoderinfo["blend"] = blend[frame_count] 

1205 frame_count += 1 

1206 

1207 if im_frames: 

1208 previous = im_frames[-1] 

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

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

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

1212 prev_disposal = Disposal.OP_BACKGROUND 

1213 

1214 if prev_disposal == Disposal.OP_BACKGROUND: 

1215 base_im = previous.im.copy() 

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

1217 bbox = previous.bbox 

1218 if bbox: 

1219 dispose = dispose.crop(bbox) 

1220 else: 

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

1222 base_im.paste(dispose, bbox) 

1223 elif prev_disposal == Disposal.OP_PREVIOUS: 

1224 base_im = im_frames[-2].im 

1225 else: 

1226 base_im = previous.im 

1227 delta = ImageChops.subtract_modulo( 

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

1229 ) 

1230 bbox = delta.getbbox(alpha_only=False) 

1231 if ( 

1232 not bbox 

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

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

1235 and "duration" in encoderinfo 

1236 ): 

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

1238 continue 

1239 else: 

1240 bbox = None 

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

1242 

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

1244 return im_frames[0].im 

1245 

1246 # animation control 

1247 chunk( 

1248 fp, 

1249 b"acTL", 

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

1251 o32(loop), # 4: num_plays 

1252 ) 

1253 

1254 # default image IDAT (if it exists) 

1255 if default_image: 

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

1257 _apply_encoderinfo(default_im, im.encoderinfo) 

1258 ImageFile._save( 

1259 default_im, 

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

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

1262 ) 

1263 

1264 seq_num = 0 

1265 for frame, frame_data in enumerate(im_frames): 

1266 im_frame = frame_data.im 

1267 if not frame_data.bbox: 

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

1269 else: 

1270 bbox = frame_data.bbox 

1271 im_frame = im_frame.crop(bbox) 

1272 size = im_frame.size 

1273 encoderinfo = frame_data.encoderinfo 

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

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

1276 if delay.numerator > 65535: 

1277 msg = "cannot write duration" 

1278 raise ValueError(msg) 

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

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

1281 # frame control 

1282 chunk( 

1283 fp, 

1284 b"fcTL", 

1285 o32(seq_num), # sequence_number 

1286 o32(size[0]), # width 

1287 o32(size[1]), # height 

1288 o32(bbox[0]), # x_offset 

1289 o32(bbox[1]), # y_offset 

1290 o16(delay.numerator), # delay_numerator 

1291 o16(delay.denominator), # delay_denominator 

1292 o8(frame_disposal), # dispose_op 

1293 o8(frame_blend), # blend_op 

1294 ) 

1295 seq_num += 1 

1296 # frame data 

1297 _apply_encoderinfo(im_frame, im.encoderinfo) 

1298 if frame == 0 and not default_image: 

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

1300 ImageFile._save( 

1301 im_frame, 

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

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

1304 ) 

1305 else: 

1306 fdat_chunks = _fdat(fp, chunk, seq_num) 

1307 ImageFile._save( 

1308 im_frame, 

1309 cast(IO[bytes], fdat_chunks), 

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

1311 ) 

1312 seq_num = fdat_chunks.seq_num 

1313 return None 

1314 

1315 

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

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

1318 

1319 

1320def _save( 

1321 im: Image.Image, 

1322 fp: IO[bytes], 

1323 filename: str | bytes, 

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

1325 save_all: bool = False, 

1326) -> None: 

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

1328 

1329 if save_all: 

1330 default_image = im.encoderinfo.get( 

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

1332 ) 

1333 modes = set() 

1334 sizes = set() 

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

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

1337 for im_frame in ImageSequence.Iterator(im_seq): 

1338 modes.add(im_frame.mode) 

1339 sizes.add(im_frame.size) 

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

1341 if mode in modes: 

1342 break 

1343 else: 

1344 mode = modes.pop() 

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

1346 else: 

1347 size = im.size 

1348 mode = im.mode 

1349 

1350 outmode = mode 

1351 palette = [] 

1352 if im.palette: 

1353 palette = im.getpalette() or [] 

1354 if mode == "P": 

1355 # 

1356 # attempt to minimize storage requirements for palette images 

1357 if "bits" in im.encoderinfo: 

1358 # number of bits specified by user 

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

1360 else: 

1361 # check palette contents 

1362 if im.palette: 

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

1364 else: 

1365 colors = 256 

1366 

1367 if colors <= 16: 

1368 if colors <= 2: 

1369 bits = 1 

1370 elif colors <= 4: 

1371 bits = 2 

1372 else: 

1373 bits = 4 

1374 outmode += f";{bits}" 

1375 

1376 # get the corresponding PNG mode 

1377 try: 

1378 rawmode, bit_depth, color_type = _OUTMODES[outmode] 

1379 except KeyError as e: 

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

1381 raise OSError(msg) from e 

1382 

1383 # 

1384 # write minimal PNG file 

1385 

1386 fp.write(_MAGIC) 

1387 

1388 chunk( 

1389 fp, 

1390 b"IHDR", 

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

1392 o32(size[1]), 

1393 bit_depth, 

1394 color_type, 

1395 b"\0", # 10: compression 

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

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

1398 ) 

1399 

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

1401 

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

1403 # ICC profile 

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

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

1406 # Null separator 1 byte (null character) 

1407 # Compression method 1 byte (0) 

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

1409 name = b"ICC Profile" 

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

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

1412 

1413 # You must either have sRGB or iCCP. 

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

1415 chunks.remove(b"sRGB") 

1416 

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

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

1419 for info_chunk in info.chunks: 

1420 cid, data = info_chunk[:2] 

1421 if cid in chunks: 

1422 chunks.remove(cid) 

1423 chunk(fp, cid, data) 

1424 elif cid in chunks_multiple_allowed: 

1425 chunk(fp, cid, data) 

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

1427 # Private chunk 

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

1429 if not after_idat: 

1430 chunk(fp, cid, data) 

1431 

1432 if im.mode == "P": 

1433 palette_byte_number = colors * 3 

1434 palette_bytes = bytes(palette[:palette_byte_number]) 

1435 while len(palette_bytes) < palette_byte_number: 

1436 palette_bytes += b"\0" 

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

1438 

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

1440 

1441 if transparency is not None: 

1442 if im.mode == "P": 

1443 # limit to actual palette size 

1444 alpha_bytes = colors 

1445 if isinstance(transparency, bytes): 

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

1447 elif isinstance(transparency, int): 

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

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

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

1451 else: 

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

1453 raise ValueError(msg) 

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

1455 if isinstance(transparency, int): 

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

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

1458 else: 

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

1460 raise ValueError(msg) 

1461 elif im.mode == "RGB": 

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

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

1464 raise ValueError(msg) 

1465 elif len(transparency) != 3: 

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

1467 raise ValueError(msg) 

1468 else: 

1469 red, green, blue = transparency 

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

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

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

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

1474 msg = "cannot use transparency for this mode" 

1475 raise OSError(msg) 

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

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

1478 alpha_bytes = colors 

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

1480 

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

1482 chunk( 

1483 fp, 

1484 b"pHYs", 

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

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

1487 b"\x01", 

1488 ) 

1489 

1490 if info: 

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

1492 for info_chunk in info.chunks: 

1493 cid, data = info_chunk[:2] 

1494 if cid in chunks: 

1495 chunks.remove(cid) 

1496 chunk(fp, cid, data) 

1497 

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

1499 if isinstance(exif, Image.Exif): 

1500 exif = exif.tobytes(8) 

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

1502 exif = exif[6:] 

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

1504 

1505 single_im: Image.Image | None = im 

1506 if save_all: 

1507 single_im = _write_multiple_frames( 

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

1509 ) 

1510 if single_im: 

1511 _apply_encoderinfo(single_im, im.encoderinfo) 

1512 ImageFile._save( 

1513 single_im, 

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

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

1516 ) 

1517 

1518 if info: 

1519 for info_chunk in info.chunks: 

1520 cid, data = info_chunk[:2] 

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

1522 # Private chunk 

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

1524 if after_idat: 

1525 chunk(fp, cid, data) 

1526 

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

1528 

1529 if hasattr(fp, "flush"): 

1530 fp.flush() 

1531 

1532 

1533# -------------------------------------------------------------------- 

1534# PNG chunk converter 

1535 

1536 

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

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

1539 from io import BytesIO 

1540 

1541 chunks = [] 

1542 

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

1544 byte_data = b"".join(data) 

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

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

1547 

1548 fp = BytesIO() 

1549 

1550 try: 

1551 im.encoderinfo = params 

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

1553 finally: 

1554 del im.encoderinfo 

1555 

1556 return chunks 

1557 

1558 

1559# -------------------------------------------------------------------- 

1560# Registry 

1561 

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

1563Image.register_save(PngImageFile.format, _save) 

1564Image.register_save_all(PngImageFile.format, _save_all) 

1565 

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

1567 

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