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

934 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 collections.abc import Callable 

42from enum import IntEnum 

43from typing import IO, Any, NamedTuple, NoReturn, cast 

44 

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

46from ._binary import i16be as i16 

47from ._binary import i32be as i32 

48from ._binary import o8 

49from ._binary import o16be as o16 

50from ._binary import o32be as o32 

51from ._deprecate import deprecate 

52from ._util import DeferredError 

53 

54TYPE_CHECKING = False 

55if TYPE_CHECKING: 

56 from . import _imaging 

57 

58logger = logging.getLogger(__name__) 

59 

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

61 

62 

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

64 

65 

66_MODES = { 

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

68 # Grayscale 

69 (1, 0): ("1", "1"), 

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

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

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

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

74 # Truecolour 

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

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

77 # Indexed-colour 

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

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

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

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

82 # Grayscale with alpha 

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

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

85 # Truecolour with alpha 

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

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

88} 

89 

90 

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

92 

93MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK 

94""" 

95Maximum decompressed size for a iTXt or zTXt chunk. 

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

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

98""" 

99MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK 

100""" 

101Set the maximum total text chunk size. 

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

103""" 

104 

105 

106# APNG frame disposal modes 

107class Disposal(IntEnum): 

108 OP_NONE = 0 

109 """ 

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

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

112 """ 

113 OP_BACKGROUND = 1 

114 """ 

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

116 the next frame. 

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

118 """ 

119 OP_PREVIOUS = 2 

120 """ 

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

122 rendering the next frame. 

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

124 """ 

125 

126 

127# APNG frame blend modes 

128class Blend(IntEnum): 

129 OP_SOURCE = 0 

130 """ 

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

132 image contents. 

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

134 """ 

135 OP_OVER = 1 

136 """ 

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

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

139 """ 

140 

141 

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

143 dobj = zlib.decompressobj() 

144 plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) 

145 if dobj.unconsumed_tail: 

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

147 raise ValueError(msg) 

148 return plaintext 

149 

150 

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

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

153 

154 

155# -------------------------------------------------------------------- 

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

157 

158 

159class ChunkStream: 

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

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

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

163 

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

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

166 cid = None 

167 

168 assert self.fp is not None 

169 if self.queue: 

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

171 self.fp.seek(pos) 

172 else: 

173 s = self.fp.read(8) 

174 cid = s[4:] 

175 pos = self.fp.tell() 

176 length = i32(s) 

177 

178 if not is_cid(cid): 

179 if not ImageFile.LOAD_TRUNCATED_IMAGES: 

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

181 raise SyntaxError(msg) 

182 

183 return cid, pos, length 

184 

185 def __enter__(self) -> ChunkStream: 

186 return self 

187 

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

189 self.close() 

190 

191 def close(self) -> None: 

192 self.queue = self.fp = None 

193 

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

195 assert self.queue is not None 

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

197 

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

199 """Call the appropriate chunk handler""" 

200 

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

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

203 

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

205 """Read and verify checksum""" 

206 

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

208 # images 

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

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

211 self.crc_skip(cid, data) 

212 return 

213 

214 assert self.fp is not None 

215 try: 

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

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

218 if crc1 != crc2: 

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

220 raise SyntaxError(msg) 

221 except struct.error as e: 

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

223 raise SyntaxError(msg) from e 

224 

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

226 """Read checksum""" 

227 

228 assert self.fp is not None 

229 self.fp.read(4) 

230 

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

232 # Simple approach; just calculate checksum for all remaining 

233 # blocks. Must be called directly after open. 

234 

235 cids = [] 

236 

237 assert self.fp is not None 

238 while True: 

239 try: 

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

241 except struct.error as e: 

242 msg = "truncated PNG file" 

243 raise OSError(msg) from e 

244 

245 if cid == endchunk: 

246 break 

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

248 cids.append(cid) 

249 

250 return cids 

251 

252 

253class iTXt(str): 

254 """ 

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

256 keeping their extra information 

257 

258 """ 

259 

260 lang: str | bytes | None 

261 tkey: str | bytes | None 

262 

263 @staticmethod 

