Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/PngImagePlugin.py: 37%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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
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
35__lazy_modules__ = {
36 "PIL._binary",
37 "PIL._util",
38 "fractions",
39 "itertools",
40 "struct",
41 "warnings",
42 "zlib",
43}
45import itertools
46import logging
47import re
48import struct
49import warnings
50import zlib
51from enum import IntEnum
52from fractions import Fraction
53from typing import IO, NamedTuple, cast
55from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
56from ._binary import i16be as i16
57from ._binary import i32be as i32
58from ._binary import o8
59from ._binary import o16be as o16
60from ._binary import o32be as o32
61from ._util import DeferredError
63TYPE_CHECKING = False
64if TYPE_CHECKING:
65 from collections.abc import Callable
66 from typing import Any, NoReturn, Self
68 from . import _imaging
70logger = logging.getLogger(__name__)
72is_cid = re.compile(rb"\w\w\w\w").match
75_MAGIC = b"\211PNG\r\n\032\n"
78_MODES = {
79 # supported bits/color combinations, and corresponding modes/rawmodes
80 # Grayscale
81 (1, 0): ("1", "1"),
82 (2, 0): ("L", "L;2"),
83 (4, 0): ("L", "L;4"),
84 (8, 0): ("L", "L"),
85 (16, 0): ("I;16", "I;16B"),
86 # Truecolour
87 (8, 2): ("RGB", "RGB"),
88 (16, 2): ("RGB", "RGB;16B"),
89 # Indexed-colour
90 (1, 3): ("P", "P;1"),
91 (2, 3): ("P", "P;2"),
92 (4, 3): ("P", "P;4"),
93 (8, 3): ("P", "P"),
94 # Grayscale with alpha
95 (8, 4): ("LA", "LA"),
96 (16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available
97 # Truecolour with alpha
98 (8, 6): ("RGBA", "RGBA"),
99 (16, 6): ("RGBA", "RGBA;16B"),
100}
103_simple_palette = re.compile(b"^\xff*\x00\xff*$")
105MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK
106"""
107Maximum decompressed size for a iTXt or zTXt chunk.
108Eliminates decompression bombs where compressed chunks can expand 1000x.
109See :ref:`Text in PNG File Format<png-text>`.
110"""
111MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK
112"""
113Set the maximum total text chunk size.
114See :ref:`Text in PNG File Format<png-text>`.
115"""
118# APNG frame disposal modes
119class Disposal(IntEnum):
120 OP_NONE = 0
121 """
122 No disposal is done on this frame before rendering the next frame.
123 See :ref:`Saving APNG sequences<apng-saving>`.
124 """
125 OP_BACKGROUND = 1
126 """
127 This frame’s modified region is cleared to fully transparent black before rendering
128 the next frame.
129 See :ref:`Saving APNG sequences<apng-saving>`.
130 """
131 OP_PREVIOUS = 2
132 """
133 This frame’s modified region is reverted to the previous frame’s contents before
134 rendering the next frame.
135 See :ref:`Saving APNG sequences<apng-saving>`.
136 """
139# APNG frame blend modes
140class Blend(IntEnum):
141 OP_SOURCE = 0
142 """
143 All color components of this frame, including alpha, overwrite the previous output
144 image contents.
145 See :ref:`Saving APNG sequences<apng-saving>`.
146 """
147 OP_OVER = 1
148 """
149 This frame should be alpha composited with the previous output image contents.
150 See :ref:`Saving APNG sequences<apng-saving>`.
151 """
154def _safe_zlib_decompress(s: bytes) -> bytes:
155 dobj = zlib.decompressobj()
156 plaintext = dobj.decompress(s, MAX_TEXT_CHUNK)
157 if dobj.unconsumed_tail:
158 msg = "Decompressed data too large for PngImagePlugin.MAX_TEXT_CHUNK"
159 raise ValueError(msg)
160 return plaintext
163def _crc32(data: bytes, seed: int = 0) -> int:
164 return zlib.crc32(data, seed) & 0xFFFFFFFF
167# --------------------------------------------------------------------
168# Support classes. Suitable for PNG and related formats like MNG etc.
171class ChunkStream:
172 def __init__(self, fp: IO[bytes]) -> None:
173 self.fp: IO[bytes] | None = fp
174 self.queue: list[tuple[bytes, int, int]] | None = []
176 def read(self) -> tuple[bytes, int, int]:
177 """Fetch a new chunk. Returns header information."""
178 cid = None
180 assert self.fp is not None
181 if self.queue:
182 cid, pos, length = self.queue.pop()
183 self.fp.seek(pos)
184 else:
185 s = self.fp.read(8)
186 cid = s[4:]
187 pos = self.fp.tell()
188 length = i32(s)
190 if not is_cid(cid):
191 if not ImageFile.LOAD_TRUNCATED_IMAGES:
192 msg = f"broken PNG file (chunk {repr(cid)})"
193 raise SyntaxError(msg)
195 return cid, pos, length
197 def __enter__(self) -> Self:
198 return self
200 def __exit__(self, *args: object) -> None:
201 self.close()
203 def close(self) -> None:
204 self.queue = self.fp = None
206 def push(self, cid: bytes, pos: int, length: int) -> None:
207 assert self.queue is not None
208 self.queue.append((cid, pos, length))
210 def call(self, cid: bytes, pos: int, length: int) -> bytes:
211 """Call the appropriate chunk handler"""
213 logger.debug("STREAM %r %s %s", cid, pos, length)
214 return getattr(self, f"chunk_{cid.decode('ascii')}")(pos, length)
216 def crc(self, cid: bytes, data: bytes) -> None:
217 """Read and verify checksum"""
219 # Skip CRC checks for ancillary chunks if allowed to load truncated
220 # images
221 # 5th byte of first char is 1 [specs, section 5.4]
222 if ImageFile.LOAD_TRUNCATED_IMAGES and (cid[0] >> 5 & 1):
223 self.crc_skip(cid, data)
224 return
226 assert self.fp is not None
227 try:
228 crc1 = _crc32(data, _crc32(cid))
229 crc2 = i32(self.fp.read(4))
230 if crc1 != crc2:
231 msg = f"broken PNG file (bad header checksum in {repr(cid)})"
232 raise SyntaxError(msg)
233 except struct.error as e:
234 msg = f"broken PNG file (incomplete checksum in {repr(cid)})"
235 raise SyntaxError(msg) from e
237 def crc_skip(self, cid: bytes, data: bytes) -> None:
238 """Read checksum"""
240 assert self.fp is not None
241 self.fp.read(4)
243 def verify(self, endchunk: bytes = b"IEND") -> list[bytes]:
244 # Simple approach; just calculate checksum for all remaining
245 # blocks. Must be called directly after open.
247 cids = []
249 assert self.fp is not None
250 while True:
251 try:
252 cid, pos, length = self.read()
253 except struct.error as e:
254 msg = "truncated PNG file"
255 raise OSError(msg) from e
257 if cid == endchunk:
258 break
259 self.crc(cid, ImageFile._safe_read(self.fp, length))
260 cids.append(cid)
262 return cids
265class iTXt(str):
266 """
267 Subclass of string to allow iTXt chunks to look like strings while
268 keeping their extra information
270 """
272 lang: str | bytes | None
273 tkey: str | bytes | None
275 @staticmethod
276 def __new__(
277 cls, text: str, lang: str | None = None, tkey: str | None = None
278 ) -> Self:
279 """
280 :param cls: the class to use when creating the instance
281 :param text: value for this key
282 :param lang: language code
283 :param tkey: UTF-8 version of the key name
284 """
286 self = str.__new__(cls, text)
287 self.lang = lang
288 self.tkey = tkey
289 return self
292class PngInfo:
293 """
294 PNG chunk container (for use with save(pnginfo=))
296 """
298 def __init__(self) -> None:
299 self.chunks: list[tuple[bytes, bytes, bool]] = []
301 def add(self, cid: bytes, data: bytes, after_idat: bool = False) -> None:
302 """Appends an arbitrary chunk. Use with caution.
304 :param cid: a byte string, 4 bytes long.
305 :param data: a byte string of the encoded data
306 :param after_idat: for use with private chunks. Whether the chunk
307 should be written after IDAT
309 """
311 self.chunks.append((cid, data, after_idat))
313 def _check_text_key(self, key: bytes) -> None:
314 if not 1 <= len(key) <= 79:
315 msg = "Text key must be 1 to 79 characters long"
316 raise ValueError(msg)
318 if b"\0" in key:
319 # The NULL character is the separator after the key
320 msg = "Text key cannot contain null characters"
321 raise ValueError(msg)
323 def add_itxt(
324 self,
325 key: str | bytes,
326 value: str | bytes,
327 lang: str | bytes = "",
328 tkey: str | bytes = "",
329 zip: bool = False,
330 ) -> None:
331 """Appends an iTXt chunk.
333 :param key: latin-1 encodable text key name. 1 to 79 characters long
334 :param value: value for this key
335 :param lang: language code
336 :param tkey: UTF-8 version of the key name
337 :param zip: compression flag
339 """
341 if not isinstance(key, bytes):
342 key = key.encode("latin-1", "strict")
343 self._check_text_key(key)
344 if not isinstance(value, bytes):
345 value = value.encode("utf-8", "strict")
346 if not isinstance(lang, bytes):
347 lang = lang.encode("utf-8", "strict")
348 if not isinstance(tkey, bytes):
349 tkey = tkey.encode("utf-8", "strict")
351 if zip:
352 self.add(
353 b"iTXt",
354 key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value),
355 )
356 else:
357 self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value)
359 def add_text(
360 self, key: str | bytes, value: str | bytes | iTXt, zip: bool = False
361 ) -> None:
362 """Appends a text chunk.
364 :param key: latin-1 encodable text key name. 1 to 79 characters long
365 :param value: value for this key, text or an
366 :py:class:`PIL.PngImagePlugin.iTXt` instance
367 :param zip: compression flag
369 """
370 if isinstance(value, iTXt):
371 return self.add_itxt(
372 key,
373 value,
374 value.lang if value.lang is not None else b"",
375 value.tkey if value.tkey is not None else b"",
376 zip=zip,
377 )
379 # The tEXt chunk stores latin-1 text
380 if not isinstance(value, bytes):
381 try:
382 value = value.encode("latin-1", "strict")
383 except UnicodeError:
384 return self.add_itxt(key, value, zip=zip)
386 if not isinstance(key, bytes):
387 key = key.encode("latin-1", "strict")
388 self._check_text_key(key)
390 if zip:
391 self.add(b"zTXt", key + b"\0\0" + zlib.compress(value))
392 else:
393 self.add(b"tEXt", key + b"\0" + value)
396# --------------------------------------------------------------------
397# PNG image stream (IHDR/IEND)
400class _RewindState(NamedTuple):
401 info: dict[str | tuple[int, int], Any]
402 tile: list[ImageFile._Tile]
403 seq_num: int | None
406class PngStream(ChunkStream):
407 def __init__(self, fp: IO[bytes]) -> None:
408 super().__init__(fp)
410 # local copies of Image attributes
411 self.im_info: dict[str | tuple[int, int], Any] = {}
412 self.im_text: dict[str, str | iTXt] = {}
413 self.im_size = (0, 0)
414 self.im_mode = ""
415 self.im_tile: list[ImageFile._Tile] = []
416 self.im_palette: tuple[str, bytes] | None = None
417 self.im_custom_mimetype: str | None = None
418 self.im_n_frames: int | None = None
419 self._seq_num: int | None = None
420 self.rewind_state = _RewindState({}, [], None)
422 self.text_memory = 0
424 def check_text_memory(self, chunklen: int) -> None:
425 self.text_memory += chunklen
426 if self.text_memory > MAX_TEXT_MEMORY:
427 msg = (
428 "Too much memory used in text chunks: "
429 f"{self.text_memory}>MAX_TEXT_MEMORY"
430 )
431 raise ValueError(msg)
433 def save_rewind(self) -> None:
434 self.rewind_state = _RewindState(
435 self.im_info.copy(),
436 self.im_tile,
437 self._seq_num,
438 )
440 def rewind(self) -> None:
441 self.im_info = self.rewind_state.info.copy()
442 self.im_tile = self.rewind_state.tile
443 self._seq_num = self.rewind_state.seq_num
445 def chunk_iCCP(self, pos: int, length: int) -> bytes:
446 # ICC profile
447 assert self.fp is not None
448 s = ImageFile._safe_read(self.fp, length)
449 # according to PNG spec, the iCCP chunk contains:
450 # Profile name 1-79 bytes (character string)
451 # Null separator 1 byte (null character)
452 # Compression method 1 byte (0)
453 # Compressed profile n bytes (zlib with deflate compression)
454 i = s.find(b"\0")
455 logger.debug("iCCP profile name %r", s[:i])
456 comp_method = s[i + 1]
457 logger.debug("Compression method %s", comp_method)
458 if comp_method != 0:
459 msg = f"Unknown compression method {comp_method} in iCCP chunk"
460 raise SyntaxError(msg)
461 try:
462 icc_profile = _safe_zlib_decompress(s[i + 2 :])
463 except ValueError:
464 if ImageFile.LOAD_TRUNCATED_IMAGES:
465 icc_profile = None
466 else:
467 raise
468 except zlib.error:
469 icc_profile = None # FIXME
470 self.im_info["icc_profile"] = icc_profile
471 return s
473 def chunk_IHDR(self, pos: int, length: int) -> bytes:
474 # image header
475 assert self.fp is not None
476 s = ImageFile._safe_read(self.fp, length)
477 if length < 13:
478 if ImageFile.LOAD_TRUNCATED_IMAGES:
479 return s
480 msg = "Truncated IHDR chunk"
481 raise ValueError(msg)
482 self.im_size = i32(s, 0), i32(s, 4)
483 try:
484 self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])]
485 except KeyError:
486 pass
487 if s[12]:
488 self.im_info["interlace"] = 1
489 if s[11]:
490 msg = "unknown filter category"
491 raise SyntaxError(msg)
492 return s
494 def chunk_IDAT(self, pos: int, length: int) -> NoReturn:
495 # image data
496 if "bbox" in self.im_info:
497 tile = [ImageFile._Tile("zip", self.im_info["bbox"], pos, self.im_rawmode)]
498 else:
499 if self.im_n_frames is not None:
500 self.im_info["default_image"] = True
501 tile = [ImageFile._Tile("zip", None, pos, self.im_rawmode)]
502 self.im_tile = tile
503 self.im_idat = length
504 msg = "image data found"
505 raise EOFError(msg)
507 def chunk_IEND(self, pos: int, length: int) -> NoReturn:
508 msg = "end of PNG image"
509 raise EOFError(msg)
511 def chunk_PLTE(self, pos: int, length: int) -> bytes:
512 # palette
513 assert self.fp is not None
514 s = ImageFile._safe_read(self.fp, length)
515 if self.im_mode == "P":
516 self.im_palette = "RGB", s
517 return s
519 def chunk_tRNS(self, pos: int, length: int) -> bytes:
520 # transparency
521 assert self.fp is not None
522 s = ImageFile._safe_read(self.fp, length)
523 if self.im_mode == "P":
524 if _simple_palette.match(s):
525 # tRNS contains only one full-transparent entry,
526 # other entries are full opaque
527 i = s.find(b"\0")
528 if i >= 0:
529 self.im_info["transparency"] = i
530 else:
531 # otherwise, we have a byte string with one alpha value
532 # for each palette entry
533 self.im_info["transparency"] = s
534 elif self.im_mode in ("1", "L", "I;16", "RGB"):
535 # 2 bytes for greyscale, 6 for truecolour
536 if length < (6 if self.im_mode == "RGB" else 2):
537 if ImageFile.LOAD_TRUNCATED_IMAGES:
538 return s
539 msg = "Truncated tRNS chunk"
540 raise ValueError(msg)
541 if self.im_mode == "1":
542 self.im_info["transparency"] = 255 if i16(s) else 0
543 elif self.im_mode in ("L", "I;16"):
544 value = i16(s)
545 if self.im_rawmode in {"L;2", "L;4"}:
546 # Scale the grey level to match the unpacked 8-bit pixels
547 max_value = (1 << int(self.im_rawmode[-1])) - 1
548 value = int(min(1, value / max_value) * 255)
549 self.im_info["transparency"] = value
550 elif self.im_mode == "RGB":
551 self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4)
552 return s
554 def chunk_gAMA(self, pos: int, length: int) -> bytes:
555 # gamma setting
556 assert self.fp is not None
557 s = ImageFile._safe_read(self.fp, length)
558 if length < 4:
559 if ImageFile.LOAD_TRUNCATED_IMAGES:
560 return s
561 msg = "Truncated gAMA chunk"
562 raise ValueError(msg)
563 self.im_info["gamma"] = i32(s) / 100000.0
564 return s
566 def chunk_cHRM(self, pos: int, length: int) -> bytes:
567 # chromaticity, 8 unsigned ints, actual value is scaled by 100,000
568 # WP x,y, Red x,y, Green x,y Blue x,y
570 assert self.fp is not None
571 s = ImageFile._safe_read(self.fp, length)
572 if length < 32:
573 if ImageFile.LOAD_TRUNCATED_IMAGES:
574 return s
575 msg = "Truncated cHRM chunk"
576 raise ValueError(msg)
577 raw_vals = struct.unpack(">8I", s[:32])
578 self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals)
579 return s
581 def chunk_sRGB(self, pos: int, length: int) -> bytes:
582 # srgb rendering intent, 1 byte
583 # 0 perceptual
584 # 1 relative colorimetric
585 # 2 saturation
586 # 3 absolute colorimetric
588 assert self.fp is not None
589 s = ImageFile._safe_read(self.fp, length)
590 if length < 1:
591 if ImageFile.LOAD_TRUNCATED_IMAGES:
592 return s
593 msg = "Truncated sRGB chunk"
594 raise ValueError(msg)
595 self.im_info["srgb"] = s[0]
596 return s
598 def chunk_pHYs(self, pos: int, length: int) -> bytes:
599 # pixels per unit
600 assert self.fp is not None
601 s = ImageFile._safe_read(self.fp, length)
602 if length < 9:
603 if ImageFile.LOAD_TRUNCATED_IMAGES:
604 return s
605 msg = "Truncated pHYs chunk"
606 raise ValueError(msg)
607 px, py = i32(s, 0), i32(s, 4)
608 unit = s[8]
609 if unit == 1: # meter
610 dpi = px * 0.0254, py * 0.0254
611 self.im_info["dpi"] = dpi
612 elif unit == 0:
613 self.im_info["aspect"] = px, py
614 return s
616 def chunk_tEXt(self, pos: int, length: int) -> bytes:
617 # text
618 assert self.fp is not None
619 s = ImageFile._safe_read(self.fp, length)
620 try:
621 k, v = s.split(b"\0", 1)
622 except ValueError:
623 # fallback for broken tEXt tags
624 k = s
625 v = b""
626 if k:
627 k_str = k.decode("latin-1", "strict")
628 v_str = v.decode("latin-1", "replace")
630 self.im_info[k_str] = v if k == b"exif" else v_str
631 self.im_text[k_str] = v_str
632 self.check_text_memory(len(v_str))
634 return s
636 def chunk_zTXt(self, pos: int, length: int) -> bytes:
637 # compressed text
638 assert self.fp is not None
639 s = ImageFile._safe_read(self.fp, length)
640 try:
641 k, v = s.split(b"\0", 1)
642 except ValueError:
643 k = s
644 v = b""
645 if v:
646 comp_method = v[0]
647 else:
648 comp_method = 0
649 if comp_method != 0:
650 msg = f"Unknown compression method {comp_method} in zTXt chunk"
651 raise SyntaxError(msg)
652 try:
653 v = _safe_zlib_decompress(v[1:])
654 except ValueError:
655 if ImageFile.LOAD_TRUNCATED_IMAGES:
656 v = b""
657 else:
658 raise
659 except zlib.error:
660 v = b""
662 if k:
663 k_str = k.decode("latin-1", "strict")
664 v_str = v.decode("latin-1", "replace")
666 self.im_info[k_str] = self.im_text[k_str] = v_str
667 self.check_text_memory(len(v_str))
669 return s
671 def chunk_iTXt(self, pos: int, length: int) -> bytes:
672 # international text
673 assert self.fp is not None
674 r = s = ImageFile._safe_read(self.fp, length)
675 try:
676 k, r = r.split(b"\0", 1)
677 except ValueError:
678 return s
679 if len(r) < 2:
680 return s
681 cf, cm, r = r[0], r[1], r[2:]
682 try:
683 lang, tk, v = r.split(b"\0", 2)
684 except ValueError:
685 return s
686 if cf != 0:
687 if cm == 0:
688 try:
689 v = _safe_zlib_decompress(v)
690 except ValueError:
691 if ImageFile.LOAD_TRUNCATED_IMAGES:
692 return s
693 else:
694 raise
695 except zlib.error:
696 return s
697 else:
698 return s
699 if k == b"XML:com.adobe.xmp":
700 self.im_info["xmp"] = v
701 try:
702 k_str = k.decode("latin-1", "strict")
703 lang_str = lang.decode("utf-8", "strict")
704 tk_str = tk.decode("utf-8", "strict")
705 v_str = v.decode("utf-8", "strict")
706 except UnicodeError:
707 return s
709 self.im_info[k_str] = self.im_text[k_str] = iTXt(v_str, lang_str, tk_str)
710 self.check_text_memory(len(v_str))
712 return s
714 def chunk_eXIf(self, pos: int, length: int) -> bytes:
715 assert self.fp is not None
716 s = ImageFile._safe_read(self.fp, length)
717 self.im_info["exif"] = b"Exif\x00\x00" + s
718 return s
720 # APNG chunks
721 def chunk_acTL(self, pos: int, length: int) -> bytes:
722 assert self.fp is not None
723 s = ImageFile._safe_read(self.fp, length)
724 if length < 8:
725 if ImageFile.LOAD_TRUNCATED_IMAGES:
726 return s
727 msg = "APNG contains truncated acTL chunk"
728 raise ValueError(msg)
729 if self.im_n_frames is not None:
730 self.im_n_frames = None
731 warnings.warn("Invalid APNG, will use default PNG image if possible")
732 return s
733 n_frames = i32(s)
734 if n_frames == 0 or n_frames > 0x80000000:
735 warnings.warn("Invalid APNG, will use default PNG image if possible")
736 return s
737 self.im_n_frames = n_frames
738 self.im_info["loop"] = i32(s, 4)
739 self.im_custom_mimetype = "image/apng"
740 return s
742 def chunk_fcTL(self, pos: int, length: int) -> bytes:
743 assert self.fp is not None
744 s = ImageFile._safe_read(self.fp, length)
745 if length < 26:
746 if ImageFile.LOAD_TRUNCATED_IMAGES:
747 return s
748 msg = "APNG contains truncated fcTL chunk"
749 raise ValueError(msg)
750 seq = i32(s)
751 if (self._seq_num is None and seq != 0) or (
752 self._seq_num is not None and self._seq_num != seq - 1
753 ):
754 msg = "APNG contains frame sequence errors"
755 raise SyntaxError(msg)
756 self._seq_num = seq
757 width, height = i32(s, 4), i32(s, 8)
758 px, py = i32(s, 12), i32(s, 16)
759 im_w, im_h = self.im_size
760 if px + width > im_w or py + height > im_h:
761 msg = "APNG contains invalid frames"
762 raise SyntaxError(msg)
763 self.im_info["bbox"] = (px, py, px + width, py + height)
764 delay_num, delay_den = i16(s, 20), i16(s, 22)
765 if delay_den == 0:
766 delay_den = 100
767 self.im_info["duration"] = float(delay_num) / float(delay_den) * 1000
768 self.im_info["disposal"] = s[24]
769 self.im_info["blend"] = s[25]
770 return s
772 def chunk_fdAT(self, pos: int, length: int) -> bytes:
773 assert self.fp is not None
774 if length < 4:
775 if ImageFile.LOAD_TRUNCATED_IMAGES:
776 s = ImageFile._safe_read(self.fp, length)
777 return s
778 msg = "APNG contains truncated fDAT chunk"
779 raise ValueError(msg)
780 s = ImageFile._safe_read(self.fp, 4)
781 seq = i32(s)
782 if self._seq_num != seq - 1:
783 msg = "APNG contains frame sequence errors"
784 raise SyntaxError(msg)
785 self._seq_num = seq
786 return self.chunk_IDAT(pos + 4, length - 4)
789# --------------------------------------------------------------------
790# PNG reader
793def _accept(prefix: bytes) -> bool:
794 return prefix.startswith(_MAGIC)
797##
798# Image plugin for PNG images.
801class PngImageFile(ImageFile.ImageFile):
802 format = "PNG"
803 format_description = "Portable network graphics"
805 def _open(self) -> None:
806 assert self.fp is not None
807 if not _accept(self.fp.read(8)):
808 msg = "not a PNG file"
809 raise SyntaxError(msg)
810 self._fp = self.fp
811 self.__frame = 0
813 #
814 # Parse headers up to the first IDAT or fDAT chunk
816 self.private_chunks: list[tuple[bytes, bytes] | tuple[bytes, bytes, bool]] = []
817 self.png: PngStream | None = PngStream(self.fp)
819 while True:
820 #
821 # get next chunk
823 cid, pos, length = self.png.read()
825 try:
826 s = self.png.call(cid, pos, length)
827 except EOFError:
828 break
829 except AttributeError:
830 logger.debug("%r %s %s (unknown)", cid, pos, length)
831 s = ImageFile._safe_read(self.fp, length)
832 if cid[1:2].islower():
833 self.private_chunks.append((cid, s))
835 self.png.crc(cid, s)
837 #
838 # Copy relevant attributes from the PngStream. An alternative
839 # would be to let the PngStream class modify these attributes
840 # directly, but that introduces circular references which are
841 # difficult to break if things go wrong in the decoder...
842 # (believe me, I've tried ;-)
844 self._mode = self.png.im_mode
845 self._size = self.png.im_size
846 self.info = self.png.im_info
847 self._text: dict[str, str | iTXt] | None = None
848 self.tile = self.png.im_tile
849 self.custom_mimetype = self.png.im_custom_mimetype
850 self.n_frames = self.png.im_n_frames or 1
851 self.default_image = self.info.get("default_image", False)
853 if self.png.im_palette:
854 rawmode, data = self.png.im_palette
855 self.palette = ImagePalette.raw(rawmode, data)
857 if cid == b"fdAT":
858 self.__prepare_idat = length - 4
859 else:
860 self.__prepare_idat = length # used by load_prepare()
862 if self.png.im_n_frames is not None:
863 self._close_exclusive_fp_after_loading = False
864 self.png.save_rewind()
865 self.__rewind_idat = self.__prepare_idat
866 self.__rewind = self._fp.tell()
867 if self.default_image:
868 # IDAT chunk contains default image and not first animation frame
869 self.n_frames += 1
870 self._seek(0)
871 self.is_animated = self.n_frames > 1
873 @property
874 def text(self) -> dict[str, str | iTXt]:
875 # experimental
876 if self._text is None:
877 # iTxt, tEXt and zTXt chunks may appear at the end of the file
878 # So load the file to ensure that they are read
879 if self.is_animated:
880 frame = self.__frame
881 # for APNG, seek to the final frame before loading
882 self.seek(self.n_frames - 1)
883 self.load()
884 if self.is_animated:
885 self.seek(frame)
886 assert self._text is not None
887 return self._text
889 def verify(self) -> None:
890 """Verify PNG file"""
892 if self.fp is None:
893 msg = "verify must be called directly after open"
894 raise RuntimeError(msg)
896 # back up to beginning of IDAT block
897 self.fp.seek(self.tile[0][2] - 8)
899 assert self.png is not None
900 self.png.verify()
901 self.png.close()
903 super().verify()
905 def seek(self, frame: int) -> None:
906 if not self._seek_check(frame):
907 return
908 if frame < self.__frame:
909 self._seek(0, True)
911 last_frame = self.__frame
912 try:
913 for f in range(self.__frame + 1, frame + 1):
914 self._seek(f)
915 except EOFError as e:
916 self.seek(last_frame)
917 msg = "no more images in APNG file"
918 raise EOFError(msg) from e
920 def _seek(self, frame: int, rewind: bool = False) -> None:
921 assert self.png is not None
922 if isinstance(self._fp, DeferredError):
923 raise self._fp.ex
925 dispose_extent = None
926 if frame == 0:
927 if rewind:
928 self._fp.seek(self.__rewind)
929 self.png.rewind()
930 self.__prepare_idat = self.__rewind_idat
931 self._im = None
932 self.info = self.png.im_info
933 self.tile = self.png.im_tile
934 self.fp = self._fp
935 self._prev_im: _imaging.ImagingCore | None = None
936 self.default_image = self.info.get("default_image", False)
937 self.dispose_op = self.info.get("disposal")
938 self.blend_op = self.info.get("blend")
939 dispose_extent = self.info.get("bbox")
940 self.__frame = 0
941 else:
942 if frame != self.__frame + 1:
943 msg = f"cannot seek to frame {frame}"
944 raise ValueError(msg)
946 if self.dispose_op == Disposal.OP_BACKGROUND:
947 x0, y0, x1, y1 = self.dispose_extent
948 dispose = Image.core.fill(self.mode, (x1 - x0, y1 - y0))
949 elif self.dispose_op == Disposal.OP_PREVIOUS and self._prev_im is not None:
950 dispose = self._crop(self._prev_im.copy(), self.dispose_extent)
951 else:
952 dispose = None
954 # ensure previous frame was loaded
955 self.load()
957 if dispose:
958 self.im.paste(dispose, self.dispose_extent)
959 self._prev_im = self.im.copy()
961 self.fp = self._fp
963 # advance to the next frame
964 if self.__prepare_idat:
965 ImageFile._safe_read(self.fp, self.__prepare_idat)
966 self.__prepare_idat = 0
967 frame_start = False
968 while True:
969 self.fp.read(4) # CRC
971 try:
972 cid, pos, length = self.png.read()
973 except (struct.error, SyntaxError):
974 break
976 if cid == b"IEND":
977 msg = "No more images in APNG file"
978 raise EOFError(msg)
979 if cid == b"fcTL":
980 if frame_start:
981 # there must be at least one fdAT chunk between fcTL chunks
982 msg = "APNG missing frame data"
983 raise SyntaxError(msg)
984 frame_start = True
986 try:
987 self.png.call(cid, pos, length)
988 except UnicodeDecodeError:
989 break
990 except EOFError:
991 if cid == b"fdAT":
992 length -= 4
993 if frame_start:
994 self.__prepare_idat = length
995 break
996 ImageFile._safe_read(self.fp, length)
997 except AttributeError:
998 logger.debug("%r %s %s (unknown)", cid, pos, length)
999 ImageFile._safe_read(self.fp, length)
1001 self.__frame = frame
1002 self.tile = self.png.im_tile
1003 self.dispose_op = self.info.get("disposal")
1004 self.blend_op = self.info.get("blend")
1005 dispose_extent = self.info.get("bbox")
1007 if not self.tile:
1008 msg = "image not found in APNG frame"
1009 raise EOFError(msg)
1010 if dispose_extent:
1011 self.dispose_extent: tuple[float, float, float, float] = dispose_extent
1013 # setup frame disposal (actual disposal done when needed in the next _seek())
1014 if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS:
1015 self.dispose_op = Disposal.OP_BACKGROUND
1017 def tell(self) -> int:
1018 return self.__frame
1020 def load_prepare(self) -> None:
1021 """internal: prepare to read PNG file"""
1023 if self.info.get("interlace"):
1024 self.decoderconfig = self.decoderconfig + (1,)
1026 self.__idat = self.__prepare_idat # used by load_read()
1027 ImageFile.ImageFile.load_prepare(self)
1029 def load_read(self, read_bytes: int) -> bytes:
1030 """internal: read more image data"""
1032 assert self.png is not None
1033 assert self.fp is not None
1034 while self.__idat == 0:
1035 # end of chunk, skip forward to next one
1037 self.fp.read(4) # CRC
1039 cid, pos, length = self.png.read()
1041 if cid not in [b"IDAT", b"DDAT", b"fdAT"]:
1042 self.png.push(cid, pos, length)
1043 return b""
1045 if cid == b"fdAT":
1046 try:
1047 self.png.call(cid, pos, length)
1048 except EOFError:
1049 pass
1050 self.__idat = length - 4 # sequence_num has already been read
1051 else:
1052 self.__idat = length # empty chunks are allowed
1054 # read more data from this chunk
1055 if read_bytes <= 0:
1056 read_bytes = self.__idat
1057 else:
1058 read_bytes = min(read_bytes, self.__idat)
1060 self.__idat = self.__idat - read_bytes
1062 return self.fp.read(read_bytes)
1064 def load_end(self) -> None:
1065 """internal: finished reading image data"""
1066 assert self.png is not None
1067 assert self.fp is not None
1068 if self.__idat != 0:
1069 self.fp.read(self.__idat)
1070 while True:
1071 self.fp.read(4) # CRC
1073 try:
1074 cid, pos, length = self.png.read()
1075 except (struct.error, SyntaxError):
1076 break
1078 if cid == b"IEND":
1079 break
1080 elif cid == b"fcTL" and self.is_animated:
1081 # start of the next frame, stop reading
1082 self.__prepare_idat = 0
1083 self.png.push(cid, pos, length)
1084 break
1086 try:
1087 self.png.call(cid, pos, length)
1088 except UnicodeDecodeError:
1089 break
1090 except EOFError:
1091 if cid == b"fdAT":
1092 length -= 4
1093 try:
1094 ImageFile._safe_read(self.fp, length)
1095 except OSError as e:
1096 if ImageFile.LOAD_TRUNCATED_IMAGES:
1097 break
1098 else:
1099 raise e
1100 except AttributeError:
1101 logger.debug("%r %s %s (unknown)", cid, pos, length)
1102 s = ImageFile._safe_read(self.fp, length)
1103 if cid[1:2].islower():
1104 self.private_chunks.append((cid, s, True))
1105 self._text = self.png.im_text
1106 if not self.is_animated:
1107 self.png.close()
1108 self.png = None
1109 else:
1110 if self._prev_im and self.blend_op == Blend.OP_OVER:
1111 updated = self._crop(self.im, self.dispose_extent)
1112 if self.im.mode == "RGB" and "transparency" in self.info:
1113 mask = updated.convert_transparent(
1114 "RGBA", self.info["transparency"]
1115 )
1116 else:
1117 if self.im.mode == "P" and "transparency" in self.info:
1118 t = self.info["transparency"]
1119 if isinstance(t, bytes):
1120 updated.putpalettealphas(t)
1121 elif isinstance(t, int):
1122 updated.putpalettealpha(t)
1123 mask = updated.convert("RGBA")
1124 if self.dispose_op == Disposal.OP_PREVIOUS:
1125 # Keep the previous image unchanged for disposal
1126 self.im = self._prev_im.copy()
1127 else:
1128 self.im = self._prev_im
1129 self.im.paste(updated, self.dispose_extent, mask)
1131 def _getexif(self) -> dict[int, Any] | None:
1132 if "exif" not in self.info:
1133 self.load()
1134 if "exif" not in self.info and "Raw profile type exif" not in self.info:
1135 return None
1136 return self.getexif()._get_merged_dict()
1138 def getexif(self) -> Image.Exif:
1139 if "exif" not in self.info:
1140 self.load()
1142 return super().getexif()
1145# --------------------------------------------------------------------
1146# PNG writer
1148_OUTMODES = {
1149 # supported PIL modes, and corresponding rawmode, bit depth and color type
1150 "1": ("1", b"\x01", b"\x00"),
1151 "L": ("L", b"\x08", b"\x00"),
1152 "L;1": ("L;1", b"\x01", b"\x00"),
1153 "L;2": ("L;2", b"\x02", b"\x00"),
1154 "L;4": ("L;4", b"\x04", b"\x00"),
1155 "LA": ("LA", b"\x08", b"\x04"),
1156 "I;16": ("I;16B", b"\x10", b"\x00"),
1157 "I;16B": ("I;16B", b"\x10", b"\x00"),
1158 "P;1": ("P;1", b"\x01", b"\x03"),
1159 "P;2": ("P;2", b"\x02", b"\x03"),
1160 "P;4": ("P;4", b"\x04", b"\x03"),
1161 "P": ("P", b"\x08", b"\x03"),
1162 "RGB": ("RGB", b"\x08", b"\x02"),
1163 "RGBA": ("RGBA", b"\x08", b"\x06"),
1164}
1167def putchunk(fp: IO[bytes], cid: bytes, *data: bytes) -> None:
1168 """Write a PNG chunk (including CRC field)"""
1170 byte_data = b"".join(data)
1172 fp.write(o32(len(byte_data)) + cid)
1173 fp.write(byte_data)
1174 crc = _crc32(byte_data, _crc32(cid))
1175 fp.write(o32(crc))
1178class _idat:
1179 # wrap output from the encoder in IDAT chunks
1181 def __init__(self, fp: IO[bytes], chunk: Callable[..., None]) -> None:
1182 self.fp = fp
1183 self.chunk = chunk
1185 def write(self, data: bytes) -> None:
1186 self.chunk(self.fp, b"IDAT", data)
1189class _fdat:
1190 # wrap encoder output in fdAT chunks
1192 def __init__(self, fp: IO[bytes], chunk: Callable[..., None], seq_num: int) -> None:
1193 self.fp = fp
1194 self.chunk = chunk
1195 self.seq_num = seq_num
1197 def write(self, data: bytes) -> None:
1198 self.chunk(self.fp, b"fdAT", o32(self.seq_num), data)
1199 self.seq_num += 1
1202def _apply_encoderinfo(im: Image.Image, encoderinfo: dict[str, Any]) -> None:
1203 im.encoderconfig = (
1204 encoderinfo.get("optimize", False),
1205 encoderinfo.get("compress_level", -1),
1206 encoderinfo.get("compress_type", -1),
1207 encoderinfo.get("dictionary", b""),
1208 )
1211class _Frame(NamedTuple):
1212 im: Image.Image
1213 bbox: tuple[int, int, int, int] | None
1214 encoderinfo: dict[str, Any]
1217def _write_multiple_frames(
1218 im: Image.Image,
1219 fp: IO[bytes],
1220 chunk: Callable[..., None],
1221 mode: str,
1222 rawmode: str,
1223 default_image: Image.Image | None,
1224 append_images: list[Image.Image],
1225) -> Image.Image | None:
1226 duration = im.encoderinfo.get("duration")
1227 loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
1228 disposal = im.encoderinfo.get("disposal")
1229 blend = im.encoderinfo.get("blend")
1231 if default_image:
1232 chain = itertools.chain(append_images)
1233 else:
1234 chain = itertools.chain([im], append_images)
1236 im_frames: list[_Frame] = []
1237 frame_count = 0
1238 for im_seq in chain:
1239 for im_frame in ImageSequence.Iterator(im_seq):
1240 if im_frame.mode == mode:
1241 im_frame = im_frame.copy()
1242 else:
1243 im_frame = im_frame.convert(mode)
1244 encoderinfo = im.encoderinfo.copy()
1245 if isinstance(duration, (list, tuple)):
1246 encoderinfo["duration"] = duration[frame_count]
1247 elif duration is None and "duration" in im_frame.info:
1248 encoderinfo["duration"] = im_frame.info["duration"]
1249 if isinstance(disposal, (list, tuple)):
1250 encoderinfo["disposal"] = disposal[frame_count]
1251 if isinstance(blend, (list, tuple)):
1252 encoderinfo["blend"] = blend[frame_count]
1253 frame_count += 1
1255 if im_frames:
1256 previous = im_frames[-1]
1257 if mode.startswith("I;16"):
1258 delta = ImageChops.subtract_modulo(
1259 Image.frombytes("LA", im.size, im_frame.tobytes()),
1260 Image.frombytes("LA", im.size, previous.im.tobytes()),
1261 )
1262 else:
1263 delta = ImageChops.subtract_modulo(im_frame, previous.im)
1264 bbox = delta.getbbox(alpha_only=False)
1265 if not bbox and "duration" in encoderinfo:
1266 new_duration = (
1267 previous.encoderinfo["duration"] + encoderinfo["duration"]
1268 )
1269 delay = Fraction(new_duration / 1000).limit_denominator(65535)
1270 if delay.numerator <= 65535:
1271 previous.encoderinfo["duration"] = new_duration
1272 continue
1273 else:
1274 bbox = None
1275 im_frames.append(_Frame(im_frame, bbox, encoderinfo))
1277 if len(im_frames) == 1 and not default_image:
1278 return im_frames[0].im
1280 # animation control
1281 chunk(
1282 fp,
1283 b"acTL",
1284 o32(len(im_frames)), # 0: num_frames
1285 o32(loop), # 4: num_plays
1286 )
1288 # default image IDAT (if it exists)
1289 if default_image:
1290 default_im = im if im.mode == mode else im.convert(mode)
1291 _apply_encoderinfo(default_im, im.encoderinfo)
1292 ImageFile._save(
1293 default_im,
1294 cast("IO[bytes]", _idat(fp, chunk)),
1295 [ImageFile._Tile("zip", args=rawmode)],
1296 )
1298 seq_num = 0
1299 for frame, frame_data in enumerate(im_frames):
1300 im_frame = frame_data.im
1301 if not frame_data.bbox:
1302 bbox = (0, 0, *im_frame.size)
1303 else:
1304 bbox = frame_data.bbox
1305 im_frame = im_frame.crop(bbox)
1306 size = im_frame.size
1307 encoderinfo = frame_data.encoderinfo
1308 frame_duration = encoderinfo.get("duration", 0)
1309 delay = Fraction(frame_duration / 1000).limit_denominator(65535)
1310 if delay.numerator > 65535:
1311 msg = "cannot write duration"
1312 raise ValueError(msg)
1313 frame_disposal = encoderinfo.get("disposal", Disposal.OP_NONE)
1314 frame_blend = encoderinfo.get("blend", Blend.OP_SOURCE)
1315 # frame control
1316 chunk(
1317 fp,
1318 b"fcTL",
1319 o32(seq_num), # sequence_number
1320 o32(size[0]), # width
1321 o32(size[1]), # height
1322 o32(bbox[0]), # x_offset
1323 o32(bbox[1]), # y_offset
1324 o16(delay.numerator), # delay_numerator
1325 o16(delay.denominator), # delay_denominator
1326 o8(frame_disposal), # dispose_op
1327 o8(frame_blend), # blend_op
1328 )
1329 seq_num += 1
1330 # frame data
1331 _apply_encoderinfo(im_frame, im.encoderinfo)
1332 if frame == 0 and not default_image:
1333 # first frame must be in IDAT chunks for backwards compatibility
1334 ImageFile._save(
1335 im_frame,
1336 cast("IO[bytes]", _idat(fp, chunk)),
1337 [ImageFile._Tile("zip", args=rawmode)],
1338 )
1339 else:
1340 fdat_chunks = _fdat(fp, chunk, seq_num)
1341 ImageFile._save(
1342 im_frame,
1343 cast("IO[bytes]", fdat_chunks),
1344 [ImageFile._Tile("zip", args=rawmode)],
1345 )
1346 seq_num = fdat_chunks.seq_num
1347 return None
1350def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
1351 _save(im, fp, filename, save_all=True)
1354def _save(
1355 im: Image.Image,
1356 fp: IO[bytes],
1357 filename: str | bytes,
1358 chunk: Callable[..., None] = putchunk,
1359 save_all: bool = False,
1360) -> None:
1361 # save an image to disk (called by the save method)
1363 if save_all:
1364 default_image = im.encoderinfo.get(
1365 "default_image", im.info.get("default_image")
1366 )
1367 modes = set()
1368 sizes = set()
1369 palette = None
1370 palette_frame_with_alpha = None
1371 different_palette = None
1372 append_images = im.encoderinfo.get("append_images", [])
1373 for im_seq in itertools.chain([im], append_images):
1374 for im_frame in ImageSequence.Iterator(im_seq):
1375 modes.add(im_frame.mode)
1376 sizes.add(im_frame.size)
1377 if im_frame.mode == "P":
1378 if im_frame.palette and im_frame.palette.mode == "RGBA":
1379 palette_frame_with_alpha = im_frame
1380 if different_palette is None:
1381 frame_palette = im_frame.getpalette() or []
1382 if palette is not None:
1383 if frame_palette != palette:
1384 different_palette = True
1385 else:
1386 palette = frame_palette
1387 elif im_frame.mode in ("RGBA", "RGBA"):
1388 different_palette = False
1389 if different_palette:
1390 # APNG can only contain a single PLTE, so use another mode.
1391 mode = "RGBA" if palette_frame_with_alpha else "RGB"
1392 else:
1393 for mode in ("RGBA", "RGB", "P"):
1394 if mode in modes:
1395 break
1396 else:
1397 mode = modes.pop()
1398 size = tuple(max(frame_size[i] for frame_size in sizes) for i in range(2))
1399 else:
1400 size = im.size
1401 mode = im.mode
1402 if mode == "P" and im.palette:
1403 palette = im.getpalette() or []
1404 palette_frame_with_alpha = im if im.palette.mode == "RGBA" else None
1406 outmode = mode
1407 if mode == "P":
1408 colors = max(
1409 1,
1410 min(
1411 # number of bits specified by user
1412 1 << im.encoderinfo.get("bits", 8),
1413 # write only as many PLTE entries as the palette actually contains
1414 len(palette) // 3 if palette else 0,
1415 256,
1416 ),
1417 )
1418 if colors <= 16:
1419 if colors <= 2:
1420 bits = 1
1421 elif colors <= 4:
1422 bits = 2
1423 else:
1424 bits = 4
1425 outmode += f";{bits}"
1426 elif mode == "L" and im.encoderinfo.get("bits", 8) != 8:
1427 outmode += f";{im.encoderinfo['bits']}"
1429 # get the corresponding PNG mode
1430 try:
1431 rawmode, bit_depth, color_type = _OUTMODES[outmode]
1432 except KeyError as e:
1433 msg = f"cannot write mode {mode} as PNG"
1434 raise OSError(msg) from e
1436 #
1437 # write minimal PNG file
1439 fp.write(_MAGIC)
1441 chunk(
1442 fp,
1443 b"IHDR",
1444 o32(size[0]), # 0: size
1445 o32(size[1]),
1446 bit_depth,
1447 color_type,
1448 b"\0", # 10: compression
1449 b"\0", # 11: filter category
1450 b"\0", # 12: interlace flag
1451 )
1453 chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"]
1455 if icc := im.encoderinfo.get("icc_profile", im.info.get("icc_profile")):
1456 # ICC profile
1457 # according to PNG spec, the iCCP chunk contains:
1458 # Profile name 1-79 bytes (character string)
1459 # Null separator 1 byte (null character)
1460 # Compression method 1 byte (0)
1461 # Compressed profile n bytes (zlib with deflate compression)
1462 name = b"ICC Profile"
1463 data = name + b"\0\0" + zlib.compress(icc)
1464 chunk(fp, b"iCCP", data)
1466 # You must either have sRGB or iCCP.
1467 # Disallow sRGB chunks when an iCCP-chunk has been emitted.
1468 chunks.remove(b"sRGB")
1470 if info := im.encoderinfo.get("pnginfo"):
1471 chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"]
1472 for info_chunk in info.chunks:
1473 cid, data = info_chunk[:2]
1474 if cid in chunks:
1475 chunks.remove(cid)
1476 chunk(fp, cid, data)
1477 elif cid in chunks_multiple_allowed:
1478 chunk(fp, cid, data)
1479 elif cid[1:2].islower():
1480 # Private chunk
1481 after_idat = len(info_chunk) == 3 and info_chunk[2]
1482 if not after_idat:
1483 chunk(fp, cid, data)
1485 if mode == "P":
1486 palette_bytes = (palette and bytes(palette[: colors * 3])) or b"\x00\x00\x00"
1487 chunk(fp, b"PLTE", palette_bytes)
1489 transparency = im.encoderinfo.get("transparency", im.info.get("transparency"))
1491 if transparency is not None:
1492 if mode == "P":
1493 # limit to actual palette size
1494 alpha_bytes = colors
1495 if isinstance(transparency, bytes):
1496 chunk(fp, b"tRNS", transparency[:alpha_bytes])
1497 elif isinstance(transparency, int):
1498 transparency = max(0, min(255, transparency))
1499 alpha = b"\xff" * transparency + b"\0"
1500 chunk(fp, b"tRNS", alpha[:alpha_bytes])
1501 else:
1502 msg = "transparency for P must be an integer or bytes"
1503 raise ValueError(msg)
1504 elif im.mode in ("1", "L", "I", "I;16"):
1505 if isinstance(transparency, int):
1506 transparency = max(0, min(65535, transparency))
1507 chunk(fp, b"tRNS", o16(transparency))
1508 else:
1509 msg = f"transparency for {im.mode} must be an integer"
1510 raise ValueError(msg)
1511 elif im.mode == "RGB":
1512 if not isinstance(transparency, (list, tuple)):
1513 msg = "transparency for RGB must be list or tuple"
1514 raise ValueError(msg)
1515 elif len(transparency) != 3:
1516 msg = "transparency for RGB must have length 3"
1517 raise ValueError(msg)
1518 else:
1519 red, green, blue = transparency
1520 chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue))
1521 elif im.encoderinfo.get("transparency") is not None:
1522 # don't bother with transparency if it's an RGBA
1523 # and it's in the info dict. It's probably just stale.
1524 msg = "cannot use transparency for this mode"
1525 raise OSError(msg)
1526 elif mode == "P" and palette_frame_with_alpha:
1527 alpha = palette_frame_with_alpha.im.getpalette("A")
1528 alpha_bytes = colors
1529 chunk(fp, b"tRNS", alpha[:alpha_bytes])
1531 if dpi := im.encoderinfo.get("dpi"):
1532 chunk(
1533 fp,
1534 b"pHYs",
1535 o32(int(dpi[0] / 0.0254 + 0.5)),
1536 o32(int(dpi[1] / 0.0254 + 0.5)),
1537 b"\x01",
1538 )
1540 if info:
1541 chunks = [b"bKGD", b"hIST"]
1542 for info_chunk in info.chunks:
1543 cid, data = info_chunk[:2]
1544 if cid in chunks:
1545 chunks.remove(cid)
1546 chunk(fp, cid, data)
1548 if exif := im.encoderinfo.get("exif"):
1549 if isinstance(exif, Image.Exif):
1550 exif = exif.tobytes(8)
1551 if exif.startswith(b"Exif\x00\x00"):
1552 exif = exif[6:]
1553 chunk(fp, b"eXIf", exif)
1555 single_im: Image.Image | None = im
1556 if save_all:
1557 single_im = _write_multiple_frames(
1558 im, fp, chunk, mode, rawmode, default_image, append_images
1559 )
1560 if single_im:
1561 _apply_encoderinfo(single_im, im.encoderinfo)
1562 ImageFile._save(
1563 single_im,
1564 cast("IO[bytes]", _idat(fp, chunk)),
1565 [ImageFile._Tile("zip", args=rawmode)],
1566 )
1568 if info:
1569 for info_chunk in info.chunks:
1570 cid, data = info_chunk[:2]
1571 if cid[1:2].islower():
1572 # Private chunk
1573 after_idat = len(info_chunk) == 3 and info_chunk[2]
1574 if after_idat:
1575 chunk(fp, cid, data)
1577 chunk(fp, b"IEND", b"")
1579 if hasattr(fp, "flush"):
1580 fp.flush()
1583# --------------------------------------------------------------------
1584# PNG chunk converter
1587def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes]]:
1588 """Return a list of PNG chunks representing this image."""
1589 from io import BytesIO
1591 chunks = []
1593 def append(fp: IO[bytes], cid: bytes, *data: bytes) -> None:
1594 byte_data = b"".join(data)
1595 crc = o32(_crc32(byte_data, _crc32(cid)))
1596 chunks.append((cid, byte_data, crc))
1598 fp = BytesIO()
1600 try:
1601 im.encoderinfo = params
1602 _save(im, fp, "", append)
1603 finally:
1604 del im.encoderinfo
1606 return chunks
1609# --------------------------------------------------------------------
1610# Registry
1612Image.register_open(PngImageFile.format, PngImageFile, _accept)
1613Image.register_save(PngImageFile.format, _save)
1614Image.register_save_all(PngImageFile.format, _save_all)
1616Image.register_extensions(PngImageFile.format, [".png", ".apng"])
1618Image.register_mime(PngImageFile.format, "image/png")