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