264 def __new__( 

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

266 ) -> iTXt: 

267 """ 

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

269 :param text: value for this key 

270 :param lang: language code 

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

272 """ 

273 

274 self = str.__new__(cls, text) 

275 self.lang = lang 

276 self.tkey = tkey 

277 return self 

278 

279 

280class PngInfo: 

281 """ 

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

283 

284 """ 

285 

286 def __init__(self) -> None: 

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

288 

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

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

291 

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

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

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

295 should be written after IDAT 

296 

297 """ 

298 

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

300 

301 def add_itxt( 

302 self, 

303 key: str | bytes, 

304 value: str | bytes, 

305 lang: str | bytes = "", 

306 tkey: str | bytes = "", 

307 zip: bool = False, 

308 ) -> None: 

309 """Appends an iTXt chunk. 

310 

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

312 :param value: value for this key 

313 :param lang: language code 

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

315 :param zip: compression flag 

316 

317 """ 

318 

319 if not isinstance(key, bytes): 

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

321 if not isinstance(value, bytes): 

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

323 if not isinstance(lang, bytes): 

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

325 if not isinstance(tkey, bytes): 

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

327 

328 if zip: 

329 self.add( 

330 b"iTXt", 

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

332 ) 

333 else: 

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

335 

336 def add_text( 

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

338 ) -> None: 

339 """Appends a text chunk. 

340 

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

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

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

344 :param zip: compression flag 

345 

