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

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

983 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 

35__lazy_modules__ = { 

36 "PIL._binary", 

37 "PIL._util", 

38 "fractions", 

39 "itertools", 

40 "struct", 

41 "warnings", 

42 "zlib", 

43} 

44 

45import itertools 

46import logging 

47import re 

48import struct 

49import warnings 

50import zlib 

51from enum import IntEnum 

52from fractions import Fraction 

53from typing import IO, NamedTuple, cast 

54 

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

56from ._binary import i16be as i16 

57from ._binary import i32be as i32 

58from ._binary import o8 

59from ._binary import o16be as o16 

60from ._binary import o32be as o32 

61from ._util import DeferredError 

62 

63TYPE_CHECKING = False 

64if TYPE_CHECKING: 

65 from collections.abc import Callable 

66 from typing import Any, NoReturn, Self 

67 

68 from . import _imaging 

69 

70logger = logging.getLogger(__name__) 

71 

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

73 

74 

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

76 

77 

78_MODES = { 

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

80 # Grayscale 

81 (1, 0): ("1", "1"), 

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

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

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

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

86 # Truecolour 

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

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

89 # Indexed-colour 

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

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

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

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

94 # Grayscale with alpha 

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

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

97 # Truecolour with alpha 

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

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

100} 

101 

102 

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

104 

105MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK 

106""" 

107Maximum decompressed size for a iTXt or zTXt chunk. 

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

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

110""" 

111MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK 

112""" 

113Set the maximum total text chunk size. 

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

115""" 

116 

117 

118# APNG frame disposal modes 

119class Disposal(IntEnum): 

120 OP_NONE = 0 

121 """ 

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

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

124 """ 

125 OP_BACKGROUND = 1 

126 """ 

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

128 the next frame. 

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

130 """ 

131 OP_PREVIOUS = 2 

132 """ 

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

134 rendering the next frame. 

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

136 """ 

137 

138 

139# APNG frame blend modes 

140class Blend(IntEnum): 

141 OP_SOURCE = 0 

142 """ 

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

144 image contents. 

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

146 """ 

147 OP_OVER = 1 

148 """ 

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

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

151 """ 

152 

153 

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

155 dobj = zlib.decompressobj() 

156 plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) 

157 if dobj.unconsumed_tail: 

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

159 raise ValueError(msg) 

160 return plaintext 

161 

162 

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

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

165 

166 

167# -------------------------------------------------------------------- 

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

169 

170 

171class ChunkStream: 

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

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

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

175 

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

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

178 cid = None 

179 

180 assert self.fp is not None 

181 if self.queue: 

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

183 self.fp.seek(pos) 

184 else: 

185 s = self.fp.read(8) 

186 cid = s[4:] 

187 pos = self.fp.tell() 

188 length = i32(s) 

189 

190 if not is_cid(cid): 

191 if not ImageFile.LOAD_TRUNCATED_IMAGES: 

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

193 raise SyntaxError(msg) 

194 

195 return cid, pos, length 

196 

197 def __enter__(self) -> Self: 

198 return self 

199 

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

201 self.close() 

202 

203 def close(self) -> None: 

204 self.queue = self.fp = None 

205 

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

207 assert self.queue is not None 

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

209 

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

211 """Call the appropriate chunk handler""" 

212 

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

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

215 

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

217 """Read and verify checksum""" 

218 

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

220 # images 

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

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

223 self.crc_skip(cid, data) 

224 return 

225 

226 assert self.fp is not None 

227 try: 

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

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

230 if crc1 != crc2: 

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

232 raise SyntaxError(msg) 

233 except struct.error as e: 

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

235 raise SyntaxError(msg) from e 

236 

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

238 """Read checksum""" 

239 

240 assert self.fp is not None 

241 self.fp.read(4) 

242 

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

244 # Simple approach; just calculate checksum for all remaining 

245 # blocks. Must be called directly after open. 

246 

247 cids = [] 

248 

249 assert self.fp is not None 

250 while True: 

251 try: 

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

253 except struct.error as e: 

254 msg = "truncated PNG file" 

255 raise OSError(msg) from e 

256 

257 if cid == endchunk: 

258 break 

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

260 cids.append(cid) 

261 

262 return cids 

263 

264 

265class iTXt(str): 

266 """ 

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

268 keeping their extra information 

269 

270 """ 

271 

272 lang: str | bytes | None 

273 tkey: str | bytes | None 

274 

275 @staticmethod 

276 def __new__( 

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

278 ) -> Self: 

279 """ 

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

281 :param text: value for this key 

282 :param lang: language code 

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

284 """ 

285 

286 self = str.__new__(cls, text) 

287 self.lang = lang 

288 self.tkey = tkey 

289 return self 

290 

291 

292class PngInfo: 

