Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/pillow-11.0.0-py3.10-linux-x86_64.egg/PIL/PngImagePlugin.py: 13%

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

926 statements  

1# 

2# The Python Imaging Library. 

3# $Id$ 

4# 

5# PNG support code 

6# 

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

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

9# 

10# history: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

27# 

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

29# Copyright (c) 1996 by Fredrik Lundh 

30# 

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

32# 

33from __future__ import annotations 

34 

35import itertools 

36import logging 

37import re 

38import struct 

39import warnings 

40import zlib 

41from collections.abc import Callable 

42from enum import IntEnum 

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

44 

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

46from ._binary import i16be as i16 

47from ._binary import i32be as i32 

48from ._binary import o8 

49from ._binary import o16be as o16 

50from ._binary import o32be as o32 

51 

52if TYPE_CHECKING: 

53 from . import _imaging 

54 

55logger = logging.getLogger(__name__) 

56 

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

58 

59 

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

61 

62 

63_MODES = { 

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

65 # Grayscale 

66 (1, 0): ("1", "1"), 

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

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

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

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

71 # Truecolour 

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

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

74 # Indexed-colour 

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

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

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

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

79 # Grayscale with alpha 

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

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

82 # Truecolour with alpha 

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

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

85} 

86 

87 

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

89 

90MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK 

91""" 

92Maximum decompressed size for a iTXt or zTXt chunk. 

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

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

95""" 

96MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK 

97""" 

98Set the maximum total text chunk size. 

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

100""" 

101 

102 

103# APNG frame disposal modes 

104class Disposal(IntEnum): 

105 OP_NONE = 0 

106 """ 

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

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

109 """ 

110 OP_BACKGROUND = 1 

111 """ 

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

113 the next frame. 

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

115 """ 

116 OP_PREVIOUS = 2 

117 """ 

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

119 rendering the next frame. 

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

121 """ 

122 

123 

124# APNG frame blend modes 

125class Blend(IntEnum): 

126 OP_SOURCE = 0 

127 """ 

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

129 image contents. 

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

131 """ 

132 OP_OVER = 1 

133 """ 

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

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

136 """ 

137 

138 

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

140 dobj = zlib.decompressobj() 

141 plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) 

142 if dobj.unconsumed_tail: 

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

144 raise ValueError(msg) 

145 return plaintext 

146 

147 

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

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

150 

151 

152# -------------------------------------------------------------------- 

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

154 

155 

156class ChunkStream: 

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

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

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

160 

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

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

163 cid = None 

164 

165 assert self.fp is not None 

166 if self.queue: 

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

168 self.fp.seek(pos) 

169 else: 

170 s = self.fp.read(8) 

171 cid = s[4:] 

172 pos = self.fp.tell() 

173 length = i32(s) 

174 

175 if not is_cid(cid): 

176 if not ImageFile.LOAD_TRUNCATED_IMAGES: 

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

178 raise SyntaxError(msg) 

179 

180 return cid, pos, length 

181 

182 def __enter__(self) -> ChunkStream: 

183 return self 

184 

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

186 self.close() 

187 

188 def close(self) -> None: 

189 self.queue = self.fp = None 

190 

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

192 assert self.queue is not None 

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

194 

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

196 """Call the appropriate chunk handler""" 

197 

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

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

200 

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

202 """Read and verify checksum""" 

203 

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

205 # images 

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

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

208 self.crc_skip(cid, data) 

209 return 

210 

211 assert self.fp is not None 

212 try: 

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

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

215 if crc1 != crc2: 

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

217 raise SyntaxError(msg) 

218 except struct.error as e: 

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

220 raise SyntaxError(msg) from e 

221 

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

223 """Read checksum""" 

224 

225 assert self.fp is not None 

226 self.fp.read(4) 

227 

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

229 # Simple approach; just calculate checksum for all remaining 

230 # blocks. Must be called directly after open. 

231 

232 cids = [] 

233 

234 assert self.fp is not None 

235 while True: 

236 try: 

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

238 except struct.error as e: 

239 msg = "truncated PNG file" 

240 raise OSError(msg) from e 

241 

242 if cid == endchunk: 

243 break 

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

245 cids.append(cid) 

