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

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

935 statements  

1# 

2# The Python Imaging Library. 

3# $Id$ 

4# 

5# PNG support code 

6# 

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

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

9# 

10# history: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

27# 

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

29# Copyright (c) 1996 by Fredrik Lundh 

30# 

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

32# 

33from __future__ import annotations 

34 

35import itertools 

36import logging 

37import re 

38import struct 

39import warnings 

40import zlib 

41from enum import IntEnum 

42from typing import IO, NamedTuple, cast 

43 

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

45from ._binary import i16be as i16 

46from ._binary import i32be as i32 

47from ._binary import o8 

48from ._binary import o16be as o16 

49from ._binary import o32be as o32 

50from ._deprecate import deprecate 

51from ._util import DeferredError 

52 

53TYPE_CHECKING = False 

54if TYPE_CHECKING: 

55 from collections.abc import Callable 

56 from typing import Any, NoReturn 

57 

58 from . import _imaging 

59 

60logger = logging.getLogger(__name__) 

61 

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

63 

64 

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

66 

67 

68_MODES = { 

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

70 # Grayscale 

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

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

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

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

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

76 # Truecolour 

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

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

79 # Indexed-colour 

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

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

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

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

84 # Grayscale with alpha 

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

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

87 # Truecolour with alpha 

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

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

90} 

91 

92 

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

94 

95MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK 

96""" 

97Maximum decompressed size for a iTXt or zTXt chunk. 

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

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

100""" 

101MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK 

102""" 

103Set the maximum total text chunk size. 

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

105""" 

106 

107 

108# APNG frame disposal modes 

109class Disposal(IntEnum): 

110 OP_NONE = 0 

111 """ 

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

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

114 """ 

115 OP_BACKGROUND = 1 

116 """ 

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

118 the next frame. 

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

120 """ 

121 OP_PREVIOUS = 2 

122 """ 

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

124 rendering the next frame. 

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

126 """ 

127 

128 

129# APNG frame blend modes 

130class Blend(IntEnum): 

131 OP_SOURCE = 0 

132 """ 

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

134 image contents. 

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

136 """ 

137 OP_OVER = 1 

138 """ 

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

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

141 """ 

142 

143 

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

145 dobj = zlib.decompressobj() 

146 plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) 

147 if dobj.unconsumed_tail: 

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

149 raise ValueError(msg) 

150 return plaintext 

151 

152 

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

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

155 

156 

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

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

159 

160 

161class ChunkStream: 

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

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

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

165 

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

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

168 cid = None 

169 

170 assert self.fp is not None 

171 if self.queue: 

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

173 self.fp.seek(pos) 

174 else: 

175 s = self.fp.read(8) 

176 cid = s[4:] 

177 pos = self.fp.tell() 

178 length = i32(s) 

179 

180 if not is_cid(cid): 

181 if not ImageFile.LOAD_TRUNCATED_IMAGES: 

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

183 raise SyntaxError(msg) 

184 

185 return cid, pos, length 

186 

187 def __enter__(self) -> ChunkStream: 

188 return self 

189 

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

191 self.close() 

192 

193 def close(self) -> None: 

194 self.queue = self.fp = None 

195 

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

197 assert self.queue is not None 

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

199 

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

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

202 

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

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

205 

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

207 """Read and verify checksum""" 

208 

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

210 # images 

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

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

213 self.crc_skip(cid, data) 

214 return 

215 

216 assert self.fp is not None 

217 try: 

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

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

220 if crc1 != crc2: 

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

222 raise SyntaxError(msg) 

223 except struct.error as e: 

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

225 raise SyntaxError(msg) from e 

226 

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

228 """Read checksum""" 

229 

230 assert self.fp is not None 

231 self.fp.read(4) 

232 

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

234 # Simple approach; just calculate checksum for all remaining 

235 # blocks. Must be called directly after open. 

236 

237 cids = [] 

238 

239 assert self.fp is not None 

240 while True: 

241 try: 

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

243 except struct.error as e: 

244 msg = "truncated PNG file" 

245 raise OSError(msg) from e 

246 

247 if cid == endchunk: 

248 break 

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

250 cids.append(cid) 

251 

