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