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