252 return cids 

253 

254 

255class iTXt(str): 

256 """ 

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

258 keeping their extra information 

259 

260 """ 

261 

262 lang: str | bytes | None 

263 tkey: str | bytes | None 

264 

265 @staticmethod 

266 def __new__( 

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

268 ) -> iTXt: 

269 """ 

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

271 :param text: value for this key 

272 :param lang: language code 

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

274 """ 

275 

276 self = str.__new__(cls, text) 

277 self.lang = lang 

278 self.tkey = tkey 

279 return self 

280 

281 

282class PngInfo: 

283 """ 

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

285 

286 """ 

287 

288 def __init__(self) -> None: 

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

290 

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

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

293 

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

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

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

297 should be written after IDAT 

298 

299 """ 

300 

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

302 

303 def add_itxt( 

304 self, 

305 key: str | bytes, 

306 value: str | bytes, 

307 lang: str | bytes = "", 

308 tkey: str | bytes = "", 

309 zip: bool = False, 

310 ) -> None: 

311 """Appends an iTXt chunk. 

312 

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

314 :param value: value for this key 

315 :param lang: language code 

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

317 :param zip: compression flag 

318 

319 """ 

320 

321 if not isinstance(key, bytes): 

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

323 if not isinstance(value, bytes): 

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

325 if not isinstance(lang, bytes): 

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

327 if not isinstance(tkey, bytes): 

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

329 

330 if zip: 

331 self.add( 

332 b"iTXt", 

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

334 ) 

335 else: 

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

337 

338 def add_text( 

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

340 ) -> None: 

341 """Appends a text chunk. 

342 

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

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

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

346 :param zip: compression flag 

347 

