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 ._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 check_text_memory(self, chunklen: int) -> None:
404 self.text_memory += chunklen
405 if self.text_memory > MAX_TEXT_MEMORY:
406 msg = (
407 "Too much memory used in text chunks: "
408 f"{self.text_memory}>MAX_TEXT_MEMORY"
409 )
410 raise ValueError(msg)
412 def save_rewind(self) -> None:
413 self.rewind_state = _RewindState(
414 self.im_info.copy(),
415 self.im_tile,
416 self._seq_num,
417 )
419 def rewind(self) -> None:
420 self.im_info = self.rewind_state.info.copy()
421 self.im_tile = self.rewind_state.tile
422 self._seq_num = self.rewind_state.seq_num
424 def chunk_iCCP(self, pos: int, length: int) -> bytes:
425 # ICC profile
426 assert self.fp is not None
427 s = ImageFile._safe_read(self.fp, length)
428 # according to PNG spec, the iCCP chunk contains:
429 # Profile name 1-79 bytes (character string)
430 # Null separator 1 byte (null character)
431 # Compression method 1 byte (0)
432 # Compressed profile n bytes (zlib with deflate compression)
433 i = s.find(b"\0")
434 logger.debug("iCCP profile name %r", s[:i])
435 comp_method = s[i + 1]
436 logger.debug("Compression method %s", comp_method)
437 if comp_method != 0:
438 msg = f"Unknown compression method {comp_method} in iCCP chunk"
439 raise SyntaxError(msg)
440 try:
441 icc_profile = _safe_zlib_decompress(s[i + 2 :])
442 except ValueError:
443 if ImageFile.LOAD_TRUNCATED_IMAGES:
444 icc_profile = None
445 else:
446 raise
447 except zlib.error:
448 icc_profile = None # FIXME
449 self.im_info["icc_profile"] = icc_profile
450 return s
452 def chunk_IHDR(self, pos: int, length: int) -> bytes:
453 # image header
454 assert self.fp is not None
455 s = ImageFile._safe_read(self.fp, length)
456 if length < 13:
457 if ImageFile.LOAD_TRUNCATED_IMAGES:
458 return s
459 msg = "Truncated IHDR chunk"
460 raise ValueError(msg)
461 self.im_size = i32(s, 0), i32(s, 4)
462 try:
463 self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])]
464 except Exception:
465 pass
466 if s[12]:
467 self.im_info["interlace"] = 1
468 if s[11]:
469 msg = "unknown filter category"
470 raise SyntaxError(msg)
471 return s
473 def chunk_IDAT(self, pos: int, length: int) -> NoReturn:
474 # image data
475 if "bbox" in self.im_info:
476 tile = [ImageFile._Tile("zip", self.im_info["bbox"], pos, self.im_rawmode)]
477 else:
478 if self.im_n_frames is not None:
479 self.im_info["default_image"] = True
480 tile = [ImageFile._Tile("zip", (0, 0) + self.im_size, pos, self.im_rawmode)]
481 self.im_tile = tile
482 self.im_idat = length
483 msg = "image data found"
484 raise EOFError(msg)
486 def chunk_IEND(self, pos: int, length: int) -> NoReturn:
487 msg = "end of PNG image"
488 raise EOFError(msg)
490 def chunk_PLTE(self, pos: int, length: int) -> bytes:
491 # palette
492 assert self.fp is not None
493 s = ImageFile._safe_read(self.fp, length)
494 if self.im_mode == "P":
495 self.im_palette = "RGB", s
496 return s
498 def chunk_tRNS(self, pos: int, length: int) -> bytes:
499 # transparency
500 assert self.fp is not None
501 s = ImageFile._safe_read(self.fp, length)
502 if self.im_mode == "P":
503 if _simple_palette.match(s):
504 # tRNS contains only one full-transparent entry,
505 # other entries are full opaque
506 i = s.find(b"\0")
507 if i >= 0:
508 self.im_info["transparency"] = i
509 else:
510 # otherwise, we have a byte string with one alpha value
511 # for each palette entry
512 self.im_info["transparency"] = s
513 elif self.im_mode == "1":
514 self.im_info["transparency"] = 255 if i16(s) else 0
515 elif self.im_mode in ("L", "I;16"):
516 self.im_info["transparency"] = i16(s)
517 elif self.im_mode == "RGB":
518 self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4)
519 return s
521 def chunk_gAMA(self, pos: int, length: int) -> bytes:
522 # gamma setting
523 assert self.fp is not None
524 s = ImageFile._safe_read(self.fp, length)
525 self.im_info["gamma"] = i32(s) / 100000.0
526 return s
528 def chunk_cHRM(self, pos: int, length: int) -> bytes:
529 # chromaticity, 8 unsigned ints, actual value is scaled by 100,000
530 # WP x,y, Red x,y, Green x,y Blue x,y
532 assert self.fp is not None
533 s = ImageFile._safe_read(self.fp, length)
534 raw_vals = struct.unpack(f">{len(s) // 4}I", s)
535 self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals)
536 return s
538 def chunk_sRGB(self, pos: int, length: int) -> bytes:
539 # srgb rendering intent, 1 byte
540 # 0 perceptual
541 # 1 relative colorimetric
542 # 2 saturation
543 # 3 absolute colorimetric
545 assert self.fp is not None
546 s = ImageFile._safe_read(self.fp, length)
547 if length < 1:
548 if ImageFile.LOAD_TRUNCATED_IMAGES:
549 return s
550 msg = "Truncated sRGB chunk"
551 raise ValueError(msg)
552 self.im_info["srgb"] = s[0]
553 return s
555 def chunk_pHYs(self, pos: int, length: int) -> bytes:
556 # pixels per unit
557 assert self.fp is not None
558 s = ImageFile._safe_read(self.fp, length)
559 if length < 9:
560 if ImageFile.LOAD_TRUNCATED_IMAGES:
561 return s
562 msg = "Truncated pHYs chunk"
563 raise ValueError(msg)
564 px, py = i32(s, 0), i32(s, 4)
565 unit = s[8]
566 if unit == 1: # meter
567 dpi = px * 0.0254, py * 0.0254
568 self.im_info["dpi"] = dpi
569 elif unit == 0:
570 self.im_info["aspect"] = px, py
571 return s
573 def chunk_tEXt(self, pos: int, length: int) -> bytes:
574 # text
575 assert self.fp is not None
576 s = ImageFile._safe_read(self.fp, length)
577 try:
578 k, v = s.split(b"\0", 1)
579 except ValueError:
580 # fallback for broken tEXt tags
581 k = s
582 v = b""
583 if k:
584 k_str = k.decode("latin-1", "strict")
585 v_str = v.decode("latin-1", "replace")
587 self.im_info[k_str] = v if k == b"exif" else v_str
588 self.im_text[k_str] = v_str
589 self.check_text_memory(len(v_str))
591 return s
593 def chunk_zTXt(self, pos: int, length: int) -> bytes:
594 # compressed text
595 assert self.fp is not None
596 s = ImageFile._safe_read(self.fp, length)
597 try:
598 k, v = s.split(b"\0", 1)
599 except ValueError:
600 k = s
601 v = b""
602 if v:
603 comp_method = v[0]
604 else:
605 comp_method = 0
606 if comp_method != 0:
607 msg = f"Unknown compression method {comp_method} in zTXt chunk"
608 raise SyntaxError(msg)
609 try:
610 v = _safe_zlib_decompress(v[1:])
611 except ValueError:
612 if ImageFile.LOAD_TRUNCATED_IMAGES:
613 v = b""
614 else:
615 raise
616 except zlib.error:
617 v = b""
619 if k:
620 k_str = k.decode("latin-1", "strict")
621 v_str = v.decode("latin-1", "replace")
623 self.im_info[k_str] = self.im_text[k_str] = v_str
624 self.check_text_memory(len(v_str))
626 return s
628 def chunk_iTXt(self, pos: int, length: int) -> bytes:
629 # international text
630 assert self.fp is not None
631 r = s = ImageFile._safe_read(self.fp, length)
632 try:
633 k, r = r.split(b"\0", 1)
634 except ValueError:
635 return s
636 if len(r) < 2:
637 return s
638 cf, cm, r = r[0], r[1], r[2:]
639 try:
640 lang, tk, v = r.split(b"\0", 2)
641 except ValueError:
642 return s
643 if cf != 0:
644 if cm == 0:
645 try:
646 v = _safe_zlib_decompress(v)
647 except ValueError:
648 if ImageFile.LOAD_TRUNCATED_IMAGES:
649 return s
650 else:
651 raise
652 except zlib.error:
653 return s
654 else:
655 return s
656 if k == b"XML:com.adobe.xmp":
657 self.im_info["xmp"] = v
658 try:
659 k_str = k.decode("latin-1", "strict")
660 lang_str = lang.decode("utf-8", "strict")
661 tk_str = tk.decode("utf-8", "strict")
662 v_str = v.decode("utf-8", "strict")
663 except UnicodeError:
664 return s
666 self.im_info[k_str] = self.im_text[k_str] = iTXt(v_str, lang_str, tk_str)
667 self.check_text_memory(len(v_str))
669 return s
671 def chunk_eXIf(self, pos: int, length: int) -> bytes:
672 assert self.fp is not None
673 s = ImageFile._safe_read(self.fp, length)
674 self.im_info["exif"] = b"Exif\x00\x00" + s
675 return s
677 # APNG chunks
678 def chunk_acTL(self, pos: int, length: int) -> bytes:
679 assert self.fp is not None
680 s = ImageFile._safe_read(self.fp, length)
681 if length < 8:
682 if ImageFile.LOAD_TRUNCATED_IMAGES:
683 return s
684 msg = "APNG contains truncated acTL chunk"
685 raise ValueError(msg)
686 if self.im_n_frames is not None:
687 self.im_n_frames = None
688 warnings.warn("Invalid APNG, will use default PNG image if possible")
689 return s
690 n_frames = i32(s)
691 if n_frames == 0 or n_frames > 0x80000000:
692 warnings.warn("Invalid APNG, will use default PNG image if possible")
693 return s
694 self.im_n_frames = n_frames
695 self.im_info["loop"] = i32(s, 4)
696 self.im_custom_mimetype = "image/apng"
697 return s
699 def chunk_fcTL(self, pos: int, length: int) -> bytes:
700 assert self.fp is not None
701 s = ImageFile._safe_read(self.fp, length)
702 if length < 26:
703 if ImageFile.LOAD_TRUNCATED_IMAGES:
704 return s
705 msg = "APNG contains truncated fcTL chunk"
706 raise ValueError(msg)
707 seq = i32(s)
708 if (self._seq_num is None and seq != 0) or (
709 self._seq_num is not None and self._seq_num != seq - 1
710 ):
711 msg = "APNG contains frame sequence errors"
712 raise SyntaxError(msg)
713 self._seq_num = seq
714 width, height = i32(s, 4), i32(s, 8)
715 px, py = i32(s, 12), i32(s, 16)
716 im_w, im_h = self.im_size
717 if px + width > im_w or py + height > im_h:
718 msg = "APNG contains invalid frames"
719 raise SyntaxError(msg)
720 self.im_info["bbox"] = (px, py, px + width, py + height)
721 delay_num, delay_den = i16(s, 20), i16(s, 22)
722 if delay_den == 0:
723 delay_den = 100
724 self.im_info["duration"] = float(delay_num) / float(delay_den) * 1000
725 self.im_info["disposal"] = s[24]
726 self.im_info["blend"] = s[25]
727 return s
729 def chunk_fdAT(self, pos: int, length: int) -> bytes:
730 assert self.fp is not None
731 if length < 4:
732 if ImageFile.LOAD_TRUNCATED_IMAGES:
733 s = ImageFile._safe_read(self.fp, length)
734 return s
735 msg = "APNG contains truncated fDAT chunk"
736 raise ValueError(msg)
737 s = ImageFile._safe_read(self.fp, 4)
738 seq = i32(s)
739 if self._seq_num != seq - 1:
740 msg = "APNG contains frame sequence errors"
741 raise SyntaxError(msg)
742 self._seq_num = seq
743 return self.chunk_IDAT(pos + 4, length - 4)
746# --------------------------------------------------------------------
747# PNG reader
750def _accept(prefix: bytes) -> bool:
751 return prefix.startswith(_MAGIC)
754##
755# Image plugin for PNG images.
758class PngImageFile(ImageFile.ImageFile):
759 format = "PNG"
760 format_description = "Portable network graphics"
762 def _open(self) -> None:
763 assert self.fp is not None
764 if not _accept(self.fp.read(8)):
765 msg = "not a PNG file"
766 raise SyntaxError(msg)
767 self._fp = self.fp
768 self.__frame = 0
770 #
771 # Parse headers up to the first IDAT or fDAT chunk
773 self.private_chunks: list[tuple[bytes, bytes] | tuple[bytes, bytes, bool]] = []
774 self.png: PngStream | None = PngStream(self.fp)
776 while True:
777 #
778 # get next chunk
780 cid, pos, length = self.png.read()
782 try:
783 s = self.png.call(cid, pos, length)
784 except EOFError:
785 break
786 except AttributeError:
787 logger.debug("%r %s %s (unknown)", cid, pos, length)
788 s = ImageFile._safe_read(self.fp, length)
789 if cid[1:2].islower():
790 self.private_chunks.append((cid, s))
792 self.png.crc(cid, s)
794 #
795 # Copy relevant attributes from the PngStream. An alternative
796 # would be to let the PngStream class modify these attributes
797 # directly, but that introduces circular references which are
798 # difficult to break if things go wrong in the decoder...
799 # (believe me, I've tried ;-)
801 self._mode = self.png.im_mode
802 self._size = self.png.im_size
803 self.info = self.png.im_info
804 self._text: dict[str, str | iTXt] | None = None
805 self.tile = self.png.im_tile
806 self.custom_mimetype = self.png.im_custom_mimetype
807 self.n_frames = self.png.im_n_frames or 1
808 self.default_image = self.info.get("default_image", False)
810 if self.png.im_palette:
811 rawmode, data = self.png.im_palette
812 self.palette = ImagePalette.raw(rawmode, data)
814 if cid == b"fdAT":
815 self.__prepare_idat = length - 4
816 else:
817 self.__prepare_idat = length # used by load_prepare()
819 if self.png.im_n_frames is not None:
820 self._close_exclusive_fp_after_loading = False
821 self.png.save_rewind()
822 self.__rewind_idat = self.__prepare_idat
823 self.__rewind = self._fp.tell()
824 if self.default_image:
825 # IDAT chunk contains default image and not first animation frame
826 self.n_frames += 1
827 self._seek(0)
828 self.is_animated = self.n_frames > 1
830 @property
831 def text(self) -> dict[str, str | iTXt]:
832 # experimental
833 if self._text is None:
834 # iTxt, tEXt and zTXt chunks may appear at the end of the file
835 # So load the file to ensure that they are read
836 if self.is_animated:
837 frame = self.__frame
838 # for APNG, seek to the final frame before loading
839 self.seek(self.n_frames - 1)
840 self.load()
841 if self.is_animated:
842 self.seek(frame)
843 assert self._text is not None
844 return self._text
846 def verify(self) -> None:
847 """Verify PNG file"""
849 if self.fp is None:
850 msg = "verify must be called directly after open"
851 raise RuntimeError(msg)
853 # back up to beginning of IDAT block
854 self.fp.seek(self.tile[0][2] - 8)
856 assert self.png is not None
857 self.png.verify()
858 self.png.close()
860 super().verify()
862 def seek(self, frame: int) -> None:
863 if not self._seek_check(frame):
864 return
865 if frame < self.__frame:
866 self._seek(0, True)
868 last_frame = self.__frame
869 for f in range(self.__frame + 1, frame + 1):
870 try:
871 self._seek(f)
872 except EOFError as e:
873 self.seek(last_frame)
874 msg = "no more images in APNG file"
875 raise EOFError(msg) from e
877 def _seek(self, frame: int, rewind: bool = False) -> None:
878 assert self.png is not None
879 if isinstance(self._fp, DeferredError):
880 raise self._fp.ex
882 self.dispose: _imaging.ImagingCore | None
883 dispose_extent = None
884 if frame == 0:
885 if rewind:
886 self._fp.seek(self.__rewind)
887 self.png.rewind()
888 self.__prepare_idat = self.__rewind_idat
889 self._im = None
890 self.info = self.png.im_info
891 self.tile = self.png.im_tile
892 self.fp = self._fp
893 self._prev_im = None
894 self.dispose = None
895 self.default_image = self.info.get("default_image", False)
896 self.dispose_op = self.info.get("disposal")
897 self.blend_op = self.info.get("blend")
898 dispose_extent = self.info.get("bbox")
899 self.__frame = 0
900 else:
901 if frame != self.__frame + 1:
902 msg = f"cannot seek to frame {frame}"
903 raise ValueError(msg)
905 # ensure previous frame was loaded
906 self.load()
908 if self.dispose:
909 self.im.paste(self.dispose, self.dispose_extent)
910 self._prev_im = self.im.copy()
912 self.fp = self._fp
914 # advance to the next frame
915 if self.__prepare_idat:
916 ImageFile._safe_read(self.fp, self.__prepare_idat)
917 self.__prepare_idat = 0
918 frame_start = False
919 while True:
920 self.fp.read(4) # CRC
922 try:
923 cid, pos, length = self.png.read()
924 except (struct.error, SyntaxError):
925 break
927 if cid == b"IEND":
928 msg = "No more images in APNG file"
929 raise EOFError(msg)
930 if cid == b"fcTL":
931 if frame_start:
932 # there must be at least one fdAT chunk between fcTL chunks
933 msg = "APNG missing frame data"
934 raise SyntaxError(msg)
935 frame_start = True
937 try:
938 self.png.call(cid, pos, length)
939 except UnicodeDecodeError:
940 break
941 except EOFError:
942 if cid == b"fdAT":
943 length -= 4
944 if frame_start:
945 self.__prepare_idat = length
946 break
947 ImageFile._safe_read(self.fp, length)
948 except AttributeError:
949 logger.debug("%r %s %s (unknown)", cid, pos, length)
950 ImageFile._safe_read(self.fp, length)
952 self.__frame = frame
953 self.tile = self.png.im_tile
954 self.dispose_op = self.info.get("disposal")
955 self.blend_op = self.info.get("blend")
956 dispose_extent = self.info.get("bbox")
958 if not self.tile:
959 msg = "image not found in APNG frame"
960 raise EOFError(msg)
961 if dispose_extent:
962 self.dispose_extent: tuple[float, float, float, float] = dispose_extent
964 # setup frame disposal (actual disposal done when needed in the next _seek())
965 if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS:
966 self.dispose_op = Disposal.OP_BACKGROUND
968 self.dispose = None
969 if self.dispose_op == Disposal.OP_PREVIOUS:
970 if self._prev_im:
971 self.dispose = self._prev_im.copy()
972 self.dispose = self._crop(self.dispose, self.dispose_extent)
973 elif self.dispose_op == Disposal.OP_BACKGROUND:
974 self.dispose = Image.core.fill(self.mode, self.size)
975 self.dispose = self._crop(self.dispose, self.dispose_extent)
977 def tell(self) -> int:
978 return self.__frame
980 def load_prepare(self) -> None:
981 """internal: prepare to read PNG file"""
983 if self.info.get("interlace"):
984 self.decoderconfig = self.decoderconfig + (1,)
986 self.__idat = self.__prepare_idat # used by load_read()
987 ImageFile.ImageFile.load_prepare(self)
989 def load_read(self, read_bytes: int) -> bytes:
990 """internal: read more image data"""
992 assert self.png is not None
993 assert self.fp is not None
994 while self.__idat == 0:
995 # end of chunk, skip forward to next one
997 self.fp.read(4) # CRC
999 cid, pos, length = self.png.read()
1001 if cid not in [b"IDAT", b"DDAT", b"fdAT"]:
1002 self.png.push(cid, pos, length)
1003 return b""
1005 if cid == b"fdAT":
1006 try:
1007 self.png.call(cid, pos, length)
1008 except EOFError:
1009 pass
1010 self.__idat = length - 4 # sequence_num has already been read
1011 else:
1012 self.__idat = length # empty chunks are allowed
1014 # read more data from this chunk
1015 if read_bytes <= 0:
1016 read_bytes = self.__idat
1017 else:
1018 read_bytes = min(read_bytes, self.__idat)
1020 self.__idat = self.__idat - read_bytes
1022 return self.fp.read(read_bytes)
1024 def load_end(self) -> None:
1025 """internal: finished reading image data"""
1026 assert self.png is not None
1027 assert self.fp is not None
1028 if self.__idat != 0:
1029 self.fp.read(self.__idat)
1030 while True:
1031 self.fp.read(4) # CRC
1033 try:
1034 cid, pos, length = self.png.read()
1035 except (struct.error, SyntaxError):
1036 break
1038 if cid == b"IEND":
1039 break
1040 elif cid == b"fcTL" and self.is_animated:
1041 # start of the next frame, stop reading
1042 self.__prepare_idat = 0
1043 self.png.push(cid, pos, length)
1044 break
1046 try:
1047 self.png.call(cid, pos, length)
1048 except UnicodeDecodeError:
1049 break
1050 except EOFError:
1051 if cid == b"fdAT":
1052 length -= 4
1053 try:
1054 ImageFile._safe_read(self.fp, length)
1055 except OSError as e:
1056 if ImageFile.LOAD_TRUNCATED_IMAGES:
1057 break
1058 else:
1059 raise e
1060 except AttributeError:
1061 logger.debug("%r %s %s (unknown)", cid, pos, length)
1062 s = ImageFile._safe_read(self.fp, length)
1063 if cid[1:2].islower():
1064 self.private_chunks.append((cid, s, True))
1065 self._text = self.png.im_text
1066 if not self.is_animated:
1067 self.png.close()
1068 self.png = None
1069 else:
1070 if self._prev_im and self.blend_op == Blend.OP_OVER:
1071 updated = self._crop(self.im, self.dispose_extent)
1072 if self.im.mode == "RGB" and "transparency" in self.info:
1073 mask = updated.convert_transparent(
1074 "RGBA", self.info["transparency"]
1075 )
1076 else:
1077 if self.im.mode == "P" and "transparency" in self.info:
1078 t = self.info["transparency"]
1079 if isinstance(t, bytes):
1080 updated.putpalettealphas(t)
1081 elif isinstance(t, int):
1082 updated.putpalettealpha(t)
1083 mask = updated.convert("RGBA")
1084 self._prev_im.paste(updated, self.dispose_extent, mask)
1085 self.im = self._prev_im
1087 def _getexif(self) -> dict[int, Any] | None:
1088 if "exif" not in self.info:
1089 self.load()
1090 if "exif" not in self.info and "Raw profile type exif" not in self.info:
1091 return None
1092 return self.getexif()._get_merged_dict()
1094 def getexif(self) -> Image.Exif:
1095 if "exif" not in self.info:
1096 self.load()
1098 return super().getexif()
1101# --------------------------------------------------------------------
1102# PNG writer
1104_OUTMODES = {
1105 # supported PIL modes, and corresponding rawmode, bit depth and color type
1106 "1": ("1", b"\x01", b"\x00"),
1107 "L;1": ("L;1", b"\x01", b"\x00"),
1108 "L;2": ("L;2", b"\x02", b"\x00"),
1109 "L;4": ("L;4", b"\x04", b"\x00"),
1110 "L": ("L", b"\x08", b"\x00"),
1111 "LA": ("LA", b"\x08", b"\x04"),
1112 "I": ("I;16B", b"\x10", b"\x00"),
1113 "I;16": ("I;16B", b"\x10", b"\x00"),
1114 "I;16B": ("I;16B", b"\x10", b"\x00"),
1115 "P;1": ("P;1", b"\x01", b"\x03"),
1116 "P;2": ("P;2", b"\x02", b"\x03"),
1117 "P;4": ("P;4", b"\x04", b"\x03"),
1118 "P": ("P", b"\x08", b"\x03"),
1119 "RGB": ("RGB", b"\x08", b"\x02"),
1120 "RGBA": ("RGBA", b"\x08", b"\x06"),
1121}
1124def putchunk(fp: IO[bytes], cid: bytes, *data: bytes) -> None:
1125 """Write a PNG chunk (including CRC field)"""
1127 byte_data = b"".join(data)
1129 fp.write(o32(len(byte_data)) + cid)
1130 fp.write(byte_data)
1131 crc = _crc32(byte_data, _crc32(cid))
1132 fp.write(o32(crc))
1135class _idat:
1136 # wrap output from the encoder in IDAT chunks
1138 def __init__(self, fp: IO[bytes], chunk: Callable[..., None]) -> None:
1139 self.fp = fp
1140 self.chunk = chunk
1142 def write(self, data: bytes) -> None:
1143 self.chunk(self.fp, b"IDAT", data)
1146class _fdat:
1147 # wrap encoder output in fdAT chunks
1149 def __init__(self, fp: IO[bytes], chunk: Callable[..., None], seq_num: int) -> None:
1150 self.fp = fp
1151 self.chunk = chunk
1152 self.seq_num = seq_num
1154 def write(self, data: bytes) -> None:
1155 self.chunk(self.fp, b"fdAT", o32(self.seq_num), data)
1156 self.seq_num += 1
1159def _apply_encoderinfo(im: Image.Image, encoderinfo: dict[str, Any]) -> None:
1160 im.encoderconfig = (
1161 encoderinfo.get("optimize", False),
1162 encoderinfo.get("compress_level", -1),
1163 encoderinfo.get("compress_type", -1),
1164 encoderinfo.get("dictionary", b""),
1165 )
1168class _Frame(NamedTuple):
1169 im: Image.Image
1170 bbox: tuple[int, int, int, int] | None
1171 encoderinfo: dict[str, Any]
1174def _write_multiple_frames(
1175 im: Image.Image,
1176 fp: IO[bytes],
1177 chunk: Callable[..., None],
1178 mode: str,
1179 rawmode: str,
1180 default_image: Image.Image | None,
1181 append_images: list[Image.Image],
1182) -> Image.Image | None:
1183 duration = im.encoderinfo.get("duration")
1184 loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
1185 disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
1186 blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))
1188 if default_image:
1189 chain = itertools.chain(append_images)
1190 else:
1191 chain = itertools.chain([im], append_images)
1193 im_frames: list[_Frame] = []
1194 frame_count = 0
1195 for im_seq in chain:
1196 for im_frame in ImageSequence.Iterator(im_seq):
1197 if im_frame.mode == mode:
1198 im_frame = im_frame.copy()
1199 else:
1200 im_frame = im_frame.convert(mode)
1201 encoderinfo = im.encoderinfo.copy()
1202 if isinstance(duration, (list, tuple)):
1203 encoderinfo["duration"] = duration[frame_count]
1204 elif duration is None and "duration" in im_frame.info:
1205 encoderinfo["duration"] = im_frame.info["duration"]
1206 if isinstance(disposal, (list, tuple)):
1207 encoderinfo["disposal"] = disposal[frame_count]
1208 if isinstance(blend, (list, tuple)):
1209 encoderinfo["blend"] = blend[frame_count]
1210 frame_count += 1
1212 if im_frames:
1213 previous = im_frames[-1]
1214 prev_disposal = previous.encoderinfo.get("disposal")
1215 prev_blend = previous.encoderinfo.get("blend")
1216 if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2:
1217 prev_disposal = Disposal.OP_BACKGROUND
1219 if prev_disposal == Disposal.OP_BACKGROUND:
1220 base_im = previous.im.copy()
1221 dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0))
1222 bbox = previous.bbox
1223 if bbox:
1224 dispose = dispose.crop(bbox)
1225 else:
1226 bbox = (0, 0) + im.size
1227 base_im.paste(dispose, bbox)
1228 elif prev_disposal == Disposal.OP_PREVIOUS:
1229 base_im = im_frames[-2].im
1230 else:
1231 base_im = previous.im
1232 delta = ImageChops.subtract_modulo(
1233 im_frame.convert("RGBA"), base_im.convert("RGBA")
1234 )
1235 bbox = delta.getbbox(alpha_only=False)
1236 if (
1237 not bbox
1238 and prev_disposal == encoderinfo.get("disposal")
1239 and prev_blend == encoderinfo.get("blend")
1240 and "duration" in encoderinfo
1241 ):
1242 previous.encoderinfo["duration"] += encoderinfo["duration"]
1243 continue
1244 else:
1245 bbox = None
1246 im_frames.append(_Frame(im_frame, bbox, encoderinfo))
1248 if len(im_frames) == 1 and not default_image:
1249 return im_frames[0].im
1251 # animation control
1252 chunk(
1253 fp,
1254 b"acTL",
1255 o32(len(im_frames)), # 0: num_frames
1256 o32(loop), # 4: num_plays
1257 )
1259 # default image IDAT (if it exists)
1260 if default_image:
1261 default_im = im if im.mode == mode else im.convert(mode)
1262 _apply_encoderinfo(default_im, im.encoderinfo)
1263 ImageFile._save(
1264 default_im,
1265 cast(IO[bytes], _idat(fp, chunk)),
1266 [ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)],
1267 )
1269 seq_num = 0
1270 for frame, frame_data in enumerate(im_frames):
1271 im_frame = frame_data.im
1272 if not frame_data.bbox:
1273 bbox = (0, 0) + im_frame.size
1274 else:
1275 bbox = frame_data.bbox
1276 im_frame = im_frame.crop(bbox)
1277 size = im_frame.size
1278 encoderinfo = frame_data.encoderinfo
1279 frame_duration = encoderinfo.get("duration", 0)
1280 delay = Fraction(frame_duration / 1000).limit_denominator(65535)
1281 if delay.numerator > 65535:
1282 msg = "cannot write duration"
1283 raise ValueError(msg)
1284 frame_disposal = encoderinfo.get("disposal", disposal)
1285 frame_blend = encoderinfo.get("blend", blend)
1286 # frame control
1287 chunk(
1288 fp,
1289 b"fcTL",
1290 o32(seq_num), # sequence_number
1291 o32(size[0]), # width
1292 o32(size[1]), # height
1293 o32(bbox[0]), # x_offset
1294 o32(bbox[1]), # y_offset
1295 o16(delay.numerator), # delay_numerator
1296 o16(delay.denominator), # delay_denominator
1297 o8(frame_disposal), # dispose_op
1298 o8(frame_blend), # blend_op
1299 )
1300 seq_num += 1
1301 # frame data
1302 _apply_encoderinfo(im_frame, im.encoderinfo)
1303 if frame == 0 and not default_image:
1304 # first frame must be in IDAT chunks for backwards compatibility
1305 ImageFile._save(
1306 im_frame,
1307 cast(IO[bytes], _idat(fp, chunk)),
1308 [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)],
1309 )
1310 else:
1311 fdat_chunks = _fdat(fp, chunk, seq_num)
1312 ImageFile._save(
1313 im_frame,
1314 cast(IO[bytes], fdat_chunks),
1315 [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)],
1316 )
1317 seq_num = fdat_chunks.seq_num
1318 return None
1321def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
1322 _save(im, fp, filename, save_all=True)
1325def _save(
1326 im: Image.Image,
1327 fp: IO[bytes],
1328 filename: str | bytes,
1329 chunk: Callable[..., None] = putchunk,
1330 save_all: bool = False,
1331) -> None:
1332 # save an image to disk (called by the save method)
1334 if save_all:
1335 default_image = im.encoderinfo.get(
1336 "default_image", im.info.get("default_image")
1337 )
1338 modes = set()
1339 sizes = set()
1340 append_images = im.encoderinfo.get("append_images", [])
1341 for im_seq in itertools.chain([im], append_images):
1342 for im_frame in ImageSequence.Iterator(im_seq):
1343 modes.add(im_frame.mode)
1344 sizes.add(im_frame.size)
1345 for mode in ("RGBA", "RGB", "P"):
1346 if mode in modes:
1347 break
1348 else:
1349 mode = modes.pop()
1350 size = tuple(max(frame_size[i] for frame_size in sizes) for i in range(2))
1351 else:
1352 size = im.size
1353 mode = im.mode
1355 outmode = mode
1356 if mode == "P":
1357 #
1358 # attempt to minimize storage requirements for palette images
1359 if "bits" in im.encoderinfo:
1360 # number of bits specified by user
1361 colors = min(1 << im.encoderinfo["bits"], 256)
1362 else:
1363 # check palette contents
1364 if im.palette:
1365 colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1)
1366 else:
1367 colors = 256
1369 if colors <= 16:
1370 if colors <= 2:
1371 bits = 1
1372 elif colors <= 4:
1373 bits = 2
1374 else:
1375 bits = 4
1376 outmode += f";{bits}"
1378 # get the corresponding PNG mode
1379 try:
1380 rawmode, bit_depth, color_type = _OUTMODES[outmode]
1381 except KeyError as e:
1382 msg = f"cannot write mode {mode} as PNG"
1383 raise OSError(msg) from e
1384 if outmode == "I":
1385 deprecate("Saving I mode images as PNG", 13, stacklevel=4)
1387 #
1388 # write minimal PNG file
1390 fp.write(_MAGIC)
1392 chunk(
1393 fp,
1394 b"IHDR",
1395 o32(size[0]), # 0: size
1396 o32(size[1]),
1397 bit_depth,
1398 color_type,
1399 b"\0", # 10: compression
1400 b"\0", # 11: filter category
1401 b"\0", # 12: interlace flag
1402 )
1404 chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"]
1406 icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile"))
1407 if icc:
1408 # ICC profile
1409 # according to PNG spec, the iCCP chunk contains:
1410 # Profile name 1-79 bytes (character string)
1411 # Null separator 1 byte (null character)
1412 # Compression method 1 byte (0)
1413 # Compressed profile n bytes (zlib with deflate compression)
1414 name = b"ICC Profile"
1415 data = name + b"\0\0" + zlib.compress(icc)
1416 chunk(fp, b"iCCP", data)
1418 # You must either have sRGB or iCCP.
1419 # Disallow sRGB chunks when an iCCP-chunk has been emitted.
1420 chunks.remove(b"sRGB")
1422 info = im.encoderinfo.get("pnginfo")
1423 if info:
1424 chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"]
1425 for info_chunk in info.chunks:
1426 cid, data = info_chunk[:2]
1427 if cid in chunks:
1428 chunks.remove(cid)
1429 chunk(fp, cid, data)
1430 elif cid in chunks_multiple_allowed:
1431 chunk(fp, cid, data)
1432 elif cid[1:2].islower():
1433 # Private chunk
1434 after_idat = len(info_chunk) == 3 and info_chunk[2]
1435 if not after_idat:
1436 chunk(fp, cid, data)
1438 if im.mode == "P":
1439 palette_byte_number = colors * 3
1440 palette_bytes = im.im.getpalette("RGB")[:palette_byte_number]
1441 while len(palette_bytes) < palette_byte_number:
1442 palette_bytes += b"\0"
1443 chunk(fp, b"PLTE", palette_bytes)
1445 transparency = im.encoderinfo.get("transparency", im.info.get("transparency", None))
1447 if transparency or transparency == 0:
1448 if im.mode == "P":
1449 # limit to actual palette size
1450 alpha_bytes = colors
1451 if isinstance(transparency, bytes):
1452 chunk(fp, b"tRNS", transparency[:alpha_bytes])
1453 else:
1454 transparency = max(0, min(255, transparency))
1455 alpha = b"\xff" * transparency + b"\0"
1456 chunk(fp, b"tRNS", alpha[:alpha_bytes])
1457 elif im.mode in ("1", "L", "I", "I;16"):
1458 transparency = max(0, min(65535, transparency))
1459 chunk(fp, b"tRNS", o16(transparency))
1460 elif im.mode == "RGB":
1461 red, green, blue = transparency
1462 chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue))
1463 else:
1464 if "transparency" in im.encoderinfo:
1465 # don't bother with transparency if it's an RGBA
1466 # and it's in the info dict. It's probably just stale.
1467 msg = "cannot use transparency for this mode"
1468 raise OSError(msg)
1469 else:
1470 if im.mode == "P" and im.im.getpalettemode() == "RGBA":
1471 alpha = im.im.getpalette("RGBA", "A")
1472 alpha_bytes = colors
1473 chunk(fp, b"tRNS", alpha[:alpha_bytes])
1475 dpi = im.encoderinfo.get("dpi")
1476 if dpi:
1477 chunk(
1478 fp,
1479 b"pHYs",
1480 o32(int(dpi[0] / 0.0254 + 0.5)),
1481 o32(int(dpi[1] / 0.0254 + 0.5)),
1482 b"\x01",
1483 )
1485 if info:
1486 chunks = [b"bKGD", b"hIST"]
1487 for info_chunk in info.chunks:
1488 cid, data = info_chunk[:2]
1489 if cid in chunks:
1490 chunks.remove(cid)
1491 chunk(fp, cid, data)
1493 exif = im.encoderinfo.get("exif")
1494 if exif:
1495 if isinstance(exif, Image.Exif):
1496 exif = exif.tobytes(8)
1497 if exif.startswith(b"Exif\x00\x00"):
1498 exif = exif[6:]
1499 chunk(fp, b"eXIf", exif)
1501 single_im: Image.Image | None = im
1502 if save_all:
1503 single_im = _write_multiple_frames(
1504 im, fp, chunk, mode, rawmode, default_image, append_images
1505 )
1506 if single_im:
1507 _apply_encoderinfo(single_im, im.encoderinfo)
1508 ImageFile._save(
1509 single_im,
1510 cast(IO[bytes], _idat(fp, chunk)),
1511 [ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)],
1512 )
1514 if info:
1515 for info_chunk in info.chunks:
1516 cid, data = info_chunk[:2]
1517 if cid[1:2].islower():
1518 # Private chunk
1519 after_idat = len(info_chunk) == 3 and info_chunk[2]
1520 if after_idat:
1521 chunk(fp, cid, data)
1523 chunk(fp, b"IEND", b"")
1525 if hasattr(fp, "flush"):
1526 fp.flush()
1529# --------------------------------------------------------------------
1530# PNG chunk converter
1533def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes]]:
1534 """Return a list of PNG chunks representing this image."""
1535 from io import BytesIO
1537 chunks = []
1539 def append(fp: IO[bytes], cid: bytes, *data: bytes) -> None:
1540 byte_data = b"".join(data)
1541 crc = o32(_crc32(byte_data, _crc32(cid)))
1542 chunks.append((cid, byte_data, crc))
1544 fp = BytesIO()
1546 try:
1547 im.encoderinfo = params
1548 _save(im, fp, "", append)
1549 finally:
1550 del im.encoderinfo
1552 return chunks
1555# --------------------------------------------------------------------
1556# Registry
1558Image.register_open(PngImageFile.format, PngImageFile, _accept)
1559Image.register_save(PngImageFile.format, _save)
1560Image.register_save_all(PngImageFile.format, _save_all)
1562Image.register_extensions(PngImageFile.format, [".png", ".apng"])
1564Image.register_mime(PngImageFile.format, "image/png")