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