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

940 statements  

1# 

2# The Python Imaging Library. 

3# $Id$ 

4# 

5# PNG support code 

6# 

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

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

9# 

10# history: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

27# 

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

29# Copyright (c) 1996 by Fredrik Lundh 

30# 

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

32# 

33from __future__ import annotations 

34 

35import itertools 

36import logging 

37import re 

38import struct 

39import warnings 

40import zlib 

41from enum import IntEnum 

42from typing import IO, NamedTuple, cast 

43 

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

45from ._binary import i16be as i16 

46from ._binary import i32be as i32 

47from ._binary import o8 

48from ._binary import o16be as o16 

49from ._binary import o32be as o32 

50from ._deprecate import deprecate 

51from ._util import DeferredError 

52 

53TYPE_CHECKING = False 

54if TYPE_CHECKING: 

55 from collections.abc import Callable 

56 from typing import Any, NoReturn 

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) -> ChunkStream: 

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 ) -> iTXt: 

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 Exception: 

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 if not _accept(self.fp.read(8)): 

763 msg = "not a PNG file" 

764 raise SyntaxError(msg) 

765 self._fp = self.fp 

766 self.__frame = 0 

767 

768 # 

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

770 

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

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

773 

774 while True: 

775 # 

776 # get next chunk 

777 

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

779 

780 try: 

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

782 except EOFError: 

783 break 

784 except AttributeError: 

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

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

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

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

789 

790 self.png.crc(cid, s) 

791 

792 # 

793 # Copy relevant attributes from the PngStream. An alternative 

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

795 # directly, but that introduces circular references which are 

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

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

798 

799 self._mode = self.png.im_mode 

800 self._size = self.png.im_size 

801 self.info = self.png.im_info 

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

803 self.tile = self.png.im_tile 

804 self.custom_mimetype = self.png.im_custom_mimetype 

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

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

807 

808 if self.png.im_palette: 

809 rawmode, data = self.png.im_palette 

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

811 

812 if cid == b"fdAT": 

813 self.__prepare_idat = length - 4 

814 else: 

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

816 

817 if self.png.im_n_frames is not None: 

818 self._close_exclusive_fp_after_loading = False 

819 self.png.save_rewind() 

820 self.__rewind_idat = self.__prepare_idat 

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

822 if self.default_image: 

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

824 self.n_frames += 1 

825 self._seek(0) 

826 self.is_animated = self.n_frames > 1 

827 

828 @property 

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

830 # experimental 

831 if self._text is None: 

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

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

834 if self.is_animated: 

835 frame = self.__frame 

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

837 self.seek(self.n_frames - 1) 

838 self.load() 

839 if self.is_animated: 

840 self.seek(frame) 

841 assert self._text is not None 

842 return self._text 

843 

844 def verify(self) -> None: 

845 """Verify PNG file""" 

846 

847 if self.fp is None: 

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

849 raise RuntimeError(msg) 

850 

851 # back up to beginning of IDAT block 

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

853 

854 assert self.png is not None 

855 self.png.verify() 

856 self.png.close() 

857 

858 if self._exclusive_fp: 

859 self.fp.close() 

860 self.fp = None 

861 

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

863 if not self._seek_check(frame): 

864 return 

865 if frame < self.__frame: 

866 self._seek(0, True) 

867 

868 last_frame = self.__frame 

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

870 try: 

871 self._seek(f) 

872 except EOFError as e: 

873 self.seek(last_frame) 

874 msg = "no more images in APNG file" 

875 raise EOFError(msg) from e 

876 

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

878 assert self.png is not None 

879 if isinstance(self._fp, DeferredError): 

880 raise self._fp.ex 

881 

882 self.dispose: _imaging.ImagingCore | None 

883 dispose_extent = None 

884 if frame == 0: 

885 if rewind: 

886 self._fp.seek(self.__rewind) 

887 self.png.rewind() 

888 self.__prepare_idat = self.__rewind_idat 

889 self._im = None 

890 self.info = self.png.im_info 

891 self.tile = self.png.im_tile 

892 self.fp = self._fp 

893 self._prev_im = None 

894 self.dispose = None 

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

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

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

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

899 self.__frame = 0 

900 else: 

901 if frame != self.__frame + 1: 

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

903 raise ValueError(msg) 

904 

905 # ensure previous frame was loaded 

906 self.load() 

907 

908 if self.dispose: 

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

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

911 

912 self.fp = self._fp 

913 

914 # advance to the next frame 