346 """ 

347 if isinstance(value, iTXt): 

348 return self.add_itxt( 

349 key, 

350 value, 

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

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

353 zip=zip, 

354 ) 

355 

356 # The tEXt chunk stores latin-1 text 

357 if not isinstance(value, bytes): 

358 try: 

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

360 except UnicodeError: 

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

362 

363 if not isinstance(key, bytes): 

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

365 

366 if zip: 

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

368 else: 

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

370 

371 

372# -------------------------------------------------------------------- 

373# PNG image stream (IHDR/IEND) 

374 

375 

376class _RewindState(NamedTuple): 

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

378 tile: list[ImageFile._Tile] 

379 seq_num: int | None 

380 

381 

382class PngStream(ChunkStream): 

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

384 super().__init__(fp) 

385 

386 # local copies of Image attributes 

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

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

389 self.im_size = (0, 0) 

390 self.im_mode = "" 

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

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

393 self.im_custom_mimetype: str | None = None 

394 self.im_n_frames: int | None = None 

395 self._seq_num: int | None = None 

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

397 

398 self.text_memory = 0 

399 

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

401 self.text_memory += chunklen 

402 if self.text_memory > MAX_TEXT_MEMORY: 

403 msg = ( 

404 "Too much memory used in text chunks: " 

405 f"{self.text_memory}>MAX_TEXT_MEMORY" 

406 ) 

407 raise ValueError(msg) 

408 

409 def save_rewind(self) -> None: 

410 self.rewind_state = _RewindState( 

411 self.im_info.copy(), 

412 self.im_tile, 

413 self._seq_num, 

414 ) 

415 

416 def rewind(self) -> None: 

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

418 self.im_tile = self.rewind_state.tile 

419 self._seq_num = self.rewind_state.seq_num 

420 

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

422 # ICC profile 

423 assert self.fp is not None 

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

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

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

427 # Null separator 1 byte (null character) 

428 # Compression method 1 byte (0) 

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

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

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

432 comp_method = s[i + 1] 

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

434 if comp_method != 0: 

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

436 raise SyntaxError(msg) 

437 try: 

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

439 except ValueError: 

440 if ImageFile.LOAD_TRUNCATED_IMAGES: 

441 icc_profile = None 

442 else: 

443 raise 

444 except zlib.error: 

445 icc_profile = None # FIXME 

446 self.im_info["icc_profile"] = icc_profile 

447 return s 

448 

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

450 # image header 

451 assert self.fp is not None 

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

453 if length < 13: 

454 if ImageFile.LOAD_TRUNCATED_IMAGES: 

455 return s 

456 msg = "Truncated IHDR chunk" 

457 raise ValueError(msg) 

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

459 try: 

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

461 except Exception: 

462 pass 

463 if s[12]: 

464 self.im_info["interlace"] = 1 

465 if s[11]: 

466 msg = "unknown filter category" 

467 raise SyntaxError(msg) 

468 return s 

469 

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

471 # image data 

472 if "bbox" in self.im_info: 

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

474 else: 

475 if self.im_n_frames is not None: 

476 self.im_info["default_image"] = True 

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

478 self.im_tile = tile 

479 self.im_idat = length 

480 msg = "image data found" 

481 raise EOFError(msg) 

482 

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

484 msg = "end of PNG image" 

485 raise EOFError(msg) 

486 

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

488 # palette 

489 assert self.fp is not None 

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

491 if self.im_mode == "P": 

492 self.im_palette = "RGB", s 

493 return s 

494 

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

496 # transparency 

497 assert self.fp is not None 

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

499 if self.im_mode == "P": 

500 if _simple_palette.match(s): 

501 # tRNS contains only one full-transparent entry, 

502 # other entries are full opaque 

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

504 if i >= 0: 

505 self.im_info["transparency"] = i 

506 else: 

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

508 # for each palette entry 

509 self.im_info["transparency"] = s 

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

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

512 elif self.im_mode == "RGB": 

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

514 return s 

515 

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

517 # gamma setting 

518 assert self.fp is not None 

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

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

521 return s 

522 

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

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

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

526 

527 assert self.fp is not None 

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

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

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

531 return s 

532 

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

534 # srgb rendering intent, 1 byte 

535 # 0 perceptual 

536 # 1 relative colorimetric 

537 # 2 saturation 

538 # 3 absolute colorimetric 

539 

540 assert self.fp is not None 

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

542 if length < 1: 

543 if ImageFile.LOAD_TRUNCATED_IMAGES: 

544 return s 

545 msg = "Truncated sRGB chunk" 

546 raise ValueError(msg) 

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

548 return s 

549 

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

551 # pixels per unit 

552 assert self.fp is not None 

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

554 if length < 9: 

555 if ImageFile.LOAD_TRUNCATED_IMAGES: 

556 return s 

557 msg = "Truncated pHYs chunk" 

558 raise ValueError(msg) 

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

560 unit = s[8] 

561 if unit == 1: # meter 

562 dpi = px * 0.0254, py * 0.0254 

563 self.im_info["dpi"] = dpi 

564 elif unit == 0: 

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

566 return s 

567 

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

569 # text 

570 assert self.fp is not None 

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

572 try: 

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

574 except ValueError: 

575 # fallback for broken tEXt tags 

576 k = s 

577 v = b"" 

578 if k: 

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

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

581 

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

583 self.im_text[k_str] = v_str 

584 self.check_text_memory(len(v_str)) 

585 

586 return s 

587 

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

589 # compressed text 

590 assert self.fp is not None 

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

592 try: 

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

594 except ValueError: 

595 k = s 

596 v = b"" 

597 if v: 

598 comp_method = v[0] 

599 else: 

600 comp_method = 0 

601 if comp_method != 0: 

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

603 raise SyntaxError(msg) 

604 try: 

605 v = _safe_zlib_decompress(v[1:]) 

606 except ValueError: 

607 if ImageFile.LOAD_TRUNCATED_IMAGES: 

608 v = b"" 

609 else: 

610 raise 

611 except zlib.error: 

612 v = b"" 

613 

614 if k: 

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

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

617 

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

619 self.check_text_memory(len(v_str)) 

620 

621 return s 

622 

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

624 # international text 

625 assert self.fp is not None 

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

627 try: 

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

629 except ValueError: 

630 return s 

631 if len(r) < 2: 

632 return s 

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

634 try: 

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

636 except ValueError: 

637 return s 

638 if cf != 0: 

639 if cm == 0: 

640 try: 

641 v = _safe_zlib_decompress(v) 

642 except ValueError: 

643 if ImageFile.LOAD_TRUNCATED_IMAGES: 

644 return s 

645 else: 

646 raise 

647 except zlib.error: 

648 return s 

649 else: 

650 return s 

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

652 self.im_info["xmp"] = v 

653 try: 

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

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

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

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

658 except UnicodeError: 

659 return s 

660 

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

662 self.check_text_memory(len(v_str)) 

663 

664 return s 

665 

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

667 assert self.fp is not None 

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

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

670 return s 

671 

672 # APNG chunks 

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

674 assert self.fp is not None 

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

676 if length < 8: 

677 if ImageFile.LOAD_TRUNCATED_IMAGES: 

678 return s 

679 msg = "APNG contains truncated acTL chunk" 

680 raise ValueError(msg) 

681 if self.im_n_frames is not None: 

682 self.im_n_frames = None 

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

684 return s 

685 n_frames = i32(s) 

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

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

688 return s 

689 self.im_n_frames = n_frames 

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

691 self.im_custom_mimetype = "image/apng" 

692 return s 

693 

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

695 assert self.fp is not None 

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

697 if length < 26: 

698 if ImageFile.LOAD_TRUNCATED_IMAGES: 

699 return s 

700 msg = "APNG contains truncated fcTL chunk" 

701 raise ValueError(msg) 

702 seq = i32(s) 

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

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

705 ): 

706 msg = "APNG contains frame sequence errors" 

707 raise SyntaxError(msg) 

708 self._seq_num = seq 

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

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

711 im_w, im_h = self.im_size 

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

713 msg = "APNG contains invalid frames" 

714 raise SyntaxError(msg) 

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

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

717 if delay_den == 0: 

718 delay_den = 100 

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

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

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

722 return s 

723 

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

725 assert self.fp is not None 

726 if length < 4: 

727 if ImageFile.LOAD_TRUNCATED_IMAGES: 

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

729 return s 

730 msg = "APNG contains truncated fDAT chunk" 

731 raise ValueError(msg) 

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

733 seq = i32(s) 

734 if self._seq_num != seq - 1: 

735 msg = "APNG contains frame sequence errors" 

736 raise SyntaxError(msg) 

737 self._seq_num = seq 

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

739 

740 

741# -------------------------------------------------------------------- 

742# PNG reader 

743 

744 

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

746 return prefix.startswith(_MAGIC) 

747 

748 

749## 

750# Image plugin for PNG images. 

751 

752 

753class PngImageFile(ImageFile.ImageFile): 

754 format = "PNG" 

755 format_description = "Portable network graphics" 

756 

757 def _open(self) -> None: 

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

759 msg = "not a PNG file" 

760 raise SyntaxError(msg) 

761 self._fp = self.fp 

762 self.__frame = 0 

763 

764 # 

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

766 

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

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

769 

770 while True: 

771 # 

772 # get next chunk 

773 

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

775 

776 try: 

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

778 except EOFError: 

779 break 

780 except AttributeError: 

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

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

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

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

785 

786 self.png.crc(cid, s) 

787 

788 # 

789 # Copy relevant attributes from the PngStream. An alternative 

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

791 # directly, but that introduces circular references which are 

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

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

794 

795 self._mode = self.png.im_mode 

796 self._size = self.png.im_size 

797 self.info = self.png.im_info 

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

799 self.tile = self.png.im_tile 

800 self.custom_mimetype = self.png.im_custom_mimetype 

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

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

803 

804 if self.png.im_palette: 

805 rawmode, data = self.png.im_palette 

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

807 

808 if cid == b"fdAT": 

809 self.__prepare_idat = length - 4 

810 else: 

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

812 

813 if self.png.im_n_frames is not None: 

814 self._close_exclusive_fp_after_loading = False 

815 self.png.save_rewind() 

816 self.__rewind_idat = self.__prepare_idat 

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

818 if self.default_image: 

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

820 self.n_frames += 1 

821 self._seek(0) 

822 self.is_animated = self.n_frames > 1 

823 

824 @property 

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

826 # experimental 

827 if self._text is None: 

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

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

830 if self.is_animated: 

831 frame = self.__frame 

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

833 self.seek(self.n_frames - 1) 

834 self.load() 

835 if self.is_animated: 

836 self.seek(frame) 

837 assert self._text is not None 

838 return self._text 

839 

840 def verify(self) -> None: 

841 """Verify PNG file""" 

842 

843 if self.fp is None: 

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

845 raise RuntimeError(msg) 

846 

847 # back up to beginning of IDAT block 

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

849 

850 assert self.png is not None 

851 self.png.verify() 

852 self.png.close() 

853 

854 if self._exclusive_fp: 

855 self.fp.close() 

856 self.fp = None 

857 

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

859 if not self._seek_check(frame): 

860 return 

861 if frame < self.__frame: 

862 self._seek(0, True) 

863 

864 last_frame = self.__frame 

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

866 try: 

867 self._seek(f) 

868 except EOFError as e: 

869 self.seek(last_frame) 

870 msg = "no more images in APNG file" 

871 raise EOFError(msg) from e 

872 

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

874 assert self.png is not None 

875 if isinstance(self._fp, DeferredError): 

876 raise self._fp.ex 

877 

878 self.dispose: _imaging.ImagingCore | None 

879 dispose_extent = None 

880 if frame == 0: 

881 if rewind: 

882 self._fp.seek(self.__rewind) 

883 self.png.rewind() 

884 self.__prepare_idat = self.__rewind_idat 

885 self._im = None 

886 self.info = self.png.im_info 

887 self.tile = self.png.im_tile 

888 self.fp = self._fp 

889 self._prev_im = None 

890 self.dispose = None 

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

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

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

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

895 self.__frame = 0 

896 else: 

897 if frame != self.__frame + 1: 

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

899 raise ValueError(msg) 

900 

901 # ensure previous frame was loaded 

902 self.load() 

903 

904 if self.dispose: 

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

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

907 

908 self.fp = self._fp 

909 

910 # advance to the next frame 

911 if self.__prepare_idat: 

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

913 self.__prepare_idat = 0 

914 frame_start = False 

915 while True: 

916 self.fp.read(4) # CRC 

917 

918 try: 

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

920 except (struct.error, SyntaxError): 

921 break 

922 

923 if cid == b"IEND": 

924 msg = "No more images in APNG file" 

925 raise EOFError(msg) 

926 if cid == b"fcTL": 

927 if frame_start: 

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

929 msg = "APNG missing frame data" 

930 raise SyntaxError(msg) 

931 frame_start = True 

932 

933 try: 

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

935 except UnicodeDecodeError: 

936 break 

937 except EOFError: 

938 if cid == b"fdAT": 

939 length -= 4 

940 if frame_start: 

941 self.__prepare_idat = length 

942 break 

943 ImageFile._safe_read(self.fp, length) 

944 except AttributeError: 

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

946 ImageFile._safe_read(self.fp, length) 

947 

948 self.__frame = frame 

949 self.tile = self.png.im_tile 

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

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

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

953 

954 if not self.tile: 

955 msg = "image not found in APNG frame" 

956 raise EOFError(msg) 

957 if dispose_extent: 

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

959 

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

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

962 self.dispose_op = Disposal.OP_BACKGROUND 

963 

964 self.dispose = None 

965 if self.dispose_op == Disposal.OP_PREVIOUS: 

966 if self._prev_im: 

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

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

969 elif self.dispose_op == Disposal.OP_BACKGROUND: 

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

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

972 

973 def tell(self) -> int: 

974 return self.__frame 

975 

976 def load_prepare(self) -> None: 

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

978 

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

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

981 

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

983 ImageFile.ImageFile.load_prepare(self) 

984 

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

986 """internal: read more image data""" 

987 

988 assert self.png is not None 

989 while self.__idat == 0: 

990 # end of chunk, skip forward to next one 

991 

992 self.fp.read(4) # CRC 

993 

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

995 

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

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

998 return b"" 

999 

1000 if cid == b"fdAT": 

1001 try: 

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

1003 except EOFError: 

1004 pass 

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

1006 else: 

1007 self.__idat = length # empty chunks are allowed 

1008 

1009 # read more data from this chunk 

1010 if read_bytes <= 0: 

1011 read_bytes = self.__idat 

1012 else: 

1013 read_bytes = min(read_bytes, self.__idat) 

1014 

1015 self.__idat = self.__idat - read_bytes 

1016 

1017 return self.fp.read(read_bytes) 

1018 

1019 def load_end(self) -> None: 

1020 """internal: finished reading image data""" 

1021 assert self.png is not None 

1022 if self.__idat != 0: 

1023 self.fp.read(self.__idat) 

1024 while True: 

1025 self.fp.read(4) # CRC 

1026 

1027 try: 

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

1029 except (struct.error, SyntaxError): 

1030 break 

1031 

1032 if cid == b"IEND": 

1033 break 

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

1035 # start of the next frame, stop reading 

1036 self.__prepare_idat = 0 

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

1038 break 

1039 

1040 try: 

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

1042 except UnicodeDecodeError: 

1043 break 

1044 except EOFError: 

1045 if cid == b"fdAT": 

1046 length -= 4 

1047 try: 

1048 ImageFile._safe_read(self.fp, length) 

1049 except OSError as e: 

1050 if ImageFile.LOAD_TRUNCATED_IMAGES: 

1051 break 

1052 else: 

1053 raise e 

1054 except AttributeError: 

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

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

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

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

1059 self._text = self.png.im_text 

1060 if not self.is_animated: 

1061 self.png.close() 

1062 self.png = None 

1063 else: 

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

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

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

1067 mask = updated.convert_transparent( 

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

1069 ) 

1070 else: 

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

1072 t = self.info["transparency"] 

1073 if isinstance(t, bytes): 

1074 updated.putpalettealphas(t) 

1075 elif isinstance(t, int): 

1076 updated.putpalettealpha(t) 

1077 mask = updated.convert("RGBA") 

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

1079 self.im = self._prev_im 

1080 

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

1082 if "exif" not in self.info: 

1083 self.load() 

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

1085 return None 

1086 return self.getexif()._get_merged_dict() 

1087 

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

1089 if "exif" not in self.info: 

1090 self.load() 

1091 

1092 return super().getexif() 

1093 

1094 

1095# -------------------------------------------------------------------- 

1096# PNG writer 

1097 

1098_OUTMODES = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1115} 

1116 

1117 

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

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

1120 

1121 byte_data = b"".join(data) 

1122 

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

1124 fp.write(byte_data) 

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

1126 fp.write(o32(crc)) 

1127 

1128 

1129class _idat: 

1130 # wrap output from the encoder in IDAT chunks 

1131 

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

1133 self.fp = fp 

1134 self.chunk = chunk 

1135 

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

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

1138 

1139 

1140class _fdat: 

1141 # wrap encoder output in fdAT chunks 

1142 

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

1144 self.fp = fp 

1145 self.chunk = chunk 

1146 self.seq_num = seq_num 

1147 

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

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

1150 self.seq_num += 1 

1151 

1152 

1153class _Frame(NamedTuple): 

1154 im: Image.Image 

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

1156 encoderinfo: dict[str, Any] 

1157 

1158 

1159def _write_multiple_frames( 

1160 im: Image.Image, 

1161 fp: IO[bytes], 

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

1163 mode: str, 

1164 rawmode: str, 

1165 default_image: Image.Image | None, 

1166 append_images: list[Image.Image], 

1167) -> Image.Image | None: 

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

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

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

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

1172 

1173 if default_image: 

1174 chain = itertools.chain(append_images) 

1175 else: 

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

1177 

1178 im_frames: list[_Frame] = [] 

1179 frame_count = 0 

1180 for im_seq in chain: 

1181 for im_frame in ImageSequence.Iterator(im_seq): 

1182 if im_frame.mode == mode: 

1183 im_frame = im_frame.copy() 

1184 else: 

1185 im_frame = im_frame.convert(mode) 

1186 encoderinfo = im.encoderinfo.copy() 

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

1188 encoderinfo["duration"] = duration[frame_count] 

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

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

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

1192 encoderinfo["disposal"] = disposal[frame_count] 

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

1194 encoderinfo["blend"] = blend[frame_count] 

1195 frame_count += 1 

1196 

1197 if im_frames: 

1198 previous = im_frames[-1] 

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

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

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

1202 prev_disposal = Disposal.OP_BACKGROUND 

1203 

1204 if prev_disposal == Disposal.OP_BACKGROUND: 

1205 base_im = previous.im.copy() 

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

1207 bbox = previous.bbox 

1208 if bbox: 

1209 dispose = dispose.crop(bbox) 

1210 else: 

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

1212 base_im.paste(dispose, bbox) 

1213 elif prev_disposal == Disposal.OP_PREVIOUS: 

1214 base_im = im_frames[-2].im 

1215 else: 

1216 base_im = previous.im 

1217 delta = ImageChops.subtract_modulo( 

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

1219 ) 

1220 bbox = delta.getbbox(alpha_only=False) 

1221 if ( 

1222 not bbox 

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

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

1225 and "duration" in encoderinfo 

1226 ): 

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

1228 continue 

1229 else: 

1230 bbox = None 

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

1232 

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

1234 return im_frames[0].im 

1235 

1236 # animation control 

1237 chunk( 

1238 fp, 

1239 b"acTL", 

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

1241 o32(loop), # 4: num_plays 

1242 ) 

1243 

1244 # default image IDAT (if it exists) 

1245 if default_image: 

1246 if im.mode != mode: 

1247 im = im.convert(mode) 

1248 ImageFile._save( 

1249 im, 

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

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

1252 ) 

1253 

1254 seq_num = 0 

1255 for frame, frame_data in enumerate(im_frames): 

1256 im_frame = frame_data.im 

1257 if not frame_data.bbox: 

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

1259 else: 

1260 bbox = frame_data.bbox 

1261 im_frame = im_frame.crop(bbox) 

1262 size = im_frame.size 

1263 encoderinfo = frame_data.encoderinfo 

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

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

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

1267 # frame control 

1268 chunk( 

1269 fp, 

1270 b"fcTL", 

1271 o32(seq_num), # sequence_number 

1272 o32(size[0]), # width 

1273 o32(size[1]), # height 

1274 o32(bbox[0]), # x_offset 

1275 o32(bbox[1]), # y_offset 

1276 o16(frame_duration), # delay_numerator 

1277 o16(1000), # delay_denominator 

1278 o8(frame_disposal), # dispose_op 

1279 o8(frame_blend), # blend_op 

1280 ) 

1281 seq_num += 1 

1282 # frame data 

1283 if frame == 0 and not default_image: 

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

1285 ImageFile._save( 

1286 im_frame, 

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

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

1289 ) 

1290 else: 

1291 fdat_chunks = _fdat(fp, chunk, seq_num) 

1292 ImageFile._save( 

1293 im_frame, 

1294 cast(IO[bytes], fdat_chunks), 

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

1296 ) 

1297 seq_num = fdat_chunks.seq_num 

1298 return None 

1299 

1300 

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

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

1303 

1304 

1305def _save( 

1306 im: Image.Image, 

1307 fp: IO[bytes], 

1308 filename: str | bytes, 

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

1310 save_all: bool = False, 

1311) -> None: 

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

1313 

1314 if save_all: 

1315 default_image = im.encoderinfo.get( 

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

1317 ) 

1318 modes = set() 

1319 sizes = set() 

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

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

1322 for im_frame in ImageSequence.Iterator(im_seq): 

1323 modes.add(im_frame.mode) 

1324 sizes.add(im_frame.size) 

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

1326 if mode in modes: 

1327 break 

1328 else: 

1329 mode = modes.pop() 

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

1331 else: 

1332 size = im.size 

1333 mode = im.mode 

1334 

1335 outmode = mode 

1336 if mode == "P": 

1337 # 

1338 # attempt to minimize storage requirements for palette images 

1339 if "bits" in im.encoderinfo: 

1340 # number of bits specified by user 

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

1342 else: 

1343 # check palette contents 

1344 if im.palette: 

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

1346 else: 

1347 colors = 256 

1348 

1349 if colors <= 16: 

1350 if colors <= 2: 

1351 bits = 1 

1352 elif colors <= 4: 

1353 bits = 2 

1354 else: 

1355 bits = 4 

1356 outmode += f";{bits}" 

1357 

1358 # encoder options 

1359 im.encoderconfig = ( 

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

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

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

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

1364 ) 

1365 

1366 # get the corresponding PNG mode 

1367 try: 

1368 rawmode, bit_depth, color_type = _OUTMODES[outmode] 

1369 except KeyError as e: 

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

1371 raise OSError(msg) from e 

1372 if outmode == "I": 

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

1374 

1375 # 

1376 # write minimal PNG file 

1377 

1378 fp.write(_MAGIC) 

1379 

1380 chunk( 

1381 fp, 

1382 b"IHDR", 

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

1384 o32(size[1]), 

1385 bit_depth, 

1386 color_type, 

1387 b"\0", # 10: compression 

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

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

1390 ) 

1391 

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

1393 

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

1395 if icc: 

1396 # ICC profile 

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

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

1399 # Null separator 1 byte (null character) 

1400 # Compression method 1 byte (0) 

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

1402 name = b"ICC Profile" 

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

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

1405 

1406 # You must either have sRGB or iCCP. 

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

1408 chunks.remove(b"sRGB") 

1409 

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

1411 if info: 

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

1413 for info_chunk in info.chunks: 

1414 cid, data = info_chunk[:2] 

1415 if cid in chunks: 

1416 chunks.remove(cid) 

1417 chunk(fp, cid, data) 

1418 elif cid in chunks_multiple_allowed: 

1419 chunk(fp, cid, data) 

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

1421 # Private chunk 

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

1423 if not after_idat: 

1424 chunk(fp, cid, data) 

1425 

1426 if im.mode == "P": 

1427 palette_byte_number = colors * 3 

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

1429 while len(palette_bytes) < palette_byte_number: 

1430 palette_bytes += b"\0" 

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

1432 

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

1434 

1435 if transparency or transparency == 0: 

1436 if im.mode == "P": 

1437 # limit to actual palette size 

1438 alpha_bytes = colors 

1439 if isinstance(transparency, bytes): 

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

1441 else: 

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

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

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

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

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

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

1448 elif im.mode == "RGB": 

1449 red, green, blue = transparency 

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

1451 else: 

1452 if "transparency" in im.encoderinfo: 

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

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

1455 msg = "cannot use transparency for this mode" 

1456 raise OSError(msg) 

1457 else: 

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

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

1460 alpha_bytes = colors 

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

1462 

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

1464 if dpi: 

1465 chunk( 

1466 fp, 

1467 b"pHYs", 

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

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

1470 b"\x01", 

1471 ) 

1472 

1473 if info: 

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

1475 for info_chunk in info.chunks: 

1476 cid, data = info_chunk[:2] 

1477 if cid in chunks: 

1478 chunks.remove(cid) 

1479 chunk(fp, cid, data) 

1480 

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

1482 if exif: 

1483 if isinstance(exif, Image.Exif): 

1484 exif = exif.tobytes(8) 

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

1486 exif = exif[6:] 

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

1488 

1489 single_im: Image.Image | None = im 

1490 if save_all: 

1491 single_im = _write_multiple_frames( 

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

1493 ) 

1494 if single_im: 

1495 ImageFile._save( 

1496 single_im, 

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

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

1499 ) 

1500 

1501 if info: 

1502 for info_chunk in info.chunks: 

1503 cid, data = info_chunk[:2] 

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

1505 # Private chunk 

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

1507 if after_idat: 

1508 chunk(fp, cid, data) 

1509 

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

1511 

1512 if hasattr(fp, "flush"): 

1513 fp.flush() 

1514 

1515 

1516# -------------------------------------------------------------------- 

1517# PNG chunk converter 

1518 

1519 

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

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

1522 from io import BytesIO 

1523 

1524 chunks = [] 

1525 

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

1527 byte_data = b"".join(data) 

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

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

1530 

1531 fp = BytesIO() 

1532 

1533 try: 

1534 im.encoderinfo = params 

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

1536 finally: 

1537 del im.encoderinfo 

1538 

1539 return chunks 

1540 

1541 

1542# -------------------------------------------------------------------- 

1543# Registry 

1544 

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

1546Image.register_save(PngImageFile.format, _save) 

1547Image.register_save_all(PngImageFile.format, _save_all) 

1548 

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

1550 

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