246 

247 return cids 

248 

249 

250class iTXt(str): 

251 """ 

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

253 keeping their extra information 

254 

255 """ 

256 

257 lang: str | bytes | None 

258 tkey: str | bytes | None 

259 

260 @staticmethod 

261 def __new__( 

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

263 ) -> iTXt: 

264 """ 

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

266 :param text: value for this key 

267 :param lang: language code 

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

269 """ 

270 

271 self = str.__new__(cls, text) 

272 self.lang = lang 

273 self.tkey = tkey 

274 return self 

275 

276 

277class PngInfo: 

278 """ 

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

280 

281 """ 

282 

283 def __init__(self) -> None: 

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

285 

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

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

288 

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

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

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

292 should be written after IDAT 

293 

294 """ 

295 

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

297 

298 def add_itxt( 

299 self, 

300 key: str | bytes, 

301 value: str | bytes, 

302 lang: str | bytes = "", 

303 tkey: str | bytes = "", 

304 zip: bool = False, 

305 ) -> None: 

306 """Appends an iTXt chunk. 

307 

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

309 :param value: value for this key 

310 :param lang: language code 

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

312 :param zip: compression flag 

313 

314 """ 

315 

316 if not isinstance(key, bytes): 

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

318 if not isinstance(value, bytes): 

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

320 if not isinstance(lang, bytes): 

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

322 if not isinstance(tkey, bytes): 

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

324 

325 if zip: 

326 self.add( 

327 b"iTXt", 

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

329 ) 

330 else: 

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

332 

333 def add_text( 

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

335 ) -> None: 

336 """Appends a text chunk. 

337 

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

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

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

341 :param zip: compression flag 

342 