915 if self.__prepare_idat: 

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

917 self.__prepare_idat = 0 

918 frame_start = False 

919 while True: 

920 self.fp.read(4) # CRC 

921 

922 try: 

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

924 except (struct.error, SyntaxError): 

925 break 

926 

927 if cid == b"IEND": 

928 msg = "No more images in APNG file" 

929 raise EOFError(msg) 

930 if cid == b"fcTL": 

931 if frame_start: 

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

933 msg = "APNG missing frame data" 

934 raise SyntaxError(msg) 

935 frame_start = True 

936 

937 try: 

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

939 except UnicodeDecodeError: 

940 break 

941 except EOFError: 

942 if cid == b"fdAT": 

943 length -= 4 

944 if frame_start: 

945 self.__prepare_idat = length 

946 break 

947 ImageFile._safe_read(self.fp, length) 

948 except AttributeError: 

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

950 ImageFile._safe_read(self.fp, length) 

951 

952 self.__frame = frame 

953 self.tile = self.png.im_tile 

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

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

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

957 

958 if not self.tile: 

959 msg = "image not found in APNG frame" 

960 raise EOFError(msg) 

961 if dispose_extent: 

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

963 

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

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

966 self.dispose_op = Disposal.OP_BACKGROUND 

967 

968 self.dispose = None 

969 if self.dispose_op == Disposal.OP_PREVIOUS: 

970 if self._prev_im: 

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

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

973 elif self.dispose_op == Disposal.OP_BACKGROUND: 

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

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

976 

977 def tell(self) -> int: 

978 return self.__frame 

979 

980 def load_prepare(self) -> None: 

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

982 

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

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

985 

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

987 ImageFile.ImageFile.load_prepare(self) 

988 

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

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

991 

992 assert self.png is not None 

993 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 if self.__idat != 0: 

1027 self.fp.read(self.__idat) 

1028 while True: 

1029 self.fp.read(4) # CRC 

1030 

1031 try: 

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

1033 except (struct.error, SyntaxError): 

1034 break 

1035 

1036 if cid == b"IEND": 

1037 break 

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

1039 # start of the next frame, stop reading 

1040 self.__prepare_idat = 0 

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

1042 break 

1043 

1044 try: 

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

1046 except UnicodeDecodeError: 

1047 break 

1048 except EOFError: 

1049 if cid == b"fdAT": 

1050 length -= 4 

1051 try: 

1052 ImageFile._safe_read(self.fp, length) 

1053 except OSError as e: 

1054 if ImageFile.LOAD_TRUNCATED_IMAGES: 

1055 break 

1056 else: 

1057 raise e 

1058 except AttributeError: 

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

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

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

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

1063 self._text = self.png.im_text 

1064 if not self.is_animated: 

1065 self.png.close() 

1066 self.png = None 

1067 else: 

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

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

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

1071 mask = updated.convert_transparent( 

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

1073 ) 

1074 else: 

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

1076 t = self.info["transparency"] 

1077 if isinstance(t, bytes): 

1078 updated.putpalettealphas(t) 

1079 elif isinstance(t, int): 

1080 updated.putpalettealpha(t) 

1081 mask = updated.convert("RGBA") 

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

1083 self.im = self._prev_im 

1084 

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

1086 if "exif" not in self.info: 

1087 self.load() 

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

1089 return None 

1090 return self.getexif()._get_merged_dict() 

1091 

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

1093 if "exif" not in self.info: 

1094 self.load() 

1095 

1096 return super().getexif() 

1097 

1098 

1099# -------------------------------------------------------------------- 

1100# PNG writer 

1101 

1102_OUTMODES = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1119} 

1120 

1121 

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

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

1124 

1125 byte_data = b"".join(data) 

1126 

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

1128 fp.write(byte_data) 

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

1130 fp.write(o32(crc)) 

1131 

1132 

1133class _idat: 

1134 # wrap output from the encoder in IDAT chunks 

1135 

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

1137 self.fp = fp 

1138 self.chunk = chunk 

1139 

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

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

1142 

1143 

1144class _fdat: 

1145 # wrap encoder output in fdAT chunks 

1146 

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

1148 self.fp = fp 

1149 self.chunk = chunk 

1150 self.seq_num = seq_num 

1151 

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

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

1154 self.seq_num += 1 

1155 

1156 

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

1158 im.encoderconfig = ( 

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

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

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

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

1163 ) 

1164 

1165 

1166class _Frame(NamedTuple): 

1167 im: Image.Image 

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