293 """ 

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

295 

296 """ 

297 

298 def __init__(self) -> None: 

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

300 

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

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

303 

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

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

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

307 should be written after IDAT 

308 

309 """ 

310 

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

312 

313 def _check_text_key(self, key: bytes) -> None: 

314 if not 1 <= len(key) <= 79: 

315 msg = "Text key must be 1 to 79 characters long" 

316 raise ValueError(msg) 

317 

318 if b"\0" in key: 

319 # The NULL character is the separator after the key 

320 msg = "Text key cannot contain null characters" 

321 raise ValueError(msg) 

322 

323 def add_itxt( 

324 self, 

325 key: str | bytes, 

326 value: str | bytes, 

327 lang: str | bytes = "", 

328 tkey: str | bytes = "", 

329 zip: bool = False, 

330 ) -> None: 

331 """Appends an iTXt chunk. 

332 

333 :param key: latin-1 encodable text key name. 1 to 79 characters long 

334 :param value: value for this key 

335 :param lang: language code 

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

337 :param zip: compression flag 

338 

339 """ 

340 

341 if not isinstance(key, bytes): 

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

343 self._check_text_key(key) 

344 if not isinstance(value, bytes): 

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

346 if not isinstance(lang, bytes): 

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

348 if not isinstance(tkey, bytes): 

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

350 

351 if zip: 

352 self.add( 

353 b"iTXt", 

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

355 ) 

356 else: 

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

358 

359 def add_text( 

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

361 ) -> None: 

362 """Appends a text chunk. 

363 

364 :param key: latin-1 encodable text key name. 1 to 79 characters long 

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

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

367 :param zip: compression flag 

368 