343 """ 

344 if isinstance(value, iTXt): 

345 return self.add_itxt( 

346 key, 

347 value, 

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

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

350 zip=zip, 

351 ) 

352 

353 # The tEXt chunk stores latin-1 text 

354 if not isinstance(value, bytes): 

355 try: 

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

357 except UnicodeError: 

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

359 

360 if not isinstance(key, bytes): 

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

362 

363 if zip: 

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

365 else: 

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

367 

368 

369# -------------------------------------------------------------------- 

370# PNG image stream (IHDR/IEND) 

371 

372 

373class _RewindState(NamedTuple): 

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

375 tile: list[ImageFile._Tile] 

376 seq_num: int | None 

377 

378 

379class PngStream(ChunkStream): 

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

381 super().__init__(fp) 

382 

383 # local copies of Image attributes 

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

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

386 self.im_size = (0, 0) 

387 self.im_mode = "" 

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

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

390 self.im_custom_mimetype: str | None = None 

391 self.im_n_frames: int | None = None 

392 self._seq_num: int | None = None 

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

394 

395 self.text_memory = 0 

396 

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

398 self.text_memory += chunklen 

399 if self.text_memory > MAX_TEXT_MEMORY: 

400 msg = ( 

401 "Too much memory used in text chunks: " 

402 f"{self.text_memory}>MAX_TEXT_MEMORY" 

403 ) 

404 raise ValueError(msg) 

405 

406 def save_rewind(self) -> None: 

407 self.rewind_state = _RewindState( 

408 self.im_info.copy(), 

409 self.im_tile, 

410 self._seq_num, 

411 ) 

412 

413 def rewind(self) -> None: 

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

415 self.im_tile = self.rewind_state.tile 

416 self._seq_num = self.rewind_state.seq_num 

417 

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

419 # ICC profile 

420 assert self.fp is not None 

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

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

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

424 # Null separator 1 byte (null character) 

425 # Compression method 1 byte (0) 

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

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

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

429 comp_method = s[i + 1] 

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

431 if comp_method != 0: 

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

433 raise SyntaxError(msg) 

434 try: 

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

436 except ValueError: 

437 if ImageFile.LOAD_TRUNCATED_IMAGES: 

438 icc_profile = None 

439 else: 

440 raise 

441 except zlib.error: 

442 icc_profile = None # FIXME 

443 self.im_info["icc_profile"] = icc_profile 

444 return s 

445 

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

447 # image header 

448 assert self.fp is not None 

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

450 if length < 13: 

451 if ImageFile.LOAD_TRUNCATED_IMAGES: 

452 return s 

453 msg = "Truncated IHDR chunk" 

454 raise ValueError(msg) 

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

456 try: 

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

458 except Exception: 

459 pass 

460 if s[12]: 

461 self.im_info["interlace"] = 1 

462 if s[11]: 

463 msg = "unknown filter category" 

464 raise SyntaxError(msg) 

465 return s 

466 

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

468 # image data 

469 if "bbox" in self.im_info: 

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

471 else: 

472 if self.im_n_frames is not None: 

473 self.im_info["default_image"] = True 

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

475 self.im_tile = tile 

476 self.im_idat = length 

477 msg = "image data found" 

478 raise EOFError(msg) 

479 

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

481 msg = "end of PNG image" 

482 raise EOFError(msg) 

483 

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

485 # palette 

486 assert self.fp is not None 

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

488 if self.im_mode == "P": 

489 self.im_palette = "RGB", s 

490 return s 

491 

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

493 # transparency 

494 assert self.fp is not None 

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

496 if self.im_mode == "P": 

497 if _simple_palette.match(s): 

498 # tRNS contains only one full-transparent entry, 

499 # other entries are full opaque 

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

501 if i >= 0: 

502 self.im_info["transparency"] = i 

503 else: 

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

505 # for each palette entry 

506 self.im_info["transparency"] = s 

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

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

509 elif self.im_mode == "RGB": 

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

511 return s 

512 

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

514 # gamma setting 

515 assert self.fp is not None 

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

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

518 return s 

519 

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

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

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

523 

524 assert self.fp is not None 

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

526 raw_vals = struct.unpack(">%dI" % (len(s) // 4), s) 

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

528 return s 

529 

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

531 # srgb rendering intent, 1 byte 

532 # 0 perceptual 

533 # 1 relative colorimetric 

534 # 2 saturation 

535 # 3 absolute colorimetric 

536 

537 assert self.fp is not None 

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

539 if length < 1: 

540 if ImageFile.LOAD_TRUNCATED_IMAGES: 

541 return s 

542 msg = "Truncated sRGB chunk" 

543 raise ValueError(msg) 

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

545 return s 

546 

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

548 # pixels per unit 

549 assert self.fp is not None 

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

551 if length < 9: 

552 if ImageFile.LOAD_TRUNCATED_IMAGES: 

553 return s 

554 msg = "Truncated pHYs chunk" 

555 raise ValueError(msg) 

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

557 unit = s[8] 

558 if unit == 1: # meter 

559 dpi = px * 0.0254, py * 0.0254 

560 self.im_info["dpi"] = dpi 

561 elif unit == 0: 

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

563 return s 

564 

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

566 # text 

567 assert self.fp is not None 

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

569 try: 

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

571 except ValueError: 

572 # fallback for broken tEXt tags 

573 k = s 

574 v = b"" 

575 if k: 

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

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

578 

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

580 self.im_text[k_str] = v_str 

581 self.check_text_memory(len(v_str)) 

582 

583 return s 

584 

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

586 # compressed text 

587 assert self.fp is not None 

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

589 try: 

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

591 except ValueError: 

592 k = s 

593 v = b"" 

594 if v: 

595 comp_method = v[0] 

596 else: 

597 comp_method = 0 

598 if comp_method != 0: 

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

600 raise SyntaxError(msg) 

601 try: 

602 v = _safe_zlib_decompress(v[1:]) 

603 except ValueError: 

604 if ImageFile.LOAD_TRUNCATED_IMAGES: 

605 v = b"" 

606 else: 

607 raise 

608 except zlib.error: 

609 v = b"" 

610 

611 if k: 

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

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

614 

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

616 self.check_text_memory(len(v_str)) 

617 

618 return s 

619 

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

621 # international text 

622 assert self.fp is not None 

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

624 try: 

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

626 except ValueError: 

627 return s 

628 if len(r) < 2: 

629 return s 

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

631 try: 

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

633 except ValueError: 

634 return s 

635 if cf != 0: 

636 if cm == 0: 

637 try: 

638 v = _safe_zlib_decompress(v) 

639 except ValueError: 

640 if ImageFile.LOAD_TRUNCATED_IMAGES: 

641 return s 

642 else: 

643 raise 

644 except zlib.error: 

645 return s 

646 else: 

647 return s 

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

649 self.im_info["xmp"] = v 

650 try: 

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

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

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

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

655 except UnicodeError: 

656 return s 

657 

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

659 self.check_text_memory(len(v_str)) 

660 

661 return s 

662 

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

664 assert self.fp is not None 

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

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

667 return s 

668 

669 # APNG chunks 

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

671 assert self.fp is not None 

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

673 if length < 8: 

674 if ImageFile.LOAD_TRUNCATED_IMAGES: 

675 return s 

676 msg = "APNG contains truncated acTL chunk" 

677 raise ValueError(msg) 

678 if self.im_n_frames is not None: 

679 self.im_n_frames = None 

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

681 return s 

682 n_frames = i32(s) 

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

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

685 return s 

686 self.im_n_frames = n_frames 

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

688 self.im_custom_mimetype = "image/apng" 

689 return s 

690 

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

692 assert self.fp is not None 

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

694 if length < 26: 

695 if ImageFile.LOAD_TRUNCATED_IMAGES: 

696 return s 

697 msg = "APNG contains truncated fcTL chunk" 

698 raise ValueError(msg) 

699 seq = i32(s) 

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

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

702 ): 

703 msg = "APNG contains frame sequence errors" 

704 raise SyntaxError(msg) 

705 self._seq_num = seq 

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

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

708 im_w, im_h = self.im_size 

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

710 msg = "APNG contains invalid frames" 

711 raise SyntaxError(msg) 

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

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

714 if delay_den == 0: 

715 delay_den = 100 

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

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

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

719 return s 

720 

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

722 assert self.fp is not None 

723 if length < 4: 

724 if ImageFile.LOAD_TRUNCATED_IMAGES: 

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

726 return s 

727 msg = "APNG contains truncated fDAT chunk" 

728 raise ValueError(msg) 

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

730 seq = i32(s) 

731 if self._seq_num != seq - 1: 

732 msg = "APNG contains frame sequence errors" 

733 raise SyntaxError(msg) 

734 self._seq_num = seq 

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

736 

737 

738# -------------------------------------------------------------------- 

739# PNG reader 

740 

741 

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

743 return prefix[:8] == _MAGIC 

744 

745 

746## 

747# Image plugin for PNG images. 

748 

749 

750class PngImageFile(ImageFile.ImageFile): 

751 format = "PNG" 

752 format_description = "Portable network graphics" 

753 

754 def _open(self) -> None: 

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

756 msg = "not a PNG file" 

757 raise SyntaxError(msg) 

758 self._fp = self.fp 

759 self.__frame = 0 

760 

761 # 

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

763 

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

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

766 

767 while True: 

768 # 

769 # get next chunk 

770 

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

772 

773 try: 

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

775 except EOFError: 

776 break 

777 except AttributeError: 

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

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

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

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

782 

783 self.png.crc(cid, s) 

784 

785 # 

786 # Copy relevant attributes from the PngStream. An alternative 

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

788 # directly, but that introduces circular references which are 

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

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

791 

792 self._mode = self.png.im_mode 

793 self._size = self.png.im_size 

794 self.info = self.png.im_info 

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

796 self.tile = self.png.im_tile 

797 self.custom_mimetype = self.png.im_custom_mimetype 

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

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

800 

801 if self.png.im_palette: 

802 rawmode, data = self.png.im_palette 

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

804 

805 if cid == b"fdAT": 

806 self.__prepare_idat = length - 4 

807 else: 

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

809 

810 if self.png.im_n_frames is not None: 

811 self._close_exclusive_fp_after_loading = False 

812 self.png.save_rewind() 

813 self.__rewind_idat = self.__prepare_idat 

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

815 if self.default_image: 

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

817 self.n_frames += 1 

818 self._seek(0) 

819 self.is_animated = self.n_frames > 1 

820 

821 @property 

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

823 # experimental 

824 if self._text is None: 

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

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

827 if self.is_animated: 

828 frame = self.__frame 

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

830 self.seek(self.n_frames - 1) 

831 self.load() 

832 if self.is_animated: 

833 self.seek(frame) 

834 assert self._text is not None 

835 return self._text 

836 

837 def verify(self) -> None: 

838 """Verify PNG file""" 

839 

840 if self.fp is None: 

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

842 raise RuntimeError(msg) 

843 

844 # back up to beginning of IDAT block 

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

846 

847 assert self.png is not None 

848 self.png.verify() 

849 self.png.close() 

850 

851 if self._exclusive_fp: 

852 self.fp.close() 

853 self.fp = None 

854 

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

856 if not self._seek_check(frame): 

857 return 

858 if frame < self.__frame: 

859 self._seek(0, True) 

860 

861 last_frame = self.__frame 

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

863 try: 

864 self._seek(f) 

865 except EOFError as e: 

866 self.seek(last_frame) 

867 msg = "no more images in APNG file" 

868 raise EOFError(msg) from e 

869 

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

871 assert self.png is not None 

872 

873 self.dispose: _imaging.ImagingCore | None 

874 dispose_extent = None 

875 if frame == 0: 

876 if rewind: 

877 self._fp.seek(self.__rewind) 

878 self.png.rewind() 

879 self.__prepare_idat = self.__rewind_idat 

880 self._im = None 

881 self.info = self.png.im_info 

882 self.tile = self.png.im_tile 

883 self.fp = self._fp 

884 self._prev_im = None 

885 self.dispose = None 

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

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

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

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

890 self.__frame = 0 

891 else: 

892 if frame != self.__frame + 1: 

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

894 raise ValueError(msg) 

895 

896 # ensure previous frame was loaded 

897 self.load() 

898 

899 if self.dispose: 

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

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

902 

903 self.fp = self._fp 

904 

905 # advance to the next frame 

906 if self.__prepare_idat: 

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

908 self.__prepare_idat = 0 

909 frame_start = False 

910 while True: 

911 self.fp.read(4) # CRC 

912 

913 try: 

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

915 except (struct.error, SyntaxError): 

916 break 

917 

918 if cid == b"IEND": 

919 msg = "No more images in APNG file" 

920 raise EOFError(msg) 

921 if cid == b"fcTL": 

922 if frame_start: 

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

924 msg = "APNG missing frame data" 

925 raise SyntaxError(msg) 

926 frame_start = True 

927 

928 try: 

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

930 except UnicodeDecodeError: 

931 break 

932 except EOFError: 

933 if cid == b"fdAT": 

934 length -= 4 

935 if frame_start: 

936 self.__prepare_idat = length 

937 break 

938 ImageFile._safe_read(self.fp, length) 

939 except AttributeError: 

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

941 ImageFile._safe_read(self.fp, length) 

942 

943 self.__frame = frame 

944 self.tile = self.png.im_tile 

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

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

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

948 

949 if not self.tile: 

950 msg = "image not found in APNG frame" 

951 raise EOFError(msg) 

952 if dispose_extent: 

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

954 

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

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

957 self.dispose_op = Disposal.OP_BACKGROUND 

958 

959 self.dispose = None 

960 if self.dispose_op == Disposal.OP_PREVIOUS: 

961 if self._prev_im: 

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

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

964 elif self.dispose_op == Disposal.OP_BACKGROUND: 

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

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

967 

968 def tell(self) -> int: 

969 return self.__frame 

970 

971 def load_prepare(self) -> None: 

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

973 

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

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

976 

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

978 ImageFile.ImageFile.load_prepare(self) 

979 

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

981 """internal: read more image data""" 

982 

983 assert self.png is not None 

984 while self.__idat == 0: 

985 # end of chunk, skip forward to next one 

986 

987 self.fp.read(4) # CRC 

988 

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

990 

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

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

993 return b"" 

994 

995 if cid == b"fdAT": 

996 try: 

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

998 except EOFError: 

999 pass 

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

1001 else: 

1002 self.__idat = length # empty chunks are allowed 

1003 

1004 # read more data from this chunk 

1005 if read_bytes <= 0: 

1006 read_bytes = self.__idat 

1007 else: 

1008 read_bytes = min(read_bytes, self.__idat) 

1009 

1010 self.__idat = self.__idat - read_bytes 

1011 

1012 return self.fp.read(read_bytes) 

1013 

1014 def load_end(self) -> None: 

1015 """internal: finished reading image data""" 

1016 assert self.png is not None 

1017 if self.__idat != 0: 

1018 self.fp.read(self.__idat) 

1019 while True: 

1020 self.fp.read(4) # CRC 

1021 

1022 try: 

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

1024 except (struct.error, SyntaxError): 

1025 break 

1026 

1027 if cid == b"IEND": 

1028 break 

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

1030 # start of the next frame, stop reading 

1031 self.__prepare_idat = 0 

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

1033 break 

1034 

1035 try: 

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

1037 except UnicodeDecodeError: 

1038 break 

1039 except EOFError: 

1040 if cid == b"fdAT": 

1041 length -= 4 

1042 try: 

1043 ImageFile._safe_read(self.fp, length) 

1044 except OSError as e: 

1045 if ImageFile.LOAD_TRUNCATED_IMAGES: 

1046 break 

1047 else: 

1048 raise e 

1049 except AttributeError: 

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

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

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

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

1054 self._text = self.png.im_text 

1055 if not self.is_animated: 

1056 self.png.close() 

1057 self.png = None 

1058 else: 

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

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

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

1062 mask = updated.convert_transparent( 

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

1064 ) 

1065 else: 

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

1067 t = self.info["transparency"] 

1068 if isinstance(t, bytes): 

1069 updated.putpalettealphas(t) 

1070 elif isinstance(t, int): 

1071 updated.putpalettealpha(t) 

1072 mask = updated.convert("RGBA") 

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

1074 self.im = self._prev_im 

1075 

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

1077 if "exif" not in self.info: 

1078 self.load() 

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

1080 return None 

1081 return self.getexif()._get_merged_dict() 

1082 

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

1084 if "exif" not in self.info: 

1085 self.load() 

1086 

1087 return super().getexif() 

1088 

1089 

1090# -------------------------------------------------------------------- 

1091# PNG writer 

1092 

1093_OUTMODES = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1110} 

1111 

1112 

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

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

1115 

1116 byte_data = b"".join(data) 

1117 

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

1119 fp.write(byte_data) 

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

1121 fp.write(o32(crc)) 

1122 

1123 

1124class _idat: 

1125 # wrap output from the encoder in IDAT chunks 

1126 

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

1128 self.fp = fp 

1129 self.chunk = chunk 

1130 

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

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

1133 

1134 

1135class _fdat: 

1136 # wrap encoder output in fdAT chunks 

1137 

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

1139 self.fp = fp 

1140 self.chunk = chunk 

1141 self.seq_num = seq_num 

1142 

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

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

1145 self.seq_num += 1 

1146 

1147 

1148class _Frame(NamedTuple): 

1149 im: Image.Image 

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

1151 encoderinfo: dict[str, Any] 

1152 

1153 

1154def _write_multiple_frames( 

1155 im: Image.Image, 

1156 fp: IO[bytes], 

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

1158 mode: str, 

1159 rawmode: str, 

1160 default_image: Image.Image | None, 

1161 append_images: list[Image.Image], 

1162) -> Image.Image | None: 

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

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

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

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

1167 

1168 if default_image: 

1169 chain = itertools.chain(append_images) 

1170 else: 

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

1172 

1173 im_frames: list[_Frame] = [] 

1174 frame_count = 0 

1175 for im_seq in chain: 

1176 for im_frame in ImageSequence.Iterator(im_seq): 

1177 if im_frame.mode == mode: 

1178 im_frame = im_frame.copy() 

1179 else: 

1180 im_frame = im_frame.convert(mode) 

1181 encoderinfo = im.encoderinfo.copy() 

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

1183 encoderinfo["duration"] = duration[frame_count] 

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

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

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

1187 encoderinfo["disposal"] = disposal[frame_count] 

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

1189 encoderinfo["blend"] = blend[frame_count] 

1190 frame_count += 1 

1191 

1192 if im_frames: 

1193 previous = im_frames[-1] 

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

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

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

1197 prev_disposal = Disposal.OP_BACKGROUND 

1198 

1199 if prev_disposal == Disposal.OP_BACKGROUND: 

1200 base_im = previous.im.copy() 

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

1202 bbox = previous.bbox 

1203 if bbox: 

1204 dispose = dispose.crop(bbox) 

1205 else: 

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

1207 base_im.paste(dispose, bbox) 

1208 elif prev_disposal == Disposal.OP_PREVIOUS: 

1209 base_im = im_frames[-2].im 

1210 else: 

1211 base_im = previous.im 

1212 delta = ImageChops.subtract_modulo( 

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

1214 ) 

1215 bbox = delta.getbbox(alpha_only=False) 

1216 if ( 

1217 not bbox 

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

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

1220 and "duration" in encoderinfo 

1221 ): 

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

1223 continue 

1224 else: 

1225 bbox = None 

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

1227 

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

1229 return im_frames[0].im 

1230 

1231 # animation control 

1232 chunk( 

1233 fp, 

1234 b"acTL", 

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

1236 o32(loop), # 4: num_plays 

1237 ) 

1238 

1239 # default image IDAT (if it exists) 

1240 if default_image: 

1241 if im.mode != mode: 

1242 im = im.convert(mode) 

1243 ImageFile._save( 

1244 im, 

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

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

1247 ) 

1248 

1249 seq_num = 0 

1250 for frame, frame_data in enumerate(im_frames): 

1251 im_frame = frame_data.im 

1252 if not frame_data.bbox: 

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

1254 else: 

1255 bbox = frame_data.bbox 

1256 im_frame = im_frame.crop(bbox) 

1257 size = im_frame.size 

1258 encoderinfo = frame_data.encoderinfo 

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

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

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

1262 # frame control 

1263 chunk( 

1264 fp, 

1265 b"fcTL", 

1266 o32(seq_num), # sequence_number 

1267 o32(size[0]), # width 

1268 o32(size[1]), # height 

1269 o32(bbox[0]), # x_offset 

1270 o32(bbox[1]), # y_offset 

1271 o16(frame_duration), # delay_numerator 

1272 o16(1000), # delay_denominator 

1273 o8(frame_disposal), # dispose_op 

1274 o8(frame_blend), # blend_op 

1275 ) 

1276 seq_num += 1 

1277 # frame data 

1278 if frame == 0 and not default_image: 

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

1280 ImageFile._save( 

1281 im_frame, 

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

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

1284 ) 

1285 else: 

1286 fdat_chunks = _fdat(fp, chunk, seq_num) 

1287 ImageFile._save( 

1288 im_frame, 

1289 cast(IO[bytes], fdat_chunks), 

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

1291 ) 

1292 seq_num = fdat_chunks.seq_num 

1293 return None 

1294 

1295 

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

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

1298 

1299 

1300def _save( 

1301 im: Image.Image, 

1302 fp: IO[bytes], 

1303 filename: str | bytes, 

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

1305 save_all: bool = False, 

1306) -> None: 

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

1308 

1309 if save_all: 

1310 default_image = im.encoderinfo.get( 

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

1312 ) 

1313 modes = set() 

1314 sizes = set() 

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

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

1317 for im_frame in ImageSequence.Iterator(im_seq): 

1318 modes.add(im_frame.mode) 

1319 sizes.add(im_frame.size) 

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

1321 if mode in modes: 

1322 break 

1323 else: 

1324 mode = modes.pop() 

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

1326 else: 

1327 size = im.size 

1328 mode = im.mode 

1329 

1330 outmode = mode 

1331 if mode == "P": 

1332 # 

1333 # attempt to minimize storage requirements for palette images 

1334 if "bits" in im.encoderinfo: 

1335 # number of bits specified by user 

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

1337 else: 

1338 # check palette contents 

1339 if im.palette: 

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

1341 else: 

1342 colors = 256 

1343 

1344 if colors <= 16: 

1345 if colors <= 2: 

1346 bits = 1 

1347 elif colors <= 4: 

1348 bits = 2 

1349 else: 

1350 bits = 4 

1351 outmode += f";{bits}" 

1352 

1353 # encoder options 

1354 im.encoderconfig = ( 

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

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

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

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

1359 ) 

1360 

1361 # get the corresponding PNG mode 

1362 try: 

1363 rawmode, bit_depth, color_type = _OUTMODES[outmode] 

1364 except KeyError as e: 

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

1366 raise OSError(msg) from e 

1367 

1368 # 

1369 # write minimal PNG file 

1370 

1371 fp.write(_MAGIC) 

1372 

1373 chunk( 

1374 fp, 

1375 b"IHDR", 

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

1377 o32(size[1]), 

1378 bit_depth, 

1379 color_type, 

1380 b"\0", # 10: compression 

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

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

1383 ) 

1384 

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

1386 

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

1388 if icc: 

1389 # ICC profile 

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

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

1392 # Null separator 1 byte (null character) 

1393 # Compression method 1 byte (0) 

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

1395 name = b"ICC Profile" 

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

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

1398 

1399 # You must either have sRGB or iCCP. 

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

1401 chunks.remove(b"sRGB") 

1402 

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

1404 if info: 

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

1406 for info_chunk in info.chunks: 

1407 cid, data = info_chunk[:2] 

1408 if cid in chunks: 

1409 chunks.remove(cid) 

1410 chunk(fp, cid, data) 

1411 elif cid in chunks_multiple_allowed: 

1412 chunk(fp, cid, data) 

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

1414 # Private chunk 

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

1416 if not after_idat: 

1417 chunk(fp, cid, data) 

1418 

1419 if im.mode == "P": 

1420 palette_byte_number = colors * 3 

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

1422 while len(palette_bytes) < palette_byte_number: 

1423 palette_bytes += b"\0" 

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

1425 

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

1427 

1428 if transparency or transparency == 0: 

1429 if im.mode == "P": 

1430 # limit to actual palette size 

1431 alpha_bytes = colors 

1432 if isinstance(transparency, bytes): 

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

1434 else: 

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

1436 alpha = b"\xFF" * transparency + b"\0" 

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

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

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

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

1441 elif im.mode == "RGB": 

1442 red, green, blue = transparency 

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

1444 else: 

1445 if "transparency" in im.encoderinfo: 

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

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

1448 msg = "cannot use transparency for this mode" 

1449 raise OSError(msg) 

1450 else: 

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

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

1453 alpha_bytes = colors 

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

1455 

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

1457 if dpi: 

1458 chunk( 

1459 fp, 

1460 b"pHYs", 

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

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

1463 b"\x01", 

1464 ) 

1465 

1466 if info: 

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

1468 for info_chunk in info.chunks: 

1469 cid, data = info_chunk[:2] 

1470 if cid in chunks: 

1471 chunks.remove(cid) 

1472 chunk(fp, cid, data) 

1473 

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

1475 if exif: 

1476 if isinstance(exif, Image.Exif): 

1477 exif = exif.tobytes(8) 

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

1479 exif = exif[6:] 

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

1481 

1482 single_im: Image.Image | None = im 

1483 if save_all: 

1484 single_im = _write_multiple_frames( 

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

1486 ) 

1487 if single_im: 

1488 ImageFile._save( 

1489 single_im, 

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

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

1492 ) 

1493 

1494 if info: 

1495 for info_chunk in info.chunks: 

1496 cid, data = info_chunk[:2] 

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

1498 # Private chunk 

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

1500 if after_idat: 

1501 chunk(fp, cid, data) 

1502 

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

1504 

1505 if hasattr(fp, "flush"): 

1506 fp.flush() 

1507 

1508 

1509# -------------------------------------------------------------------- 

1510# PNG chunk converter 

1511 

1512 

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

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

1515 from io import BytesIO 

1516 

1517 chunks = [] 

1518 

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

1520 byte_data = b"".join(data) 

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

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

1523 

1524 fp = BytesIO() 

1525 

1526 try: 

1527 im.encoderinfo = params 

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

1529 finally: 

1530 del im.encoderinfo 

1531 

1532 return chunks 

1533 

1534 

1535# -------------------------------------------------------------------- 

1536# Registry 

1537 

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

1539Image.register_save(PngImageFile.format, _save) 

1540Image.register_save_all(PngImageFile.format, _save_all) 

1541 

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

1543 

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