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