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 typing import IO, NamedTuple, cast
44from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
45from ._binary import i16be as i16
46from ._binary import i32be as i32
47from ._binary import o8
48from ._binary import o16be as o16
49from ._binary import o32be as o32
50from ._deprecate import deprecate
51from ._util import DeferredError
53TYPE_CHECKING = False
54if TYPE_CHECKING:
55 from collections.abc import Callable
56 from typing import Any, NoReturn
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) -> ChunkStream:
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 ) -> iTXt:
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 Exception:
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 if not _accept(self.fp.read(8)):
763 msg = "not a PNG file"
764 raise SyntaxError(msg)
765 self._fp = self.fp
766 self.__frame = 0
768 #
769 # Parse headers up to the first IDAT or fDAT chunk
771 self.private_chunks: list[tuple[bytes, bytes] | tuple[bytes, bytes, bool]] = []
772 self.png: PngStream | None = PngStream(self.fp)
774 while True:
775 #
776 # get next chunk
778 cid, pos, length = self.png.read()
780 try:
781 s = self.png.call(cid, pos, length)
782 except EOFError:
783 break
784 except AttributeError:
785 logger.debug("%r %s %s (unknown)", cid, pos, length)
786 s = ImageFile._safe_read(self.fp, length)
787 if cid[1:2].islower():
788 self.private_chunks.append((cid, s))
790 self.png.crc(cid, s)
792 #
793 # Copy relevant attributes from the PngStream. An alternative
794 # would be to let the PngStream class modify these attributes
795 # directly, but that introduces circular references which are
796 # difficult to break if things go wrong in the decoder...
797 # (believe me, I've tried ;-)
799 self._mode = self.png.im_mode
800 self._size = self.png.im_size
801 self.info = self.png.im_info
802 self._text: dict[str, str | iTXt] | None = None
803 self.tile = self.png.im_tile
804 self.custom_mimetype = self.png.im_custom_mimetype
805 self.n_frames = self.png.im_n_frames or 1
806 self.default_image = self.info.get("default_image", False)
808 if self.png.im_palette:
809 rawmode, data = self.png.im_palette
810 self.palette = ImagePalette.raw(rawmode, data)
812 if cid == b"fdAT":
813 self.__prepare_idat = length - 4
814 else:
815 self.__prepare_idat = length # used by load_prepare()
817 if self.png.im_n_frames is not None:
818 self._close_exclusive_fp_after_loading = False
819 self.png.save_rewind()
820 self.__rewind_idat = self.__prepare_idat
821 self.__rewind = self._fp.tell()
822 if self.default_image:
823 # IDAT chunk contains default image and not first animation frame
824 self.n_frames += 1
825 self._seek(0)
826 self.is_animated = self.n_frames > 1
828 @property
829 def text(self) -> dict[str, str | iTXt]:
830 # experimental
831 if self._text is None:
832 # iTxt, tEXt and zTXt chunks may appear at the end of the file
833 # So load the file to ensure that they are read
834 if self.is_animated:
835 frame = self.__frame
836 # for APNG, seek to the final frame before loading
837 self.seek(self.n_frames - 1)
838 self.load()
839 if self.is_animated:
840 self.seek(frame)
841 assert self._text is not None
842 return self._text
844 def verify(self) -> None:
845 """Verify PNG file"""
847 if self.fp is None:
848 msg = "verify must be called directly after open"
849 raise RuntimeError(msg)
851 # back up to beginning of IDAT block
852 self.fp.seek(self.tile[0][2] - 8)
854 assert self.png is not None
855 self.png.verify()
856 self.png.close()
858 if self._exclusive_fp:
859 self.fp.close()
860 self.fp = None
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 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 if self.__idat != 0:
1027 self.fp.read(self.__idat)
1028 while True:
1029 self.fp.read(4) # CRC
1031 try:
1032 cid, pos, length = self.png.read()
1033 except (struct.error, SyntaxError):
1034 break
1036 if cid == b"IEND":
1037 break
1038 elif cid == b"fcTL" and self.is_animated:
1039 # start of the next frame, stop reading
1040 self.__prepare_idat = 0
1041 self.png.push(cid, pos, length)
1042 break
1044 try:
1045 self.png.call(cid, pos, length)
1046 except UnicodeDecodeError:
1047 break
1048 except EOFError:
1049 if cid == b"fdAT":
1050 length -= 4
1051 try:
1052 ImageFile._safe_read(self.fp, length)
1053 except OSError as e:
1054 if ImageFile.LOAD_TRUNCATED_IMAGES:
1055 break
1056 else:
1057 raise e
1058 except AttributeError:
1059 logger.debug("%r %s %s (unknown)", cid, pos, length)
1060 s = ImageFile._safe_read(self.fp, length)
1061 if cid[1:2].islower():
1062 self.private_chunks.append((cid, s, True))
1063 self._text = self.png.im_text
1064 if not self.is_animated:
1065 self.png.close()
1066 self.png = None
1067 else:
1068 if self._prev_im and self.blend_op == Blend.OP_OVER:
1069 updated = self._crop(self.im, self.dispose_extent)
1070 if self.im.mode == "RGB" and "transparency" in self.info:
1071 mask = updated.convert_transparent(
1072 "RGBA", self.info["transparency"]
1073 )
1074 else:
1075 if self.im.mode == "P" and "transparency" in self.info:
1076 t = self.info["transparency"]
1077 if isinstance(t, bytes):
1078 updated.putpalettealphas(t)
1079 elif isinstance(t, int):
1080 updated.putpalettealpha(t)
1081 mask = updated.convert("RGBA")
1082 self._prev_im.paste(updated, self.dispose_extent, mask)
1083 self.im = self._prev_im
1085 def _getexif(self) -> dict[int, Any] | None:
1086 if "exif" not in self.info:
1087 self.load()
1088 if "exif" not in self.info and "Raw profile type exif" not in self.info:
1089 return None
1090 return self.getexif()._get_merged_dict()
1092 def getexif(self) -> Image.Exif:
1093 if "exif" not in self.info:
1094 self.load()
1096 return super().getexif()
1099# --------------------------------------------------------------------
1100# PNG writer
1102_OUTMODES = {
1103 # supported PIL modes, and corresponding rawmode, bit depth and color type
1104 "1": ("1", b"\x01", b"\x00"),
1105 "L;1": ("L;1", b"\x01", b"\x00"),
1106 "L;2": ("L;2", b"\x02", b"\x00"),
1107 "L;4": ("L;4", b"\x04", b"\x00"),
1108 "L": ("L", b"\x08", b"\x00"),
1109 "LA": ("LA", b"\x08", b"\x04"),
1110 "I": ("I;16B", b"\x10", b"\x00"),
1111 "I;16": ("I;16B", b"\x10", b"\x00"),
1112 "I;16B": ("I;16B", b"\x10", b"\x00"),
1113 "P;1": ("P;1", b"\x01", b"\x03"),
1114 "P;2": ("P;2", b"\x02", b"\x03"),
1115 "P;4": ("P;4", b"\x04", b"\x03"),
1116 "P": ("P", b"\x08", b"\x03"),
1117 "RGB": ("RGB", b"\x08", b"\x02"),
1118 "RGBA": ("RGBA", b"\x08", b"\x06"),
1119}
1122def putchunk(fp: IO[bytes], cid: bytes, *data: bytes) -> None:
1123 """Write a PNG chunk (including CRC field)"""
1125 byte_data = b"".join(data)
1127 fp.write(o32(len(byte_data)) + cid)
1128 fp.write(byte_data)
1129 crc = _crc32(byte_data, _crc32(cid))
1130 fp.write(o32(crc))
1133class _idat:
1134 # wrap output from the encoder in IDAT chunks
1136 def __init__(self, fp: IO[bytes], chunk: Callable[..., None]) -> None:
1137 self.fp = fp
1138 self.chunk = chunk
1140 def write(self, data: bytes) -> None:
1141 self.chunk(self.fp, b"IDAT", data)
1144class _fdat:
1145 # wrap encoder output in fdAT chunks
1147 def __init__(self, fp: IO[bytes], chunk: Callable[..., None], seq_num: int) -> None:
1148 self.fp = fp
1149 self.chunk = chunk
1150 self.seq_num = seq_num
1152 def write(self, data: bytes) -> None:
1153 self.chunk(self.fp, b"fdAT", o32(self.seq_num), data)
1154 self.seq_num += 1
1157def _apply_encoderinfo(im: Image.Image, encoderinfo: dict[str, Any]) -> None:
1158 im.encoderconfig = (
1159 encoderinfo.get("optimize", False),
1160 encoderinfo.get("compress_level", -1),
1161 encoderinfo.get("compress_type", -1),
1162 encoderinfo.get("dictionary", b""),
1163 )
1166class _Frame(NamedTuple):
1167 im: Image.Image
1168 bbox: tuple[int, int, int, int] | None
1169 encoderinfo: dict[str, Any]
1172def _write_multiple_frames(
1173 im: Image.Image,
1174 fp: IO[bytes],
1175 chunk: Callable[..., None],
1176 mode: str,
1177 rawmode: str,
1178 default_image: Image.Image | None,
1179 append_images: list[Image.Image],
1180) -> Image.Image | None:
1181 duration = im.encoderinfo.get("duration")
1182 loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
1183 disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
1184 blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))
1186 if default_image:
1187 chain = itertools.chain(append_images)
1188 else:
1189 chain = itertools.chain([im], append_images)
1191 im_frames: list[_Frame] = []
1192 frame_count = 0
1193 for im_seq in chain:
1194 for im_frame in ImageSequence.Iterator(im_seq):
1195 if im_frame.mode == mode:
1196 im_frame = im_frame.copy()
1197 else:
1198 im_frame = im_frame.convert(mode)
1199 encoderinfo = im.encoderinfo.copy()
1200 if isinstance(duration, (list, tuple)):
1201 encoderinfo["duration"] = duration[frame_count]
1202 elif duration is None and "duration" in im_frame.info:
1203 encoderinfo["duration"] = im_frame.info["duration"]
1204 if isinstance(disposal, (list, tuple)):
1205 encoderinfo["disposal"] = disposal[frame_count]
1206 if isinstance(blend, (list, tuple)):
1207 encoderinfo["blend"] = blend[frame_count]
1208 frame_count += 1
1210 if im_frames:
1211 previous = im_frames[-1]
1212 prev_disposal = previous.encoderinfo.get("disposal")
1213 prev_blend = previous.encoderinfo.get("blend")
1214 if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2:
1215 prev_disposal = Disposal.OP_BACKGROUND
1217 if prev_disposal == Disposal.OP_BACKGROUND:
1218 base_im = previous.im.copy()
1219 dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0))
1220 bbox = previous.bbox
1221 if bbox:
1222 dispose = dispose.crop(bbox)
1223 else:
1224 bbox = (0, 0) + im.size
1225 base_im.paste(dispose, bbox)
1226 elif prev_disposal == Disposal.OP_PREVIOUS:
1227 base_im = im_frames[-2].im
1228 else:
1229 base_im = previous.im
1230 delta = ImageChops.subtract_modulo(
1231 im_frame.convert("RGBA"), base_im.convert("RGBA")
1232 )
1233 bbox = delta.getbbox(alpha_only=False)
1234 if (
1235 not bbox
1236 and prev_disposal == encoderinfo.get("disposal")
1237 and prev_blend == encoderinfo.get("blend")
1238 and "duration" in encoderinfo
1239 ):
1240 previous.encoderinfo["duration"] += encoderinfo["duration"]
1241 continue
1242 else:
1243 bbox = None
1244 im_frames.append(_Frame(im_frame, bbox, encoderinfo))
1246 if len(im_frames) == 1 and not default_image:
1247 return im_frames[0].im
1249 # animation control
1250 chunk(
1251 fp,
1252 b"acTL",
1253 o32(len(im_frames)), # 0: num_frames
1254 o32(loop), # 4: num_plays
1255 )
1257 # default image IDAT (if it exists)
1258 if default_image:
1259 default_im = im if im.mode == mode else im.convert(mode)
1260 _apply_encoderinfo(default_im, im.encoderinfo)
1261 ImageFile._save(
1262 default_im,
1263 cast(IO[bytes], _idat(fp, chunk)),
1264 [ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)],
1265 )
1267 seq_num = 0
1268 for frame, frame_data in enumerate(im_frames):
1269 im_frame = frame_data.im
1270 if not frame_data.bbox:
1271 bbox = (0, 0) + im_frame.size
1272 else:
1273 bbox = frame_data.bbox
1274 im_frame = im_frame.crop(bbox)
1275 size = im_frame.size
1276 encoderinfo = frame_data.encoderinfo
1277 frame_duration = int(round(encoderinfo.get("duration", 0)))
1278 frame_disposal = encoderinfo.get("disposal", disposal)
1279 frame_blend = encoderinfo.get("blend", blend)
1280 # frame control
1281 chunk(
1282 fp,
1283 b"fcTL",
1284 o32(seq_num), # sequence_number
1285 o32(size[0]), # width
1286 o32(size[1]), # height
1287 o32(bbox[0]), # x_offset
1288 o32(bbox[1]), # y_offset
1289 o16(frame_duration), # delay_numerator
1290 o16(1000), # delay_denominator
1291 o8(frame_disposal), # dispose_op
1292 o8(frame_blend), # blend_op
1293 )
1294 seq_num += 1
1295 # frame data
1296 _apply_encoderinfo(im_frame, im.encoderinfo)
1297 if frame == 0 and not default_image:
1298 # first frame must be in IDAT chunks for backwards compatibility
1299 ImageFile._save(
1300 im_frame,
1301 cast(IO[bytes], _idat(fp, chunk)),
1302 [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)],
1303 )
1304 else:
1305 fdat_chunks = _fdat(fp, chunk, seq_num)
1306 ImageFile._save(
1307 im_frame,
1308 cast(IO[bytes], fdat_chunks),
1309 [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)],
1310 )
1311 seq_num = fdat_chunks.seq_num
1312 return None
1315def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
1316 _save(im, fp, filename, save_all=True)
1319def _save(
1320 im: Image.Image,
1321 fp: IO[bytes],
1322 filename: str | bytes,
1323 chunk: Callable[..., None] = putchunk,
1324 save_all: bool = False,
1325) -> None:
1326 # save an image to disk (called by the save method)
1328 if save_all:
1329 default_image = im.encoderinfo.get(
1330 "default_image", im.info.get("default_image")
1331 )
1332 modes = set()
1333 sizes = set()
1334 append_images = im.encoderinfo.get("append_images", [])
1335 for im_seq in itertools.chain([im], append_images):
1336 for im_frame in ImageSequence.Iterator(im_seq):
1337 modes.add(im_frame.mode)
1338 sizes.add(im_frame.size)
1339 for mode in ("RGBA", "RGB", "P"):
1340 if mode in modes:
1341 break
1342 else:
1343 mode = modes.pop()
1344 size = tuple(max(frame_size[i] for frame_size in sizes) for i in range(2))
1345 else:
1346 size = im.size
1347 mode = im.mode
1349 outmode = mode
1350 if mode == "P":
1351 #
1352 # attempt to minimize storage requirements for palette images
1353 if "bits" in im.encoderinfo:
1354 # number of bits specified by user
1355 colors = min(1 << im.encoderinfo["bits"], 256)
1356 else:
1357 # check palette contents
1358 if im.palette:
1359 colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1)
1360 else:
1361 colors = 256
1363 if colors <= 16:
1364 if colors <= 2:
1365 bits = 1
1366 elif colors <= 4:
1367 bits = 2
1368 else:
1369 bits = 4
1370 outmode += f";{bits}"
1372 # get the corresponding PNG mode
1373 try:
1374 rawmode, bit_depth, color_type = _OUTMODES[outmode]
1375 except KeyError as e:
1376 msg = f"cannot write mode {mode} as PNG"
1377 raise OSError(msg) from e
1378 if outmode == "I":
1379 deprecate("Saving I mode images as PNG", 13, stacklevel=4)
1381 #
1382 # write minimal PNG file
1384 fp.write(_MAGIC)
1386 chunk(
1387 fp,
1388 b"IHDR",
1389 o32(size[0]), # 0: size
1390 o32(size[1]),
1391 bit_depth,
1392 color_type,
1393 b"\0", # 10: compression
1394 b"\0", # 11: filter category
1395 b"\0", # 12: interlace flag
1396 )
1398 chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"]
1400 icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile"))
1401 if icc:
1402 # ICC profile
1403 # according to PNG spec, the iCCP chunk contains:
1404 # Profile name 1-79 bytes (character string)
1405 # Null separator 1 byte (null character)
1406 # Compression method 1 byte (0)
1407 # Compressed profile n bytes (zlib with deflate compression)
1408 name = b"ICC Profile"
1409 data = name + b"\0\0" + zlib.compress(icc)
1410 chunk(fp, b"iCCP", data)
1412 # You must either have sRGB or iCCP.
1413 # Disallow sRGB chunks when an iCCP-chunk has been emitted.
1414 chunks.remove(b"sRGB")
1416 info = im.encoderinfo.get("pnginfo")
1417 if info:
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 = im.im.getpalette("RGB")[: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", None))
1441 if transparency or transparency == 0:
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 else:
1448 transparency = max(0, min(255, transparency))
1449 alpha = b"\xff" * transparency + b"\0"
1450 chunk(fp, b"tRNS", alpha[:alpha_bytes])
1451 elif im.mode in ("1", "L", "I", "I;16"):
1452 transparency = max(0, min(65535, transparency))
1453 chunk(fp, b"tRNS", o16(transparency))
1454 elif im.mode == "RGB":
1455 red, green, blue = transparency
1456 chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue))
1457 else:
1458 if "transparency" in im.encoderinfo:
1459 # don't bother with transparency if it's an RGBA
1460 # and it's in the info dict. It's probably just stale.
1461 msg = "cannot use transparency for this mode"
1462 raise OSError(msg)
1463 else:
1464 if im.mode == "P" and im.im.getpalettemode() == "RGBA":
1465 alpha = im.im.getpalette("RGBA", "A")
1466 alpha_bytes = colors
1467 chunk(fp, b"tRNS", alpha[:alpha_bytes])
1469 dpi = im.encoderinfo.get("dpi")
1470 if dpi:
1471 chunk(
1472 fp,
1473 b"pHYs",
1474 o32(int(dpi[0] / 0.0254 + 0.5)),
1475 o32(int(dpi[1] / 0.0254 + 0.5)),
1476 b"\x01",
1477 )
1479 if info:
1480 chunks = [b"bKGD", b"hIST"]
1481 for info_chunk in info.chunks:
1482 cid, data = info_chunk[:2]
1483 if cid in chunks:
1484 chunks.remove(cid)
1485 chunk(fp, cid, data)
1487 exif = im.encoderinfo.get("exif")
1488 if exif:
1489 if isinstance(exif, Image.Exif):
1490 exif = exif.tobytes(8)
1491 if exif.startswith(b"Exif\x00\x00"):
1492 exif = exif[6:]
1493 chunk(fp, b"eXIf", exif)
1495 single_im: Image.Image | None = im
1496 if save_all:
1497 single_im = _write_multiple_frames(
1498 im, fp, chunk, mode, rawmode, default_image, append_images
1499 )
1500 if single_im:
1501 _apply_encoderinfo(single_im, im.encoderinfo)
1502 ImageFile._save(
1503 single_im,
1504 cast(IO[bytes], _idat(fp, chunk)),
1505 [ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)],
1506 )
1508 if info:
1509 for info_chunk in info.chunks:
1510 cid, data = info_chunk[:2]
1511 if cid[1:2].islower():
1512 # Private chunk
1513 after_idat = len(info_chunk) == 3 and info_chunk[2]
1514 if after_idat:
1515 chunk(fp, cid, data)
1517 chunk(fp, b"IEND", b"")
1519 if hasattr(fp, "flush"):
1520 fp.flush()
1523# --------------------------------------------------------------------
1524# PNG chunk converter
1527def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes]]:
1528 """Return a list of PNG chunks representing this image."""
1529 from io import BytesIO
1531 chunks = []
1533 def append(fp: IO[bytes], cid: bytes, *data: bytes) -> None:
1534 byte_data = b"".join(data)
1535 crc = o32(_crc32(byte_data, _crc32(cid)))
1536 chunks.append((cid, byte_data, crc))
1538 fp = BytesIO()
1540 try:
1541 im.encoderinfo = params
1542 _save(im, fp, "", append)
1543 finally:
1544 del im.encoderinfo
1546 return chunks
1549# --------------------------------------------------------------------
1550# Registry
1552Image.register_open(PngImageFile.format, PngImageFile, _accept)
1553Image.register_save(PngImageFile.format, _save)
1554Image.register_save_all(PngImageFile.format, _save_all)
1556Image.register_extensions(PngImageFile.format, [".png", ".apng"])
1558Image.register_mime(PngImageFile.format, "image/png")