1169 encoderinfo: dict[str, Any] 

1170 

1171 

1172def _write_multiple_frames( 

1173 im: Image.Image, 

1174 fp: IO[bytes], 

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

1176 mode: str, 

1177 rawmode: str, 

1178 default_image: Image.Image | None, 

1179 append_images: list[Image.Image], 

1180) -> Image.Image | None: 

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

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

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

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

1185 

1186 if default_image: 

1187 chain = itertools.chain(append_images) 

1188 else: 

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

1190 

1191 im_frames: list[_Frame] = [] 

1192 frame_count = 0 

1193 for im_seq in chain: 

1194 for im_frame in ImageSequence.Iterator(im_seq): 

1195 if im_frame.mode == mode: 

1196 im_frame = im_frame.copy() 

1197 else: 

1198 im_frame = im_frame.convert(mode) 

1199 encoderinfo = im.encoderinfo.copy() 

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

1201 encoderinfo["duration"] = duration[frame_count] 

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

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

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

1205 encoderinfo["disposal"] = disposal[frame_count] 

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

1207 encoderinfo["blend"] = blend[frame_count] 

1208 frame_count += 1 

1209 

1210 if im_frames: 

1211 previous = im_frames[-1] 

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

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

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

1215 prev_disposal = Disposal.OP_BACKGROUND 

1216 

1217 if prev_disposal == Disposal.OP_BACKGROUND: 

1218 base_im = previous.im.copy() 

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

1220 bbox = previous.bbox 

1221 if bbox: 

1222 dispose = dispose.crop(bbox) 

1223 else: 

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

1225 base_im.paste(dispose, bbox) 

1226 elif prev_disposal == Disposal.OP_PREVIOUS: 

1227 base_im = im_frames[-2].im 

1228 else: 

1229 base_im = previous.im 

1230 delta = ImageChops.subtract_modulo( 

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

1232 ) 

1233 bbox = delta.getbbox(alpha_only=False) 

1234 if ( 

1235 not bbox 

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

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

1238 and "duration" in encoderinfo 

1239 ): 

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

1241 continue 

1242 else: 

1243 bbox = None 

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

1245 

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

1247 return im_frames[0].im 

1248 

1249 # animation control 

1250 chunk( 

1251 fp, 

1252 b"acTL", 

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

1254 o32(loop), # 4: num_plays 

1255 ) 

1256 

1257 # default image IDAT (if it exists) 

1258 if default_image: 

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

1260 _apply_encoderinfo(default_im, im.encoderinfo) 

1261 ImageFile._save( 

1262 default_im, 

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

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

1265 ) 

1266 

1267 seq_num = 0 

1268 for frame, frame_data in enumerate(im_frames): 

1269 im_frame = frame_data.im 

1270 if not frame_data.bbox: 

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

1272 else: 

1273 bbox = frame_data.bbox 

1274 im_frame = im_frame.crop(bbox) 

1275 size = im_frame.size 

1276 encoderinfo = frame_data.encoderinfo 

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

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

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

1280 # frame control 

1281 chunk( 

1282 fp, 

1283 b"fcTL", 

1284 o32(seq_num), # sequence_number 

1285 o32(size[0]), # width 

1286 o32(size[1]), # height 

1287 o32(bbox[0]), # x_offset 

1288 o32(bbox[1]), # y_offset 

1289 o16(frame_duration), # delay_numerator 

1290 o16(1000), # delay_denominator 

1291 o8(frame_disposal), # dispose_op 

1292 o8(frame_blend), # blend_op 

1293 ) 

1294 seq_num += 1 

1295 # frame data 

1296 _apply_encoderinfo(im_frame, im.encoderinfo) 

1297 if frame == 0 and not default_image: 

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

1299 ImageFile._save( 

1300 im_frame, 

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

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

1303 ) 

1304 else: 

1305 fdat_chunks = _fdat(fp, chunk, seq_num) 

1306 ImageFile._save( 

1307 im_frame, 

1308 cast(IO[bytes], fdat_chunks), 

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

1310 ) 

1311 seq_num = fdat_chunks.seq_num 

1312 return None 

1313 

1314 

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

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

1317 

1318 

1319def _save( 

1320 im: Image.Image, 

1321 fp: IO[bytes], 

1322 filename: str | bytes, 

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

1324 save_all: bool = False, 

1325) -> None: 

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

1327 

1328 if save_all: 

1329 default_image = im.encoderinfo.get( 

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

1331 ) 