369 """ 

370 if isinstance(value, iTXt): 

371 return self.add_itxt( 

372 key, 

373 value, 

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

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

376 zip=zip, 

377 ) 

378 

379 # The tEXt chunk stores latin-1 text 

380 if not isinstance(value, bytes): 

381 try: 

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

383 except UnicodeError: 

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

385 

386 if not isinstance(key, bytes): 

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

388 self._check_text_key(key) 

389 

390 if zip: 

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

392 else: 

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

394 

395 

396# -------------------------------------------------------------------- 

397# PNG image stream (IHDR/IEND) 

398 

399 

400class _RewindState(NamedTuple): 

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

402 tile: list[ImageFile._Tile] 

403 seq_num: int | None 

404 

405 

406class PngStream(ChunkStream): 

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

408 super().__init__(fp) 

409 

410 # local copies of Image attributes 

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

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

413 self.im_size = (0, 0) 

414 self.im_mode = "" 

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

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

417 self.im_custom_mimetype: str | None = None 

418 self.im_n_frames: int | None = None 

419 self._seq_num: int | None = None 

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

421 

422 self.text_memory = 0 

423 

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

425 self.text_memory += chunklen 

426 if self.text_memory > MAX_TEXT_MEMORY: 

427 msg = ( 

428 "Too much memory used in text chunks: " 

429 f"{self.text_memory}>MAX_TEXT_MEMORY" 

430 ) 

431 raise ValueError(msg) 

432 

433 def save_rewind(self) -> None: 

434 self.rewind_state = _RewindState( 

435 self.im_info.copy(), 

436 self.im_tile, 

437 self._seq_num, 

438 ) 

439 

440 def rewind(self) -> None: 

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

442 self.im_tile = self.rewind_state.tile 

443 self._seq_num = self.rewind_state.seq_num 

444 

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

446 # ICC profile 

447 assert self.fp is not None 

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

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

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

451 # Null separator 1 byte (null character) 

452 # Compression method 1 byte (0) 

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

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

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

456 comp_method = s[i + 1] 

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

458 if comp_method != 0: 

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

460 raise SyntaxError(msg) 

461 try: 

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

463 except ValueError: 

464 if ImageFile.LOAD_TRUNCATED_IMAGES: 

465 icc_profile = None 

466 else: 

467 raise 

468 except zlib.error: 

469 icc_profile = None # FIXME 

470 self.im_info["icc_profile"] = icc_profile 

471 return s 

472 

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

474 # image header 

475 assert self.fp is not None 

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

477 if length < 13: 

478 if ImageFile.LOAD_TRUNCATED_IMAGES: 

479 return s 

480 msg = "Truncated IHDR chunk" 

481 raise ValueError(msg) 

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

483 try: 

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

485 except KeyError: 

486 pass 

487 if s[12]: 

488 self.im_info["interlace"] = 1 

489 if s[11]: 

490 msg = "unknown filter category" 

491 raise SyntaxError(msg) 

492 return s 

493 

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

495 # image data 

496 if "bbox" in self.im_info: 

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

498 else: 

499 if self.im_n_frames is not None: 

500 self.im_info["default_image"] = True 

501 tile = [ImageFile._Tile("zip", None, pos, self.im_rawmode)] 

502 self.im_tile = tile 

503 self.im_idat = length 

504 msg = "image data found" 

505 raise EOFError(msg) 

506 

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

508 msg = "end of PNG image" 

509 raise EOFError(msg) 

510 

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

512 # palette 

513 assert self.fp is not None 

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

515 if self.im_mode == "P": 

516 self.im_palette = "RGB", s 

517 return s 

518 

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

520 # transparency 

521 assert self.fp is not None 

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

523 if self.im_mode == "P": 

524 if _simple_palette.match(s): 

525 # tRNS contains only one full-transparent entry, 

526 # other entries are full opaque 

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

528 if i >= 0: 

529 self.im_info["transparency"] = i 

530 else: 

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

532 # for each palette entry 

533 self.im_info["transparency"] = s 

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

535 # 2 bytes for greyscale, 6 for truecolour 

536 if length < (6 if self.im_mode == "RGB" else 2): 

537 if ImageFile.LOAD_TRUNCATED_IMAGES: 

538 return s 

539 msg = "Truncated tRNS chunk" 

540 raise ValueError(msg) 

541 if self.im_mode == "1": 

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

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

544 value = i16(s) 

545 if self.im_rawmode in {"L;2", "L;4"}: 

546 # Scale the grey level to match the unpacked 8-bit pixels 

547 max_value = (1 << int(self.im_rawmode[-1])) - 1 

548 value = int(min(1, value / max_value) * 255) 

549 self.im_info["transparency"] = value 

550 elif self.im_mode == "RGB": 

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

552 return s 

553 

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

555 # gamma setting 

556 assert self.fp is not None 

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

558 if length < 4: 

559 if ImageFile.LOAD_TRUNCATED_IMAGES: 

560 return s 

561 msg = "Truncated gAMA chunk" 

562 raise ValueError(msg) 

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

564 return s 

565 

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

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

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

569 

570 assert self.fp is not None 

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

572 if length < 32: 

573 if ImageFile.LOAD_TRUNCATED_IMAGES: 

574 return s 

575 msg = "Truncated cHRM chunk" 

576 raise ValueError(msg) 

577 raw_vals = struct.unpack(">8I", s[:32]) 

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

579 return s 

580 

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

582 # srgb rendering intent, 1 byte 

583 # 0 perceptual 

584 # 1 relative colorimetric 

585 # 2 saturation 

586 # 3 absolute colorimetric 

587 

588 assert self.fp is not None 

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

590 if length < 1: 

591 if ImageFile.LOAD_TRUNCATED_IMAGES: 

592 return s 

593 msg = "Truncated sRGB chunk" 

594 raise ValueError(msg) 

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

596 return s 

597 

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

599 # pixels per unit 

600 assert self.fp is not None 

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

602 if length < 9: 

603 if ImageFile.LOAD_TRUNCATED_IMAGES: 

604 return s 

605 msg = "Truncated pHYs chunk" 

606 raise ValueError(msg) 

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

608 unit = s[8] 

609 if unit == 1: # meter 

610 dpi = px * 0.0254, py * 0.0254 

611 self.im_info["dpi"] = dpi 

612 elif unit == 0: 

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

614 return s 

615 

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

617 # text 

618 assert self.fp is not None 

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

620 try: 

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

622 except ValueError: 

623 # fallback for broken tEXt tags 

624 k = s 

625 v = b"" 

626 if k: 

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

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

629 

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

631 self.im_text[k_str] = v_str 

632 self.check_text_memory(len(v_str)) 

633 

634 return s 

635 

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

637 # compressed text 

638 assert self.fp is not None 

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

640 try: 

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

642 except ValueError: 

643 k = s 

644 v = b"" 

645 if v: 

646 comp_method = v[0] 

647 else: 

648 comp_method = 0 

649 if comp_method != 0: 

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

651 raise SyntaxError(msg) 

652 try: 

653 v = _safe_zlib_decompress(v[1:]) 

654 except ValueError: 

655 if ImageFile.LOAD_TRUNCATED_IMAGES: 

656 v = b"" 

657 else: 

658 raise 

659 except zlib.error: 

660 v = b"" 

661 

662 if k: 

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

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

665 

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

667 self.check_text_memory(len(v_str)) 

668 

669 return s 

670 

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

672 # international text 

673 assert self.fp is not None 

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

675 try: 

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

677 except ValueError: 

678 return s 

679 if len(r) < 2: 

680 return s 

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

682 try: 

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

684 except ValueError: 

685 return s 

686 if cf != 0: 

687 if cm == 0: 

688 try: 

689 v = _safe_zlib_decompress(v) 

690 except ValueError: 

691 if ImageFile.LOAD_TRUNCATED_IMAGES: 

692 return s 

693 else: 

694 raise 

695 except zlib.error: 

696 return s 

697 else: 

698 return s 

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

700 self.im_info["xmp"] = v 

701 try: 

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

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

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

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

706 except UnicodeError: 

707 return s 

708 

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

710 self.check_text_memory(len(v_str)) 

711 

712 return s 

713 

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

715 assert self.fp is not None 

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

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

718 return s 

719 

720 # APNG chunks 

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

722 assert self.fp is not None 

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

724 if length < 8: 

725 if ImageFile.LOAD_TRUNCATED_IMAGES: 

726 return s 

727 msg = "APNG contains truncated acTL chunk" 

728 raise ValueError(msg) 

729 if self.im_n_frames is not None: 

730 self.im_n_frames = None 

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

732 return s 

733 n_frames = i32(s) 

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

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

736 return s 

737 self.im_n_frames = n_frames 

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

739 self.im_custom_mimetype = "image/apng" 

740 return s 

741 

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

743 assert self.fp is not None 

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

745 if length < 26: 

746 if ImageFile.LOAD_TRUNCATED_IMAGES: 

747 return s 

748 msg = "APNG contains truncated fcTL chunk" 

749 raise ValueError(msg) 

750 seq = i32(s) 

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

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

753 ): 

754 msg = "APNG contains frame sequence errors" 

755 raise SyntaxError(msg) 

756 self._seq_num = seq 

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

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

759 im_w, im_h = self.im_size 

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

761 msg = "APNG contains invalid frames" 

762 raise SyntaxError(msg) 

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

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

765 if delay_den == 0: 

766 delay_den = 100 

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

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

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

770 return s 

771 

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

773 assert self.fp is not None 

774 if length < 4: 

775 if ImageFile.LOAD_TRUNCATED_IMAGES: 

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

777 return s 

778 msg = "APNG contains truncated fDAT chunk" 

779 raise ValueError(msg) 

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

781 seq = i32(s) 

782 if self._seq_num != seq - 1: 

783 msg = "APNG contains frame sequence errors" 

784 raise SyntaxError(msg) 

785 self._seq_num = seq 

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

787 

788 

789# -------------------------------------------------------------------- 

790# PNG reader 

791 

792 

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

794 return prefix.startswith(_MAGIC) 

795 

796 

797## 

798# Image plugin for PNG images. 

799 

800 

801class PngImageFile(ImageFile.ImageFile): 

802 format = "PNG" 

803 format_description = "Portable network graphics" 

804 

805 def _open(self) -> None: 

806 assert self.fp is not None 

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

808 msg = "not a PNG file" 

809 raise SyntaxError(msg) 

810 self._fp = self.fp 

811 self.__frame = 0 

812 

813 # 

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

815 

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

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

818 

819 while True: 

820 # 

821 # get next chunk 

822 

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

824 

825 try: 

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

827 except EOFError: 

828 break 

829 except AttributeError: 

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

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

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

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

834 

835 self.png.crc(cid, s) 

836 

837 # 

838 # Copy relevant attributes from the PngStream. An alternative 

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

840 # directly, but that introduces circular references which are 

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

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

843 

844 self._mode = self.png.im_mode 

845 self._size = self.png.im_size 

846 self.info = self.png.im_info 

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

848 self.tile = self.png.im_tile 

849 self.custom_mimetype = self.png.im_custom_mimetype 

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

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

852 

853 if self.png.im_palette: 

854 rawmode, data = self.png.im_palette 

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

856 

857 if cid == b"fdAT": 

858 self.__prepare_idat = length - 4 

859 else: 

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

861 

862 if self.png.im_n_frames is not None: 

863 self._close_exclusive_fp_after_loading = False 

864 self.png.save_rewind() 

865 self.__rewind_idat = self.__prepare_idat 

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

867 if self.default_image: 

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

869 self.n_frames += 1 

870 self._seek(0) 

871 self.is_animated = self.n_frames > 1 

872 

873 @property 

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

875 # experimental 

876 if self._text is None: 

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

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

879 if self.is_animated: 

880 frame = self.__frame 

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

882 self.seek(self.n_frames - 1) 

883 self.load() 

884 if self.is_animated: 

885 self.seek(frame) 

886 assert self._text is not None 

887 return self._text 

888 

889 def verify(self) -> None: 

890 """Verify PNG file""" 

891 

892 if self.fp is None: 

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

894 raise RuntimeError(msg) 

895 

896 # back up to beginning of IDAT block 

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

898 

899 assert self.png is not None 

900 self.png.verify() 

901 self.png.close() 

902 

903 super().verify() 

904 

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

906 if not self._seek_check(frame): 

907 return 

908 if frame < self.__frame: 

909 self._seek(0, True) 

910 

911 last_frame = self.__frame 

912 try: 

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

914 self._seek(f) 

915 except EOFError as e: 

916 self.seek(last_frame) 

917 msg = "no more images in APNG file" 

918 raise EOFError(msg) from e 

919 

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

921 assert self.png is not None 

922 if isinstance(self._fp, DeferredError): 

923 raise self._fp.ex 

924 

925 dispose_extent = None 

926 if frame == 0: 

927 if rewind: 

928 self._fp.seek(self.__rewind) 

929 self.png.rewind() 

930 self.__prepare_idat = self.__rewind_idat 

931 self._im = None 

932 self.info = self.png.im_info 

933 self.tile = self.png.im_tile 

934 self.fp = self._fp 

935 self._prev_im: _imaging.ImagingCore | None = None 

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

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

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

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

940 self.__frame = 0 

941 else: 

942 if frame != self.__frame + 1: 

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

944 raise ValueError(msg) 

945 

946 if self.dispose_op == Disposal.OP_BACKGROUND: 

947 x0, y0, x1, y1 = self.dispose_extent 

948 dispose = Image.core.fill(self.mode, (x1 - x0, y1 - y0)) 

949 elif self.dispose_op == Disposal.OP_PREVIOUS and self._prev_im is not None: 

950 dispose = self._crop(self._prev_im.copy(), self.dispose_extent) 

951 else: 

952 dispose = None 

953 

954 # ensure previous frame was loaded 

955 self.load() 

956 

957 if dispose: 

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

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

960 

961 self.fp = self._fp 

962 

963 # advance to the next frame 

964 if self.__prepare_idat: 

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

966 self.__prepare_idat = 0 

967 frame_start = False 

968 while True: 

969 self.fp.read(4) # CRC 

970 

971 try: 

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

973 except (struct.error, SyntaxError): 

974 break 

975 

976 if cid == b"IEND": 

977 msg = "No more images in APNG file" 

978 raise EOFError(msg) 

979 if cid == b"fcTL": 

980 if frame_start: 

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

982 msg = "APNG missing frame data" 

983 raise SyntaxError(msg) 

984 frame_start = True 

985 

986 try: 

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

988 except UnicodeDecodeError: 

989 break 

990 except EOFError: 

991 if cid == b"fdAT": 

992 length -= 4 

993 if frame_start: 

994 self.__prepare_idat = length 

995 break 

996 ImageFile._safe_read(self.fp, length) 

997 except AttributeError: 

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

999 ImageFile._safe_read(self.fp, length) 

1000 

1001 self.__frame = frame 

1002 self.tile = self.png.im_tile 

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

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

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

1006 

1007 if not self.tile: 

1008 msg = "image not found in APNG frame" 

1009 raise EOFError(msg) 

1010 if dispose_extent: 

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

1012 

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

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

1015 self.dispose_op = Disposal.OP_BACKGROUND 

1016 

1017 def tell(self) -> int: 

1018 return self.__frame 

1019 

1020 def load_prepare(self) -> None: 

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

1022 

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

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

1025 

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

1027 ImageFile.ImageFile.load_prepare(self) 

1028 

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

1030 """internal: read more image data""" 

1031 

1032 assert self.png is not None 

1033 assert self.fp is not None 

1034 while self.__idat == 0: 

1035 # end of chunk, skip forward to next one 

1036 

1037 self.fp.read(4) # CRC 

1038 

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

1040 

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

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

1043 return b"" 

1044 

1045 if cid == b"fdAT": 

1046 try: 

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

1048 except EOFError: 

1049 pass 

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

1051 else: 

1052 self.__idat = length # empty chunks are allowed 

1053 

1054 # read more data from this chunk 

1055 if read_bytes <= 0: 

1056 read_bytes = self.__idat 

1057 else: 

1058 read_bytes = min(read_bytes, self.__idat) 

1059 

1060 self.__idat = self.__idat - read_bytes 

1061 

1062 return self.fp.read(read_bytes) 

1063 

1064 def load_end(self) -> None: 

1065 """internal: finished reading image data""" 

1066 assert self.png is not None 

1067 assert self.fp is not None 

1068 if self.__idat != 0: 

1069 self.fp.read(self.__idat) 

1070 while True: 

1071 self.fp.read(4) # CRC 

1072 

1073 try: 

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

1075 except (struct.error, SyntaxError): 

1076 break 

1077 

1078 if cid == b"IEND": 

1079 break 

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

1081 # start of the next frame, stop reading 

1082 self.__prepare_idat = 0 

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

1084 break 

1085 

1086 try: 

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

1088 except UnicodeDecodeError: 

1089 break 

1090 except EOFError: 

1091 if cid == b"fdAT": 

1092 length -= 4 

1093 try: 

1094 ImageFile._safe_read(self.fp, length) 

1095 except OSError as e: 

1096 if ImageFile.LOAD_TRUNCATED_IMAGES: 

1097 break 

1098 else: 

1099 raise e 

1100 except AttributeError: 

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

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

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

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

1105 self._text = self.png.im_text 

1106 if not self.is_animated: 

1107 self.png.close() 

1108 self.png = None 

1109 else: 

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

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

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

1113 mask = updated.convert_transparent( 

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

1115 ) 

1116 else: 

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

1118 t = self.info["transparency"] 

1119 if isinstance(t, bytes): 

1120 updated.putpalettealphas(t) 

1121 elif isinstance(t, int): 

1122 updated.putpalettealpha(t) 

1123 mask = updated.convert("RGBA") 

1124 if self.dispose_op == Disposal.OP_PREVIOUS: 

1125 # Keep the previous image unchanged for disposal 

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

1127 else: 

1128 self.im = self._prev_im 

1129 self.im.paste(updated, self.dispose_extent, mask) 

1130 

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

1132 if "exif" not in self.info: 

1133 self.load() 

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

1135 return None 

1136 return self.getexif()._get_merged_dict() 

1137 

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

1139 if "exif" not in self.info: 

1140 self.load() 

1141 

1142 return super().getexif() 

1143 

1144 

1145# -------------------------------------------------------------------- 

1146# PNG writer 

1147 

1148_OUTMODES = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1164} 

1165 

1166 

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

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

1169 

1170 byte_data = b"".join(data) 

1171 

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

1173 fp.write(byte_data) 

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

1175 fp.write(o32(crc)) 

1176 

1177 

1178class _idat: 

1179 # wrap output from the encoder in IDAT chunks 

1180 

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

1182 self.fp = fp 

1183 self.chunk = chunk 

1184 

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

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

1187 

1188 

1189class _fdat: 

1190 # wrap encoder output in fdAT chunks 

1191 

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

1193 self.fp = fp 

1194 self.chunk = chunk 

1195 self.seq_num = seq_num 

1196 

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

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

1199 self.seq_num += 1 

1200 

1201 

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

1203 im.encoderconfig = ( 

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

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

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

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

1208 ) 

1209 

1210 

1211class _Frame(NamedTuple): 

1212 im: Image.Image 

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

1214 encoderinfo: dict[str, Any] 

1215 

1216 

1217def _write_multiple_frames( 

1218 im: Image.Image, 

1219 fp: IO[bytes], 

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

1221 mode: str, 

1222 rawmode: str, 

1223 default_image: Image.Image | None, 

1224 append_images: list[Image.Image], 

1225) -> Image.Image | None: 

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

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

1228 disposal = im.encoderinfo.get("disposal") 

1229 blend = im.encoderinfo.get("blend") 

1230 

1231 if default_image: 

1232 chain = itertools.chain(append_images) 

1233 else: 

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

1235 

1236 im_frames: list[_Frame] = [] 

1237 frame_count = 0 

1238 for im_seq in chain: 

1239 for im_frame in ImageSequence.Iterator(im_seq): 

1240 if im_frame.mode == mode: 

1241 im_frame = im_frame.copy() 

1242 else: 

1243 im_frame = im_frame.convert(mode) 

1244 encoderinfo = im.encoderinfo.copy() 

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

1246 encoderinfo["duration"] = duration[frame_count] 

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

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

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

1250 encoderinfo["disposal"] = disposal[frame_count] 

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

1252 encoderinfo["blend"] = blend[frame_count] 

1253 frame_count += 1 

1254 

1255 if im_frames: 

1256 previous = im_frames[-1] 

1257 if mode.startswith("I;16"): 

1258 delta = ImageChops.subtract_modulo( 

1259 Image.frombytes("LA", im.size, im_frame.tobytes()), 

1260 Image.frombytes("LA", im.size, previous.im.tobytes()), 

1261 ) 

1262 else: 

1263 delta = ImageChops.subtract_modulo(im_frame, previous.im) 

1264 bbox = delta.getbbox(alpha_only=False) 

1265 if not bbox and "duration" in encoderinfo: 

1266 new_duration = ( 

1267 previous.encoderinfo["duration"] + encoderinfo["duration"] 

1268 ) 

1269 delay = Fraction(new_duration / 1000).limit_denominator(65535) 

1270 if delay.numerator <= 65535: 

1271 previous.encoderinfo["duration"] = new_duration 

1272 continue 

1273 else: 

1274 bbox = None 

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

1276 

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

1278 return im_frames[0].im 

1279 

1280 # animation control 

1281 chunk( 

1282 fp, 

1283 b"acTL", 

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

1285 o32(loop), # 4: num_plays 

1286 ) 

1287 

1288 # default image IDAT (if it exists) 

1289 if default_image: 

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

1291 _apply_encoderinfo(default_im, im.encoderinfo) 

1292 ImageFile._save( 

1293 default_im, 

1294 cast("IO[bytes]", _idat(fp, chunk)), 

1295 [ImageFile._Tile("zip", args=rawmode)], 

1296 ) 

1297 

1298 seq_num = 0 

1299 for frame, frame_data in enumerate(im_frames): 

1300 im_frame = frame_data.im 

1301 if not frame_data.bbox: 

1302 bbox = (0, 0, *im_frame.size) 

1303 else: 

1304 bbox = frame_data.bbox 

1305 im_frame = im_frame.crop(bbox) 

1306 size = im_frame.size 

1307 encoderinfo = frame_data.encoderinfo 

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

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

1310 if delay.numerator > 65535: 

1311 msg = "cannot write duration" 

1312 raise ValueError(msg) 

1313 frame_disposal = encoderinfo.get("disposal", Disposal.OP_NONE) 

1314 frame_blend = encoderinfo.get("blend", Blend.OP_SOURCE) 

1315 # frame control 

1316 chunk( 

1317 fp, 

1318 b"fcTL", 

1319 o32(seq_num), # sequence_number 

1320 o32(size[0]), # width 

1321 o32(size[1]), # height 

1322 o32(bbox[0]), # x_offset 

1323 o32(bbox[1]), # y_offset 

1324 o16(delay.numerator), # delay_numerator 

1325 o16(delay.denominator), # delay_denominator 

1326 o8(frame_disposal), # dispose_op 

1327 o8(frame_blend), # blend_op 

1328 ) 

1329 seq_num += 1 

1330 # frame data 

1331 _apply_encoderinfo(im_frame, im.encoderinfo) 

1332 if frame == 0 and not default_image: 

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

1334 ImageFile._save( 

1335 im_frame, 

1336 cast("IO[bytes]", _idat(fp, chunk)), 

1337 [ImageFile._Tile("zip", args=rawmode)], 

1338 ) 

1339 else: 

1340 fdat_chunks = _fdat(fp, chunk, seq_num) 

1341 ImageFile._save( 

1342 im_frame, 

1343 cast("IO[bytes]", fdat_chunks), 

1344 [ImageFile._Tile("zip", args=rawmode)], 

1345 ) 

1346 seq_num = fdat_chunks.seq_num 

1347 return None 

1348 

1349 

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

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

1352 

1353 

1354def _save( 

1355 im: Image.Image, 

1356 fp: IO[bytes], 

1357 filename: str | bytes, 

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

1359 save_all: bool = False, 

1360) -> None: 

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

1362 

1363 if save_all: 

1364 default_image = im.encoderinfo.get( 

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

1366 ) 

1367 modes = set() 

1368 sizes = set() 

1369 palette = None 

1370 palette_frame_with_alpha = None 

1371 different_palette = None 

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

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

1374 for im_frame in ImageSequence.Iterator(im_seq): 

1375 modes.add(im_frame.mode) 

1376 sizes.add(im_frame.size) 

1377 if im_frame.mode == "P": 

1378 if im_frame.palette and im_frame.palette.mode == "RGBA": 

1379 palette_frame_with_alpha = im_frame 

1380 if different_palette is None: 

1381 frame_palette = im_frame.getpalette() or [] 

1382 if palette is not None: 

1383 if frame_palette != palette: 

1384 different_palette = True 

1385 else: 

1386 palette = frame_palette 

1387 elif im_frame.mode in ("RGBA", "RGBA"): 

1388 different_palette = False 

1389 if different_palette: 

1390 # APNG can only contain a single PLTE, so use another mode. 

1391 mode = "RGBA" if palette_frame_with_alpha else "RGB" 

1392 else: 

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

1394 if mode in modes: 

1395 break 

1396 else: 

1397 mode = modes.pop() 

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

1399 else: 

1400 size = im.size 

1401 mode = im.mode 

1402 if mode == "P" and im.palette: 

1403 palette = im.getpalette() or [] 

1404 palette_frame_with_alpha = im if im.palette.mode == "RGBA" else None 

1405 

1406 outmode = mode 

1407 if mode == "P": 

1408 colors = max( 

1409 1, 

1410 min( 

1411 # number of bits specified by user 

1412 1 << im.encoderinfo.get("bits", 8), 

1413 # write only as many PLTE entries as the palette actually contains 

1414 len(palette) // 3 if palette else 0, 

1415 256, 

1416 ), 

1417 ) 

1418 if colors <= 16: 

1419 if colors <= 2: 

1420 bits = 1 

1421 elif colors <= 4: 

1422 bits = 2 

1423 else: 

1424 bits = 4 

1425 outmode += f";{bits}" 

1426 elif mode == "L" and im.encoderinfo.get("bits", 8) != 8: 

1427 outmode += f";{im.encoderinfo['bits']}" 

1428 

1429 # get the corresponding PNG mode 

1430 try: 

1431 rawmode, bit_depth, color_type = _OUTMODES[outmode] 

1432 except KeyError as e: 

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

1434 raise OSError(msg) from e 

1435 

1436 # 

1437 # write minimal PNG file 

1438 

1439 fp.write(_MAGIC) 

1440 

1441 chunk( 

1442 fp, 

1443 b"IHDR", 

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

1445 o32(size[1]), 

1446 bit_depth, 

1447 color_type, 

1448 b"\0", # 10: compression 

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

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

1451 ) 

1452 

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

1454 

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

1456 # ICC profile 

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

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

1459 # Null separator 1 byte (null character) 

1460 # Compression method 1 byte (0) 

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

1462 name = b"ICC Profile" 

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

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

1465 

1466 # You must either have sRGB or iCCP. 

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

1468 chunks.remove(b"sRGB") 

1469 

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

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

1472 for info_chunk in info.chunks: 

1473 cid, data = info_chunk[:2] 

1474 if cid in chunks: 

1475 chunks.remove(cid) 

1476 chunk(fp, cid, data) 

1477 elif cid in chunks_multiple_allowed: 

1478 chunk(fp, cid, data) 

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

1480 # Private chunk 

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

1482 if not after_idat: 

1483 chunk(fp, cid, data) 

1484 

1485 if mode == "P": 

1486 palette_bytes = (palette and bytes(palette[: colors * 3])) or b"\x00\x00\x00" 

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

1488 

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

1490 

1491 if transparency is not None: 

1492 if mode == "P": 

1493 # limit to actual palette size 

1494 alpha_bytes = colors 

1495 if isinstance(transparency, bytes): 

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

1497 elif isinstance(transparency, int): 

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

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

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

1501 else: 

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

1503 raise ValueError(msg) 

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

1505 if isinstance(transparency, int): 

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

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

1508 else: 

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

1510 raise ValueError(msg) 

1511 elif im.mode == "RGB": 

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

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

1514 raise ValueError(msg) 

1515 elif len(transparency) != 3: 

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

1517 raise ValueError(msg) 

1518 else: 

1519 red, green, blue = transparency 

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

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

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

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

1524 msg = "cannot use transparency for this mode" 

1525 raise OSError(msg) 

1526 elif mode == "P" and palette_frame_with_alpha: 

1527 alpha = palette_frame_with_alpha.im.getpalette("A") 

1528 alpha_bytes = colors 

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

1530 

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

1532 chunk( 

1533 fp, 

1534 b"pHYs", 

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

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

1537 b"\x01", 

1538 ) 

1539 

1540 if info: 

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

1542 for info_chunk in info.chunks: 

1543 cid, data = info_chunk[:2] 

1544 if cid in chunks: 

1545 chunks.remove(cid) 

1546 chunk(fp, cid, data) 

1547 

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

1549 if isinstance(exif, Image.Exif): 

1550 exif = exif.tobytes(8) 

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

1552 exif = exif[6:] 

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

1554 

1555 single_im: Image.Image | None = im 

1556 if save_all: 

1557 single_im = _write_multiple_frames( 

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

1559 ) 

1560 if single_im: 

1561 _apply_encoderinfo(single_im, im.encoderinfo) 

1562 ImageFile._save( 

1563 single_im, 

1564 cast("IO[bytes]", _idat(fp, chunk)), 

1565 [ImageFile._Tile("zip", args=rawmode)], 

1566 ) 

1567 

1568 if info: 

1569 for info_chunk in info.chunks: 

1570 cid, data = info_chunk[:2] 

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

1572 # Private chunk 

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

1574 if after_idat: 

1575 chunk(fp, cid, data) 

1576 

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

1578 

1579 if hasattr(fp, "flush"): 

1580 fp.flush() 

1581 

1582 

1583# -------------------------------------------------------------------- 

1584# PNG chunk converter 

1585 

1586 

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

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

1589 from io import BytesIO 

1590 

1591 chunks = [] 

1592 

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

1594 byte_data = b"".join(data) 

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

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

1597 

1598 fp = BytesIO() 

1599 

1600 try: 

1601 im.encoderinfo = params 

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

1603 finally: 

1604 del im.encoderinfo 

1605 

1606 return chunks 

1607 

1608 

1609# -------------------------------------------------------------------- 

1610# Registry 

1611 

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

1613Image.register_save(PngImageFile.format, _save) 

1614Image.register_save_all(PngImageFile.format, _save_all) 

1615 

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

1617 

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