348 """ 

349 if isinstance(value, iTXt): 

350 return self.add_itxt( 

351 key, 

352 value, 

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

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

355 zip=zip, 

356 ) 

357 

358 # The tEXt chunk stores latin-1 text 

359 if not isinstance(value, bytes): 

360 try: 

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

362 except UnicodeError: 

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

364 

365 if not isinstance(key, bytes): 

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

367 

368 if zip: 

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

370 else: 

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

372 

373 

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

375# PNG image stream (IHDR/IEND) 

376 

377 

378class _RewindState(NamedTuple): 

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

380 tile: list[ImageFile._Tile] 

381 seq_num: int | None 

382 

383 

384class PngStream(ChunkStream): 

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

386 super().__init__(fp) 

387 

388 # local copies of Image attributes 

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

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

391 self.im_size = (0, 0) 

392 self.im_mode = "" 

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

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

395 self.im_custom_mimetype: str | None = None 

396 self.im_n_frames: int | None = None 

397 self._seq_num: int | None = None 

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

399 

400 self.text_memory = 0 

401 

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

403 self.text_memory += chunklen 

404 if self.text_memory > MAX_TEXT_MEMORY: 

405 msg = ( 

406 "Too much memory used in text chunks: " 

407 f"{self.text_memory}>MAX_TEXT_MEMORY" 

408 ) 

409 raise ValueError(msg) 

410 

411 def save_rewind(self) -> None: 

412 self.rewind_state = _RewindState( 

413 self.im_info.copy(), 

414 self.im_tile, 

415 self._seq_num, 

416 ) 

417 

418 def rewind(self) -> None: 

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

420 self.im_tile = self.rewind_state.tile 

421 self._seq_num = self.rewind_state.seq_num 

422 

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

424 # ICC profile 

425 assert self.fp is not None 

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

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

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

429 # Null separator 1 byte (null character) 

430 # Compression method 1 byte (0) 

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

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

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

434 comp_method = s[i + 1] 

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

436 if comp_method != 0: 

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

438 raise SyntaxError(msg) 

439 try: 

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

441 except ValueError: 

442 if ImageFile.LOAD_TRUNCATED_IMAGES: 

443 icc_profile = None 

444 else: 

445 raise 

446 except zlib.error: 

447 icc_profile = None # FIXME 

448 self.im_info["icc_profile"] = icc_profile 

449 return s 

450 

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

452 # image header 

453 assert self.fp is not None 

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

455 if length < 13: 

456 if ImageFile.LOAD_TRUNCATED_IMAGES: 

457 return s 

458 msg = "Truncated IHDR chunk" 

459 raise ValueError(msg) 

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

461 try: 

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

463 except Exception: 

464 pass 

465 if s[12]: 

466 self.im_info["interlace"] = 1 

467 if s[11]: 

468 msg = "unknown filter category" 

469 raise SyntaxError(msg) 

470 return s 

471 

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

473 # image data 

474 if "bbox" in self.im_info: 

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

476 else: 

477 if self.im_n_frames is not None: 

478 self.im_info["default_image"] = True 

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

480 self.im_tile = tile 

481 self.im_idat = length 

482 msg = "image data found" 

483 raise EOFError(msg) 

484 

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

486 msg = "end of PNG image" 

487 raise EOFError(msg) 

488 

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

490 # palette 

491 assert self.fp is not None 

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

493 if self.im_mode == "P": 

494 self.im_palette = "RGB", s 

495 return s 

496 

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

498 # transparency 

499 assert self.fp is not None 

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

501 if self.im_mode == "P": 

502 if _simple_palette.match(s): 

503 # tRNS contains only one full-transparent entry, 

504 # other entries are full opaque 

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

506 if i >= 0: 

507 self.im_info["transparency"] = i 

508 else: 

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

510 # for each palette entry 

511 self.im_info["transparency"] = s 

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

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

514 elif self.im_mode == "RGB": 

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

516 return s 

517 

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

519 # gamma setting 

520 assert self.fp is not None 

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

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

523 return s 

524 

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

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

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

528 

529 assert self.fp is not None 

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

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

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

533 return s 

534 

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

536 # srgb rendering intent, 1 byte 

537 # 0 perceptual 

538 # 1 relative colorimetric 

539 # 2 saturation 

540 # 3 absolute colorimetric 

541 

542 assert self.fp is not None 

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

544 if length < 1: 

545 if ImageFile.LOAD_TRUNCATED_IMAGES: 

546 return s 

547 msg = "Truncated sRGB chunk" 

548 raise ValueError(msg) 

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

550 return s 

551 

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

553 # pixels per unit 

554 assert self.fp is not None 

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

556 if length < 9: 

557 if ImageFile.LOAD_TRUNCATED_IMAGES: 

558 return s 

559 msg = "Truncated pHYs chunk" 

560 raise ValueError(msg) 

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

562 unit = s[8] 

563 if unit == 1: # meter 

564 dpi = px * 0.0254, py * 0.0254 

565 self.im_info["dpi"] = dpi 

566 elif unit == 0: 

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

568 return s 

569 

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

571 # text 

572 assert self.fp is not None 

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

574 try: 

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

576 except ValueError: 

577 # fallback for broken tEXt tags 

578 k = s 

579 v = b"" 

580 if k: 

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

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

583 

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

585 self.im_text[k_str] = v_str 

586 self.check_text_memory(len(v_str)) 

587 

588 return s 

589 

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

591 # compressed text 

592 assert self.fp is not None 

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

594 try: 

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

596 except ValueError: 

597 k = s 

598 v = b"" 

599 if v: 

600 comp_method = v[0] 

601 else: 

602 comp_method = 0 

603 if comp_method != 0: 

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

605 raise SyntaxError(msg) 

606 try: 

607 v = _safe_zlib_decompress(v[1:]) 

608 except ValueError: 

609 if ImageFile.LOAD_TRUNCATED_IMAGES: 

610 v = b"" 

611 else: 

612 raise 

613 except zlib.error: 

614 v = b"" 

615 

616 if k: 

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

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

619 

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

621 self.check_text_memory(len(v_str)) 

622 

623 return s 

624 

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

626 # international text 

627 assert self.fp is not None 

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

629 try: 

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

631 except ValueError: 

632 return s 

633 if len(r) < 2: 

634 return s 

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

636 try: 

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

638 except ValueError: 

639 return s 

640 if cf != 0: 

641 if cm == 0: 

642 try: 

643 v = _safe_zlib_decompress(v) 

644 except ValueError: 

645 if ImageFile.LOAD_TRUNCATED_IMAGES: 

646 return s 

647 else: 

648 raise 

649 except zlib.error: 

650 return s 

651 else: 

652 return s 

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

654 self.im_info["xmp"] = v 

655 try: 

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

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

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

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

660 except UnicodeError: 

661 return s 

662 

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

664 self.check_text_memory(len(v_str)) 

665 

666 return s 

667 

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

669 assert self.fp is not None 

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

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

672 return s 

673 

674 # APNG chunks 

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

676 assert self.fp is not None 

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

678 if length < 8: 

679 if ImageFile.LOAD_TRUNCATED_IMAGES: 

680 return s 

681 msg = "APNG contains truncated acTL chunk" 

682 raise ValueError(msg) 

683 if self.im_n_frames is not None: 

684 self.im_n_frames = None 

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

686 return s 

687 n_frames = i32(s) 

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

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

690 return s 

691 self.im_n_frames = n_frames 

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

693 self.im_custom_mimetype = "image/apng" 

694 return s 

695 

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

697 assert self.fp is not None 

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

699 if length < 26: 

700 if ImageFile.LOAD_TRUNCATED_IMAGES: 

701 return s 

702 msg = "APNG contains truncated fcTL chunk" 

703 raise ValueError(msg) 

704 seq = i32(s) 

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

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

707 ): 

708 msg = "APNG contains frame sequence errors" 

709 raise SyntaxError(msg) 

710 self._seq_num = seq 

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

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

713 im_w, im_h = self.im_size 

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

715 msg = "APNG contains invalid frames" 

716 raise SyntaxError(msg) 

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

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

719 if delay_den == 0: 

720 delay_den = 100 

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

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

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

724 return s 

725 

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

727 assert self.fp is not None 

728 if length < 4: 

729 if ImageFile.LOAD_TRUNCATED_IMAGES: 

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

731 return s 

732 msg = "APNG contains truncated fDAT chunk" 

733 raise ValueError(msg) 

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

735 seq = i32(s) 

736 if self._seq_num != seq - 1: 

737 msg = "APNG contains frame sequence errors" 

738 raise SyntaxError(msg) 

739 self._seq_num = seq 

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

741 

742 

743# -------------------------------------------------------------------- 

744# PNG reader 

745 

746 

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

748 return prefix.startswith(_MAGIC) 

749 

750 

751## 

752# Image plugin for PNG images. 

753 

754 

755class PngImageFile(ImageFile.ImageFile): 

756 format = "PNG" 

757 format_description = "Portable network graphics" 

758 

759 def _open(self) -> None: 

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

761 msg = "not a PNG file" 

762 raise SyntaxError(msg) 

763 self._fp = self.fp 

764 self.__frame = 0 

765 

766 # 

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

768 

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

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

771 

772 while True: 

773 # 

774 # get next chunk 

775 

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

777 

778 try: 

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

780 except EOFError: 

781 break 

782 except AttributeError: 

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

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

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

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

787 

788 self.png.crc(cid, s) 

789 

790 # 

791 # Copy relevant attributes from the PngStream. An alternative 

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

793 # directly, but that introduces circular references which are 

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

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

796 

797 self._mode = self.png.im_mode 

798 self._size = self.png.im_size 

799 self.info = self.png.im_info 

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

801 self.tile = self.png.im_tile 

802 self.custom_mimetype = self.png.im_custom_mimetype 

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

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

805 

806 if self.png.im_palette: 

807 rawmode, data = self.png.im_palette 

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

809 

810 if cid == b"fdAT": 

811 self.__prepare_idat = length - 4 

812 else: 

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

814 

815 if self.png.im_n_frames is not None: 

816 self._close_exclusive_fp_after_loading = False 

817 self.png.save_rewind() 

818 self.__rewind_idat = self.__prepare_idat 

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

820 if self.default_image: 

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

822 self.n_frames += 1 

823 self._seek(0) 

824 self.is_animated = self.n_frames > 1 

825 

826 @property 

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

828 # experimental 

829 if self._text is None: 

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

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

832 if self.is_animated: 

833 frame = self.__frame 

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

835 self.seek(self.n_frames - 1) 

836 self.load() 

837 if self.is_animated: 

838 self.seek(frame) 

839 assert self._text is not None 

840 return self._text 

841 

842 def verify(self) -> None: 

843 """Verify PNG file""" 

844 

845 if self.fp is None: 

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

847 raise RuntimeError(msg) 

848 

849 # back up to beginning of IDAT block 

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

851 

852 assert self.png is not None 

853 self.png.verify() 

854 self.png.close() 

855 

856 if self._exclusive_fp: 

857 self.fp.close() 

858 self.fp = None 

859 

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

861 if not self._seek_check(frame): 

862 return 

863 if frame < self.__frame: 

864 self._seek(0, True) 

865 

866 last_frame = self.__frame 

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

868 try: 

869 self._seek(f) 

870 except EOFError as e: 

871 self.seek(last_frame) 

872 msg = "no more images in APNG file" 

873 raise EOFError(msg) from e 

874 

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

876 assert self.png is not None 

877 if isinstance(self._fp, DeferredError): 

878 raise self._fp.ex 

879 

880 self.dispose: _imaging.ImagingCore | None 

881 dispose_extent = None 

882 if frame == 0: 

883 if rewind: 

884 self._fp.seek(self.__rewind) 

885 self.png.rewind() 

886 self.__prepare_idat = self.__rewind_idat 

887 self._im = None 

888 self.info = self.png.im_info 

889 self.tile = self.png.im_tile 

890 self.fp = self._fp 

891 self._prev_im = None 

892 self.dispose = None 

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

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

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

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

897 self.__frame = 0 

898 else: 

899 if frame != self.__frame + 1: 

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

901 raise ValueError(msg) 

902 

903 # ensure previous frame was loaded 

904 self.load() 

905 

906 if self.dispose: 

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

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

909 

910 self.fp = self._fp 

911 

912 # advance to the next frame 

913 if self.__prepare_idat: 

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

915 self.__prepare_idat = 0 

916 frame_start = False 

917 while True: 

918 self.fp.read(4) # CRC 

919 

920 try: 

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

922 except (struct.error, SyntaxError): 

923 break 

924 

925 if cid == b"IEND": 

926 msg = "No more images in APNG file" 

927 raise EOFError(msg) 

928 if cid == b"fcTL": 

929 if frame_start: 

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

931 msg = "APNG missing frame data" 

932 raise SyntaxError(msg) 

933 frame_start = True 

934 

935 try: 

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

937 except UnicodeDecodeError: 

938 break 

939 except EOFError: 

940 if cid == b"fdAT": 

941 length -= 4 

942 if frame_start: 

943 self.__prepare_idat = length 

944 break 

945 ImageFile._safe_read(self.fp, length) 

946 except AttributeError: 

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

948 ImageFile._safe_read(self.fp, length) 

949 

950 self.__frame = frame 

951 self.tile = self.png.im_tile 

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

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

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

955 

956 if not self.tile: 

957 msg = "image not found in APNG frame" 

958 raise EOFError(msg) 

959 if dispose_extent: 

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

961 

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

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

964 self.dispose_op = Disposal.OP_BACKGROUND 

965 

966 self.dispose = None 

967 if self.dispose_op == Disposal.OP_PREVIOUS: 

968 if self._prev_im: 

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

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

971 elif self.dispose_op == Disposal.OP_BACKGROUND: 

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

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

974 

975 def tell(self) -> int: 

976 return self.__frame 

977 

978 def load_prepare(self) -> None: 

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

980 

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

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

983 

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

985 ImageFile.ImageFile.load_prepare(self) 

986 

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

988 """internal: read more image data""" 

989 

990 assert self.png is not None 

991 while self.__idat == 0: 

992 # end of chunk, skip forward to next one 

993 

994 self.fp.read(4) # CRC 

995 

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

997 

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

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

1000 return b"" 

1001 

1002 if cid == b"fdAT": 

1003 try: 

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

1005 except EOFError: 

1006 pass 

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

1008 else: 

1009 self.__idat = length # empty chunks are allowed 

1010 

1011 # read more data from this chunk 

1012 if read_bytes <= 0: 

1013 read_bytes = self.__idat 

1014 else: 

1015 read_bytes = min(read_bytes, self.__idat) 

1016 

1017 self.__idat = self.__idat - read_bytes 

1018 

1019 return self.fp.read(read_bytes) 

1020 

1021 def load_end(self) -> None: 

1022 """internal: finished reading image data""" 

1023 assert self.png is not None 

1024 if self.__idat != 0: 

1025 self.fp.read(self.__idat) 

1026 while True: 

1027 self.fp.read(4) # CRC 

1028 

1029 try: 

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

1031 except (struct.error, SyntaxError): 

1032 break 

1033 

1034 if cid == b"IEND": 

1035 break 

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

1037 # start of the next frame, stop reading 

1038 self.__prepare_idat = 0 

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

1040 break 

1041 

1042 try: 

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

1044 except UnicodeDecodeError: 

1045 break 

1046 except EOFError: 

1047 if cid == b"fdAT": 

1048 length -= 4 

1049 try: 

1050 ImageFile._safe_read(self.fp, length) 

1051 except OSError as e: 

1052 if ImageFile.LOAD_TRUNCATED_IMAGES: 

1053 break 

1054 else: 

1055 raise e 

1056 except AttributeError: 

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

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

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

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

1061 self._text = self.png.im_text 

1062 if not self.is_animated: 

1063 self.png.close() 

1064 self.png = None 

1065 else: 

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

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

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

1069 mask = updated.convert_transparent( 

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

1071 ) 

1072 else: 

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

1074 t = self.info["transparency"] 

1075 if isinstance(t, bytes): 

1076 updated.putpalettealphas(t) 

1077 elif isinstance(t, int): 

1078 updated.putpalettealpha(t) 

1079 mask = updated.convert("RGBA") 

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

1081 self.im = self._prev_im 

1082 

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

1084 if "exif" not in self.info: 

1085 self.load() 

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

1087 return None 

1088 return self.getexif()._get_merged_dict() 

1089 

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

1091 if "exif" not in self.info: 

1092 self.load() 

1093 

1094 return super().getexif() 

1095 

1096 

1097# -------------------------------------------------------------------- 

1098# PNG writer 

1099 

1100_OUTMODES = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1117} 

1118 

1119 

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

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

1122 

1123 byte_data = b"".join(data) 

1124 

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

1126 fp.write(byte_data) 

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

1128 fp.write(o32(crc)) 

1129 

1130 

1131class _idat: 

1132 # wrap output from the encoder in IDAT chunks 

1133 

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

1135 self.fp = fp 

1136 self.chunk = chunk 

1137 

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

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

1140 

1141 

1142class _fdat: 

1143 # wrap encoder output in fdAT chunks 

1144 

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

1146 self.fp = fp 

1147 self.chunk = chunk 

1148 self.seq_num = seq_num 

1149 

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

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

1152 self.seq_num += 1 

1153 

1154 

1155class _Frame(NamedTuple): 

1156 im: Image.Image 

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

1158 encoderinfo: dict[str, Any] 

1159 

1160 

1161def _write_multiple_frames( 

1162 im: Image.Image, 

1163 fp: IO[bytes], 

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

1165 mode: str, 

1166 rawmode: str, 

1167 default_image: Image.Image | None, 

1168 append_images: list[Image.Image], 

1169) -> Image.Image | None: 

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

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

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

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

1174 

1175 if default_image: 

1176 chain = itertools.chain(append_images) 

1177 else: 

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

1179 

1180 im_frames: list[_Frame] = [] 

1181 frame_count = 0 

1182 for im_seq in chain: 

1183 for im_frame in ImageSequence.Iterator(im_seq): 

1184 if im_frame.mode == mode: 

1185 im_frame = im_frame.copy() 

1186 else: 

1187 im_frame = im_frame.convert(mode) 

1188 encoderinfo = im.encoderinfo.copy() 

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

1190 encoderinfo["duration"] = duration[frame_count] 

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

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

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

1194 encoderinfo["disposal"] = disposal[frame_count] 

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

1196 encoderinfo["blend"] = blend[frame_count] 

1197 frame_count += 1 

1198 

1199 if im_frames: 

1200 previous = im_frames[-1] 

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

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

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

1204 prev_disposal = Disposal.OP_BACKGROUND 

1205 

1206 if prev_disposal == Disposal.OP_BACKGROUND: 

1207 base_im = previous.im.copy() 

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

1209 bbox = previous.bbox 

1210 if bbox: 

1211 dispose = dispose.crop(bbox) 

1212 else: 

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

1214 base_im.paste(dispose, bbox) 

1215 elif prev_disposal == Disposal.OP_PREVIOUS: 

1216 base_im = im_frames[-2].im 

1217 else: 

1218 base_im = previous.im 

1219 delta = ImageChops.subtract_modulo( 

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

1221 ) 

1222 bbox = delta.getbbox(alpha_only=False) 

1223 if ( 

1224 not bbox 

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

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

1227 and "duration" in encoderinfo 

1228 ): 

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

1230 continue 

1231 else: 

1232 bbox = None 

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

1234 

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

1236 return im_frames[0].im 

1237 

1238 # animation control 

1239 chunk( 

1240 fp, 

1241 b"acTL", 

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

1243 o32(loop), # 4: num_plays 

1244 ) 

1245 

1246 # default image IDAT (if it exists) 

1247 if default_image: 

1248 if im.mode != mode: 

1249 im = im.convert(mode) 

1250 ImageFile._save( 

1251 im, 

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

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

1254 ) 

1255 

1256 seq_num = 0 

1257 for frame, frame_data in enumerate(im_frames): 

1258 im_frame = frame_data.im 

1259 if not frame_data.bbox: 

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

1261 else: 

1262 bbox = frame_data.bbox 

1263 im_frame = im_frame.crop(bbox) 

1264 size = im_frame.size 

1265 encoderinfo = frame_data.encoderinfo 

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

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

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

1269 # frame control 

1270 chunk( 

1271 fp, 

1272 b"fcTL", 

1273 o32(seq_num), # sequence_number 

1274 o32(size[0]), # width 

1275 o32(size[1]), # height 

1276 o32(bbox[0]), # x_offset 

1277 o32(bbox[1]), # y_offset 

1278 o16(frame_duration), # delay_numerator 

1279 o16(1000), # delay_denominator 

1280 o8(frame_disposal), # dispose_op 

1281 o8(frame_blend), # blend_op 

1282 ) 

1283 seq_num += 1 

1284 # frame data 

1285 if frame == 0 and not default_image: 

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

1287 ImageFile._save( 

1288 im_frame, 

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

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

1291 ) 

1292 else: 

1293 fdat_chunks = _fdat(fp, chunk, seq_num) 

1294 ImageFile._save( 

1295 im_frame, 

1296 cast(IO[bytes], fdat_chunks), 

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

1298 ) 

1299 seq_num = fdat_chunks.seq_num 

1300 return None 

1301 

1302 

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

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

1305 

1306 

1307def _save( 

1308 im: Image.Image, 

1309 fp: IO[bytes], 

1310 filename: str | bytes, 

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

1312 save_all: bool = False, 

1313) -> None: 

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

1315 

1316 if save_all: 

1317 default_image = im.encoderinfo.get( 

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

1319 ) 

1320 modes = set() 

1321 sizes = set() 

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

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

1324 for im_frame in ImageSequence.Iterator(im_seq): 

1325 modes.add(im_frame.mode) 

1326 sizes.add(im_frame.size) 

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

1328 if mode in modes: 

1329 break 

1330 else: 

1331 mode = modes.pop() 

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

1333 else: 

1334 size = im.size 

1335 mode = im.mode 

1336 

1337 outmode = mode 

1338 if mode == "P": 

1339 # 

1340 # attempt to minimize storage requirements for palette images 

1341 if "bits" in im.encoderinfo: 

1342 # number of bits specified by user 

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

1344 else: 

1345 # check palette contents 

1346 if im.palette: 

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

1348 else: 

1349 colors = 256 

1350 

1351 if colors <= 16: 

1352 if colors <= 2: 

1353 bits = 1 

1354 elif colors <= 4: 

1355 bits = 2 

1356 else: 

1357 bits = 4 

1358 outmode += f";{bits}" 

1359 

1360 # encoder options 

1361 im.encoderconfig = ( 

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

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

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

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

1366 ) 

1367 

1368 # get the corresponding PNG mode 

1369 try: 

1370 rawmode, bit_depth, color_type = _OUTMODES[outmode] 

1371 except KeyError as e: 

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

1373 raise OSError(msg) from e 

1374 if outmode == "I": 

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

1376 

1377 # 

1378 # write minimal PNG file 

1379 

1380 fp.write(_MAGIC) 

1381 

1382 chunk( 

1383 fp, 

1384 b"IHDR", 

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

1386 o32(size[1]), 

1387 bit_depth, 

1388 color_type, 

1389 b"\0", # 10: compression 

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

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

1392 ) 

1393 

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

1395 

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

1397 if icc: 

1398 # ICC profile 

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

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

1401 # Null separator 1 byte (null character) 

1402 # Compression method 1 byte (0) 

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

1404 name = b"ICC Profile" 

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

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

1407 

1408 # You must either have sRGB or iCCP. 

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

1410 chunks.remove(b"sRGB") 

1411 

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

1413 if info: 

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

1415 for info_chunk in info.chunks: 

1416 cid, data = info_chunk[:2] 

1417 if cid in chunks: 

1418 chunks.remove(cid) 

1419 chunk(fp, cid, data) 

1420 elif cid in chunks_multiple_allowed: 

1421 chunk(fp, cid, data) 

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

1423 # Private chunk 

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

1425 if not after_idat: 

1426 chunk(fp, cid, data) 

1427 

1428 if im.mode == "P": 

1429 palette_byte_number = colors * 3 

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

1431 while len(palette_bytes) < palette_byte_number: 

1432 palette_bytes += b"\0" 

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

1434 

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

1436 

1437 if transparency or transparency == 0: 

1438 if im.mode == "P": 

1439 # limit to actual palette size 

1440 alpha_bytes = colors 

1441 if isinstance(transparency, bytes): 

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

1443 else: 

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

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

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

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

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

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

1450 elif im.mode == "RGB": 

1451 red, green, blue = transparency 

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

1453 else: 

1454 if "transparency" in im.encoderinfo: 

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

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

1457 msg = "cannot use transparency for this mode" 

1458 raise OSError(msg) 

1459 else: 

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

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

1462 alpha_bytes = colors 

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

1464 

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

1466 if dpi: 

1467 chunk( 

1468 fp, 

1469 b"pHYs", 

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

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

1472 b"\x01", 

1473 ) 

1474 

1475 if info: 

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

1477 for info_chunk in info.chunks: 

1478 cid, data = info_chunk[:2] 

1479 if cid in chunks: 

1480 chunks.remove(cid) 

1481 chunk(fp, cid, data) 

1482 

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

1484 if exif: 

1485 if isinstance(exif, Image.Exif): 

1486 exif = exif.tobytes(8) 

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

1488 exif = exif[6:] 

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

1490 

1491 single_im: Image.Image | None = im 

1492 if save_all: 

1493 single_im = _write_multiple_frames( 

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

1495 ) 

1496 if single_im: 

1497 ImageFile._save( 

1498 single_im, 

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

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

1501 ) 

1502 

1503 if info: 

1504 for info_chunk in info.chunks: 

1505 cid, data = info_chunk[:2] 

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

1507 # Private chunk 

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

1509 if after_idat: 

1510 chunk(fp, cid, data) 

1511 

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

1513 

1514 if hasattr(fp, "flush"): 

1515 fp.flush() 

1516 

1517 

1518# -------------------------------------------------------------------- 

1519# PNG chunk converter 

1520 

1521 

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

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

1524 from io import BytesIO 

1525 

1526 chunks = [] 

1527 

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

1529 byte_data = b"".join(data) 

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

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

1532 

1533 fp = BytesIO() 

1534 

1535 try: 

1536 im.encoderinfo = params 

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

1538 finally: 

1539 del im.encoderinfo 

1540 

1541 return chunks 

1542 

1543 

1544# -------------------------------------------------------------------- 

1545# Registry 

1546 

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

1548Image.register_save(PngImageFile.format, _save) 

1549Image.register_save_all(PngImageFile.format, _save_all) 

1550 

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

1552 

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