1332 modes = set() 

1333 sizes = set() 

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

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

1336 for im_frame in ImageSequence.Iterator(im_seq): 

1337 modes.add(im_frame.mode) 

1338 sizes.add(im_frame.size) 

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

1340 if mode in modes: 

1341 break 

1342 else: 

1343 mode = modes.pop() 

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

1345 else: 

1346 size = im.size 

1347 mode = im.mode 

1348 

1349 outmode = mode 

1350 if mode == "P": 

1351 # 

1352 # attempt to minimize storage requirements for palette images 

1353 if "bits" in im.encoderinfo: 

1354 # number of bits specified by user 

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

1356 else: 

1357 # check palette contents 

1358 if im.palette: 

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

1360 else: 

1361 colors = 256 

1362 

1363 if colors <= 16: 

1364 if colors <= 2: 

1365 bits = 1 

1366 elif colors <= 4: 

1367 bits = 2 

1368 else: 

1369 bits = 4 

1370 outmode += f";{bits}" 

1371 

1372 # get the corresponding PNG mode 

1373 try: 

1374 rawmode, bit_depth, color_type = _OUTMODES[outmode] 

1375 except KeyError as e: 

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

1377 raise OSError(msg) from e 

1378 if outmode == "I": 

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

1380 

1381 # 

1382 # write minimal PNG file 

1383 

1384 fp.write(_MAGIC) 

1385 

1386 chunk( 

1387 fp, 

1388 b"IHDR", 

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

1390 o32(size[1]), 

1391 bit_depth, 

1392 color_type, 

1393 b"\0", # 10: compression 

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

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

1396 ) 

1397 

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

1399 

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

1401 if icc: 

1402 # ICC profile 

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

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

1405 # Null separator 1 byte (null character) 

1406 # Compression method 1 byte (0) 

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

1408 name = b"ICC Profile" 

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

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

1411 

1412 # You must either have sRGB or iCCP. 

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

1414 chunks.remove(b"sRGB") 

1415 

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

1417 if info: 

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 = im.im.getpalette("RGB")[: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", None)) 

1440 

1441 if transparency or transparency == 0: 

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 else: 

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

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

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

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

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

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

1454 elif im.mode == "RGB": 

1455 red, green, blue = transparency 

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

1457 else: 

1458 if "transparency" in im.encoderinfo: 

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

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

1461 msg = "cannot use transparency for this mode" 

1462 raise OSError(msg) 

1463 else: 

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

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

1466 alpha_bytes = colors 

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

1468 

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

1470 if dpi: 

1471 chunk( 

1472 fp, 

1473 b"pHYs", 

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

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

1476 b"\x01", 

1477 ) 

1478 

1479 if info: 

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

1481 for info_chunk in info.chunks: 

1482 cid, data = info_chunk[:2] 

1483 if cid in chunks: 

1484 chunks.remove(cid) 

1485 chunk(fp, cid, data) 

1486 

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

1488 if exif: 

1489 if isinstance(exif, Image.Exif): 

1490 exif = exif.tobytes(8) 

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

1492 exif = exif[6:] 

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

1494 

1495 single_im: Image.Image | None = im 

1496 if save_all: 

1497 single_im = _write_multiple_frames( 

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

1499 ) 

1500 if single_im: 

1501 _apply_encoderinfo(single_im, im.encoderinfo) 

1502 ImageFile._save( 

1503 single_im, 

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

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

1506 ) 

1507 

1508 if info: 

1509 for info_chunk in info.chunks: 

1510 cid, data = info_chunk[:2] 

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

1512 # Private chunk 

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

1514 if after_idat: 

1515 chunk(fp, cid, data) 

1516 

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

1518 

1519 if hasattr(fp, "flush"): 

1520 fp.flush() 

1521 

1522 

1523# -------------------------------------------------------------------- 

1524# PNG chunk converter 

1525 

1526 

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

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

1529 from io import BytesIO 

1530 

1531 chunks = [] 

1532 

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

1534 byte_data = b"".join(data) 

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

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

1537 

1538 fp = BytesIO() 

1539 

1540 try: 

1541 im.encoderinfo = params 

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

1543 finally: 

1544 del im.encoderinfo 

1545 

1546 return chunks 

1547 

1548 

1549# -------------------------------------------------------------------- 

1550# Registry 

1551 

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

1553Image.register_save(PngImageFile.format, _save) 

1554Image.register_save_all(PngImageFile.format, _save_all) 

1555 

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

1557 

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