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