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