Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/PIL/PngImagePlugin.py: 23%
824 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
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#
34import itertools
35import logging
36import re
37import struct
38import warnings
39import zlib
40from enum import IntEnum
42from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
43from ._binary import i16be as i16
44from ._binary import i32be as i32
45from ._binary import o8
46from ._binary import o16be as o16
47from ._binary import o32be as o32
48from ._deprecate import deprecate
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 # Greyscale
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 # Greyscale 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 __getattr__(name):
135 for enum, prefix in {Disposal: "APNG_DISPOSE_", Blend: "APNG_BLEND_"}.items():
136 if name.startswith(prefix):
137 name = name[len(prefix) :]
138 if name in enum.__members__:
139 deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
140 return enum[name]
141 raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
144def _safe_zlib_decompress(s):
145 dobj = zlib.decompressobj()
146 plaintext = dobj.decompress(s, MAX_TEXT_CHUNK)
147 if dobj.unconsumed_tail:
148 raise ValueError("Decompressed Data Too Large")
149 return plaintext
152def _crc32(data, seed=0):
153 return zlib.crc32(data, seed) & 0xFFFFFFFF
156# --------------------------------------------------------------------
157# Support classes. Suitable for PNG and related formats like MNG etc.
160class ChunkStream:
161 def __init__(self, fp):
163 self.fp = fp
164 self.queue = []
166 def read(self):
167 """Fetch a new chunk. Returns header information."""
168 cid = None
170 if self.queue: 170 ↛ 171line 170 didn't jump to line 171, because the condition on line 170 was never true
171 cid, pos, length = self.queue.pop()
172 self.fp.seek(pos)
173 else:
174 s = self.fp.read(8)
175 cid = s[4:]
176 pos = self.fp.tell()
177 length = i32(s)
179 if not is_cid(cid): 179 ↛ 180line 179 didn't jump to line 180, because the condition on line 179 was never true
180 if not ImageFile.LOAD_TRUNCATED_IMAGES:
181 raise SyntaxError(f"broken PNG file (chunk {repr(cid)})")
183 return cid, pos, length
185 def __enter__(self):
186 return self
188 def __exit__(self, *args):
189 self.close()
191 def close(self):
192 self.queue = self.crc = self.fp = None
194 def push(self, cid, pos, length):
196 self.queue.append((cid, pos, length))
198 def call(self, cid, pos, length):
199 """Call the appropriate chunk handler"""
201 logger.debug("STREAM %r %s %s", cid, pos, length)
202 return getattr(self, "chunk_" + cid.decode("ascii"))(pos, length)
204 def crc(self, cid, data):
205 """Read and verify checksum"""
207 # Skip CRC checks for ancillary chunks if allowed to load truncated
208 # images
209 # 5th byte of first char is 1 [specs, section 5.4]
210 if ImageFile.LOAD_TRUNCATED_IMAGES and (cid[0] >> 5 & 1): 210 ↛ 211line 210 didn't jump to line 211, because the condition on line 210 was never true
211 self.crc_skip(cid, data)
212 return
214 try:
215 crc1 = _crc32(data, _crc32(cid))
216 crc2 = i32(self.fp.read(4))
217 if crc1 != crc2: 217 ↛ 218line 217 didn't jump to line 218, because the condition on line 217 was never true
218 raise SyntaxError(
219 f"broken PNG file (bad header checksum in {repr(cid)})"
220 )
221 except struct.error as e:
222 raise SyntaxError(
223 f"broken PNG file (incomplete checksum in {repr(cid)})"
224 ) from e
226 def crc_skip(self, cid, data):
227 """Read checksum. Used if the C module is not present"""
229 self.fp.read(4)
231 def verify(self, endchunk=b"IEND"):
233 # Simple approach; just calculate checksum for all remaining
234 # blocks. Must be called directly after open.
236 cids = []
238 while True:
239 try:
240 cid, pos, length = self.read()
241 except struct.error as e:
242 raise OSError("truncated PNG file") from e
244 if cid == endchunk:
245 break
246 self.crc(cid, ImageFile._safe_read(self.fp, length))
247 cids.append(cid)
249 return cids
252class iTXt(str):
253 """
254 Subclass of string to allow iTXt chunks to look like strings while
255 keeping their extra information
257 """
259 @staticmethod
260 def __new__(cls, text, lang=None, tkey=None):
261 """
262 :param cls: the class to use when creating the instance
263 :param text: value for this key
264 :param lang: language code
265 :param tkey: UTF-8 version of the key name
266 """
268 self = str.__new__(cls, text)
269 self.lang = lang
270 self.tkey = tkey
271 return self
274class PngInfo:
275 """
276 PNG chunk container (for use with save(pnginfo=))
278 """
280 def __init__(self):
281 self.chunks = []
283 def add(self, cid, data, after_idat=False):
284 """Appends an arbitrary chunk. Use with caution.
286 :param cid: a byte string, 4 bytes long.
287 :param data: a byte string of the encoded data
288 :param after_idat: for use with private chunks. Whether the chunk
289 should be written after IDAT
291 """
293 chunk = [cid, data]
294 if after_idat:
295 chunk.append(True)
296 self.chunks.append(tuple(chunk))
298 def add_itxt(self, key, value, lang="", tkey="", zip=False):
299 """Appends an iTXt chunk.
301 :param key: latin-1 encodable text key name
302 :param value: value for this key
303 :param lang: language code
304 :param tkey: UTF-8 version of the key name
305 :param zip: compression flag
307 """
309 if not isinstance(key, bytes):
310 key = key.encode("latin-1", "strict")
311 if not isinstance(value, bytes):
312 value = value.encode("utf-8", "strict")
313 if not isinstance(lang, bytes):
314 lang = lang.encode("utf-8", "strict")
315 if not isinstance(tkey, bytes):
316 tkey = tkey.encode("utf-8", "strict")
318 if zip:
319 self.add(
320 b"iTXt",
321 key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value),
322 )
323 else:
324 self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value)
326 def add_text(self, key, value, zip=False):
327 """Appends a text chunk.
329 :param key: latin-1 encodable text key name
330 :param value: value for this key, text or an
331 :py:class:`PIL.PngImagePlugin.iTXt` instance
332 :param zip: compression flag
334 """
335 if isinstance(value, iTXt):
336 return self.add_itxt(key, value, value.lang, value.tkey, zip=zip)
338 # The tEXt chunk stores latin-1 text
339 if not isinstance(value, bytes):
340 try:
341 value = value.encode("latin-1", "strict")
342 except UnicodeError:
343 return self.add_itxt(key, value, zip=zip)
345 if not isinstance(key, bytes):
346 key = key.encode("latin-1", "strict")
348 if zip:
349 self.add(b"zTXt", key + b"\0\0" + zlib.compress(value))
350 else:
351 self.add(b"tEXt", key + b"\0" + value)
354# --------------------------------------------------------------------
355# PNG image stream (IHDR/IEND)
358class PngStream(ChunkStream):
359 def __init__(self, fp):
360 super().__init__(fp)
362 # local copies of Image attributes
363 self.im_info = {}
364 self.im_text = {}
365 self.im_size = (0, 0)
366 self.im_mode = None
367 self.im_tile = None
368 self.im_palette = None
369 self.im_custom_mimetype = None
370 self.im_n_frames = None
371 self._seq_num = None
372 self.rewind_state = None
374 self.text_memory = 0
376 def check_text_memory(self, chunklen):
377 self.text_memory += chunklen
378 if self.text_memory > MAX_TEXT_MEMORY:
379 raise ValueError(
380 "Too much memory used in text chunks: "
381 f"{self.text_memory}>MAX_TEXT_MEMORY"
382 )
384 def save_rewind(self):
385 self.rewind_state = {
386 "info": self.im_info.copy(),
387 "tile": self.im_tile,
388 "seq_num": self._seq_num,
389 }
391 def rewind(self):
392 self.im_info = self.rewind_state["info"]
393 self.im_tile = self.rewind_state["tile"]
394 self._seq_num = self.rewind_state["seq_num"]
396 def chunk_iCCP(self, pos, length):
398 # ICC profile
399 s = ImageFile._safe_read(self.fp, length)
400 # according to PNG spec, the iCCP chunk contains:
401 # Profile name 1-79 bytes (character string)
402 # Null separator 1 byte (null character)
403 # Compression method 1 byte (0)
404 # Compressed profile n bytes (zlib with deflate compression)
405 i = s.find(b"\0")
406 logger.debug("iCCP profile name %r", s[:i])
407 logger.debug("Compression method %s", s[i])
408 comp_method = s[i]
409 if comp_method != 0:
410 raise SyntaxError(f"Unknown compression method {comp_method} in iCCP chunk")
411 try:
412 icc_profile = _safe_zlib_decompress(s[i + 2 :])
413 except ValueError:
414 if ImageFile.LOAD_TRUNCATED_IMAGES:
415 icc_profile = None
416 else:
417 raise
418 except zlib.error:
419 icc_profile = None # FIXME
420 self.im_info["icc_profile"] = icc_profile
421 return s
423 def chunk_IHDR(self, pos, length):
425 # image header
426 s = ImageFile._safe_read(self.fp, length)
427 if length < 13: 427 ↛ 428line 427 didn't jump to line 428, because the condition on line 427 was never true
428 if ImageFile.LOAD_TRUNCATED_IMAGES:
429 return s
430 raise ValueError("Truncated IHDR chunk")
431 self.im_size = i32(s, 0), i32(s, 4)
432 try:
433 self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])]
434 except Exception:
435 pass
436 if s[12]: 436 ↛ 437line 436 didn't jump to line 437, because the condition on line 436 was never true
437 self.im_info["interlace"] = 1
438 if s[11]: 438 ↛ 439line 438 didn't jump to line 439, because the condition on line 438 was never true
439 raise SyntaxError("unknown filter category")
440 return s
442 def chunk_IDAT(self, pos, length):
444 # image data
445 if "bbox" in self.im_info: 445 ↛ 446line 445 didn't jump to line 446, because the condition on line 445 was never true
446 tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)]
447 else:
448 if self.im_n_frames is not None: 448 ↛ 449line 448 didn't jump to line 449, because the condition on line 448 was never true
449 self.im_info["default_image"] = True
450 tile = [("zip", (0, 0) + self.im_size, pos, self.im_rawmode)]
451 self.im_tile = tile
452 self.im_idat = length
453 raise EOFError
455 def chunk_IEND(self, pos, length):
457 # end of PNG image
458 raise EOFError
460 def chunk_PLTE(self, pos, length):
462 # palette
463 s = ImageFile._safe_read(self.fp, length)
464 if self.im_mode == "P":
465 self.im_palette = "RGB", s
466 return s
468 def chunk_tRNS(self, pos, length):
470 # transparency
471 s = ImageFile._safe_read(self.fp, length)
472 if self.im_mode == "P":
473 if _simple_palette.match(s):
474 # tRNS contains only one full-transparent entry,
475 # other entries are full opaque
476 i = s.find(b"\0")
477 if i >= 0:
478 self.im_info["transparency"] = i
479 else:
480 # otherwise, we have a byte string with one alpha value
481 # for each palette entry
482 self.im_info["transparency"] = s
483 elif self.im_mode in ("1", "L", "I"):
484 self.im_info["transparency"] = i16(s)
485 elif self.im_mode == "RGB":
486 self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4)
487 return s
489 def chunk_gAMA(self, pos, length):
490 # gamma setting
491 s = ImageFile._safe_read(self.fp, length)
492 self.im_info["gamma"] = i32(s) / 100000.0
493 return s
495 def chunk_cHRM(self, pos, length):
496 # chromaticity, 8 unsigned ints, actual value is scaled by 100,000
497 # WP x,y, Red x,y, Green x,y Blue x,y
499 s = ImageFile._safe_read(self.fp, length)
500 raw_vals = struct.unpack(">%dI" % (len(s) // 4), s)
501 self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals)
502 return s
504 def chunk_sRGB(self, pos, length):
505 # srgb rendering intent, 1 byte
506 # 0 perceptual
507 # 1 relative colorimetric
508 # 2 saturation
509 # 3 absolute colorimetric
511 s = ImageFile._safe_read(self.fp, length)
512 self.im_info["srgb"] = s[0]
513 return s
515 def chunk_pHYs(self, pos, length):
517 # pixels per unit
518 s = ImageFile._safe_read(self.fp, length)
519 if length < 9:
520 if ImageFile.LOAD_TRUNCATED_IMAGES:
521 return s
522 raise ValueError("Truncated pHYs chunk")
523 px, py = i32(s, 0), i32(s, 4)
524 unit = s[8]
525 if unit == 1: # meter
526 dpi = px * 0.0254, py * 0.0254
527 self.im_info["dpi"] = dpi
528 elif unit == 0:
529 self.im_info["aspect"] = px, py
530 return s
532 def chunk_tEXt(self, pos, length):
534 # text
535 s = ImageFile._safe_read(self.fp, length)
536 try:
537 k, v = s.split(b"\0", 1)
538 except ValueError:
539 # fallback for broken tEXt tags
540 k = s
541 v = b""
542 if k:
543 k = k.decode("latin-1", "strict")
544 v_str = v.decode("latin-1", "replace")
546 self.im_info[k] = v if k == "exif" else v_str
547 self.im_text[k] = v_str
548 self.check_text_memory(len(v_str))
550 return s
552 def chunk_zTXt(self, pos, length):
554 # compressed text
555 s = ImageFile._safe_read(self.fp, length)
556 try:
557 k, v = s.split(b"\0", 1)
558 except ValueError:
559 k = s
560 v = b""
561 if v:
562 comp_method = v[0]
563 else:
564 comp_method = 0
565 if comp_method != 0:
566 raise SyntaxError(f"Unknown compression method {comp_method} in zTXt chunk")
567 try:
568 v = _safe_zlib_decompress(v[1:])
569 except ValueError:
570 if ImageFile.LOAD_TRUNCATED_IMAGES:
571 v = b""
572 else:
573 raise
574 except zlib.error:
575 v = b""
577 if k:
578 k = k.decode("latin-1", "strict")
579 v = v.decode("latin-1", "replace")
581 self.im_info[k] = self.im_text[k] = v
582 self.check_text_memory(len(v))
584 return s
586 def chunk_iTXt(self, pos, length):
588 # international text
589 r = s = ImageFile._safe_read(self.fp, length)
590 try:
591 k, r = r.split(b"\0", 1)
592 except ValueError:
593 return s
594 if len(r) < 2:
595 return s
596 cf, cm, r = r[0], r[1], r[2:]
597 try:
598 lang, tk, v = r.split(b"\0", 2)
599 except ValueError:
600 return s
601 if cf != 0:
602 if cm == 0:
603 try:
604 v = _safe_zlib_decompress(v)
605 except ValueError:
606 if ImageFile.LOAD_TRUNCATED_IMAGES:
607 return s
608 else:
609 raise
610 except zlib.error:
611 return s
612 else:
613 return s
614 try:
615 k = k.decode("latin-1", "strict")
616 lang = lang.decode("utf-8", "strict")
617 tk = tk.decode("utf-8", "strict")
618 v = v.decode("utf-8", "strict")
619 except UnicodeError:
620 return s
622 self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk)
623 self.check_text_memory(len(v))
625 return s
627 def chunk_eXIf(self, pos, length):
628 s = ImageFile._safe_read(self.fp, length)
629 self.im_info["exif"] = b"Exif\x00\x00" + s
630 return s
632 # APNG chunks
633 def chunk_acTL(self, pos, length):
634 s = ImageFile._safe_read(self.fp, length)
635 if length < 8:
636 if ImageFile.LOAD_TRUNCATED_IMAGES:
637 return s
638 raise ValueError("APNG contains truncated acTL chunk")
639 if self.im_n_frames is not None:
640 self.im_n_frames = None
641 warnings.warn("Invalid APNG, will use default PNG image if possible")
642 return s
643 n_frames = i32(s)
644 if n_frames == 0 or n_frames > 0x80000000:
645 warnings.warn("Invalid APNG, will use default PNG image if possible")
646 return s
647 self.im_n_frames = n_frames
648 self.im_info["loop"] = i32(s, 4)
649 self.im_custom_mimetype = "image/apng"
650 return s
652 def chunk_fcTL(self, pos, length):
653 s = ImageFile._safe_read(self.fp, length)
654 if length < 26:
655 if ImageFile.LOAD_TRUNCATED_IMAGES:
656 return s
657 raise ValueError("APNG contains truncated fcTL chunk")
658 seq = i32(s)
659 if (self._seq_num is None and seq != 0) or (
660 self._seq_num is not None and self._seq_num != seq - 1
661 ):
662 raise SyntaxError("APNG contains frame sequence errors")
663 self._seq_num = seq
664 width, height = i32(s, 4), i32(s, 8)
665 px, py = i32(s, 12), i32(s, 16)
666 im_w, im_h = self.im_size
667 if px + width > im_w or py + height > im_h:
668 raise SyntaxError("APNG contains invalid frames")
669 self.im_info["bbox"] = (px, py, px + width, py + height)
670 delay_num, delay_den = i16(s, 20), i16(s, 22)
671 if delay_den == 0:
672 delay_den = 100
673 self.im_info["duration"] = float(delay_num) / float(delay_den) * 1000
674 self.im_info["disposal"] = s[24]
675 self.im_info["blend"] = s[25]
676 return s
678 def chunk_fdAT(self, pos, length):
679 if length < 4:
680 if ImageFile.LOAD_TRUNCATED_IMAGES:
681 s = ImageFile._safe_read(self.fp, length)
682 return s
683 raise ValueError("APNG contains truncated fDAT chunk")
684 s = ImageFile._safe_read(self.fp, 4)
685 seq = i32(s)
686 if self._seq_num != seq - 1:
687 raise SyntaxError("APNG contains frame sequence errors")
688 self._seq_num = seq
689 return self.chunk_IDAT(pos + 4, length - 4)
692# --------------------------------------------------------------------
693# PNG reader
696def _accept(prefix):
697 return prefix[:8] == _MAGIC
700##
701# Image plugin for PNG images.
704class PngImageFile(ImageFile.ImageFile):
706 format = "PNG"
707 format_description = "Portable network graphics"
709 def _open(self):
711 if not _accept(self.fp.read(8)): 711 ↛ 712line 711 didn't jump to line 712, because the condition on line 711 was never true
712 raise SyntaxError("not a PNG file")
713 self._fp = self.fp
714 self.__frame = 0
716 #
717 # Parse headers up to the first IDAT or fDAT chunk
719 self.private_chunks = []
720 self.png = PngStream(self.fp)
722 while True:
724 #
725 # get next chunk
727 cid, pos, length = self.png.read()
729 try:
730 s = self.png.call(cid, pos, length)
731 except EOFError: 731 ↛ 733line 731 didn't jump to line 733
732 break
733 except AttributeError:
734 logger.debug("%r %s %s (unknown)", cid, pos, length)
735 s = ImageFile._safe_read(self.fp, length)
736 if cid[1:2].islower():
737 self.private_chunks.append((cid, s))
739 self.png.crc(cid, s)
741 #
742 # Copy relevant attributes from the PngStream. An alternative
743 # would be to let the PngStream class modify these attributes
744 # directly, but that introduces circular references which are
745 # difficult to break if things go wrong in the decoder...
746 # (believe me, I've tried ;-)
748 self.mode = self.png.im_mode
749 self._size = self.png.im_size
750 self.info = self.png.im_info
751 self._text = None
752 self.tile = self.png.im_tile
753 self.custom_mimetype = self.png.im_custom_mimetype
754 self.n_frames = self.png.im_n_frames or 1
755 self.default_image = self.info.get("default_image", False)
757 if self.png.im_palette: 757 ↛ 758line 757 didn't jump to line 758, because the condition on line 757 was never true
758 rawmode, data = self.png.im_palette
759 self.palette = ImagePalette.raw(rawmode, data)
761 if cid == b"fdAT": 761 ↛ 762line 761 didn't jump to line 762, because the condition on line 761 was never true
762 self.__prepare_idat = length - 4
763 else:
764 self.__prepare_idat = length # used by load_prepare()
766 if self.png.im_n_frames is not None: 766 ↛ 767line 766 didn't jump to line 767, because the condition on line 766 was never true
767 self._close_exclusive_fp_after_loading = False
768 self.png.save_rewind()
769 self.__rewind_idat = self.__prepare_idat
770 self.__rewind = self._fp.tell()
771 if self.default_image:
772 # IDAT chunk contains default image and not first animation frame
773 self.n_frames += 1
774 self._seek(0)
775 self.is_animated = self.n_frames > 1
777 @property
778 def text(self):
779 # experimental
780 if self._text is None:
781 # iTxt, tEXt and zTXt chunks may appear at the end of the file
782 # So load the file to ensure that they are read
783 if self.is_animated:
784 frame = self.__frame
785 # for APNG, seek to the final frame before loading
786 self.seek(self.n_frames - 1)
787 self.load()
788 if self.is_animated:
789 self.seek(frame)
790 return self._text
792 def verify(self):
793 """Verify PNG file"""
795 if self.fp is None: 795 ↛ 796line 795 didn't jump to line 796, because the condition on line 795 was never true
796 raise RuntimeError("verify must be called directly after open")
798 # back up to beginning of IDAT block
799 self.fp.seek(self.tile[0][2] - 8)
801 self.png.verify()
802 self.png.close()
804 if self._exclusive_fp: 804 ↛ 805line 804 didn't jump to line 805, because the condition on line 804 was never true
805 self.fp.close()
806 self.fp = None
808 def seek(self, frame):
809 if not self._seek_check(frame):
810 return
811 if frame < self.__frame:
812 self._seek(0, True)
814 last_frame = self.__frame
815 for f in range(self.__frame + 1, frame + 1):
816 try:
817 self._seek(f)
818 except EOFError as e:
819 self.seek(last_frame)
820 raise EOFError("no more images in APNG file") from e
822 def _seek(self, frame, rewind=False):
823 if frame == 0:
824 if rewind:
825 self._fp.seek(self.__rewind)
826 self.png.rewind()
827 self.__prepare_idat = self.__rewind_idat
828 self.im = None
829 if self.pyaccess:
830 self.pyaccess = None
831 self.info = self.png.im_info
832 self.tile = self.png.im_tile
833 self.fp = self._fp
834 self._prev_im = None
835 self.dispose = None
836 self.default_image = self.info.get("default_image", False)
837 self.dispose_op = self.info.get("disposal")
838 self.blend_op = self.info.get("blend")
839 self.dispose_extent = self.info.get("bbox")
840 self.__frame = 0
841 else:
842 if frame != self.__frame + 1:
843 raise ValueError(f"cannot seek to frame {frame}")
845 # ensure previous frame was loaded
846 self.load()
848 if self.dispose:
849 self.im.paste(self.dispose, self.dispose_extent)
850 self._prev_im = self.im.copy()
852 self.fp = self._fp
854 # advance to the next frame
855 if self.__prepare_idat:
856 ImageFile._safe_read(self.fp, self.__prepare_idat)
857 self.__prepare_idat = 0
858 frame_start = False
859 while True:
860 self.fp.read(4) # CRC
862 try:
863 cid, pos, length = self.png.read()
864 except (struct.error, SyntaxError):
865 break
867 if cid == b"IEND":
868 raise EOFError("No more images in APNG file")
869 if cid == b"fcTL":
870 if frame_start:
871 # there must be at least one fdAT chunk between fcTL chunks
872 raise SyntaxError("APNG missing frame data")
873 frame_start = True
875 try:
876 self.png.call(cid, pos, length)
877 except UnicodeDecodeError:
878 break
879 except EOFError:
880 if cid == b"fdAT":
881 length -= 4
882 if frame_start:
883 self.__prepare_idat = length
884 break
885 ImageFile._safe_read(self.fp, length)
886 except AttributeError:
887 logger.debug("%r %s %s (unknown)", cid, pos, length)
888 ImageFile._safe_read(self.fp, length)
890 self.__frame = frame
891 self.tile = self.png.im_tile
892 self.dispose_op = self.info.get("disposal")
893 self.blend_op = self.info.get("blend")
894 self.dispose_extent = self.info.get("bbox")
896 if not self.tile:
897 raise EOFError
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 self._prev_im.paste(
998 updated, self.dispose_extent, updated.convert("RGBA")
999 )
1000 self.im = self._prev_im
1001 if self.pyaccess:
1002 self.pyaccess = None
1004 def _getexif(self):
1005 if "exif" not in self.info:
1006 self.load()
1007 if "exif" not in self.info and "Raw profile type exif" not in self.info:
1008 return None
1009 return self.getexif()._get_merged_dict()
1011 def getexif(self):
1012 if "exif" not in self.info:
1013 self.load()
1015 return super().getexif()
1017 def getxmp(self):
1018 """
1019 Returns a dictionary containing the XMP tags.
1020 Requires defusedxml to be installed.
1022 :returns: XMP tags in a dictionary.
1023 """
1024 return (
1025 self._getxmp(self.info["XML:com.adobe.xmp"])
1026 if "XML:com.adobe.xmp" in self.info
1027 else {}
1028 )
1031# --------------------------------------------------------------------
1032# PNG writer
1034_OUTMODES = {
1035 # supported PIL modes, and corresponding rawmodes/bits/color combinations
1036 "1": ("1", b"\x01\x00"),
1037 "L;1": ("L;1", b"\x01\x00"),
1038 "L;2": ("L;2", b"\x02\x00"),
1039 "L;4": ("L;4", b"\x04\x00"),
1040 "L": ("L", b"\x08\x00"),
1041 "LA": ("LA", b"\x08\x04"),
1042 "I": ("I;16B", b"\x10\x00"),
1043 "I;16": ("I;16B", b"\x10\x00"),
1044 "P;1": ("P;1", b"\x01\x03"),
1045 "P;2": ("P;2", b"\x02\x03"),
1046 "P;4": ("P;4", b"\x04\x03"),
1047 "P": ("P", b"\x08\x03"),
1048 "RGB": ("RGB", b"\x08\x02"),
1049 "RGBA": ("RGBA", b"\x08\x06"),
1050}
1053def putchunk(fp, cid, *data):
1054 """Write a PNG chunk (including CRC field)"""
1056 data = b"".join(data)
1058 fp.write(o32(len(data)) + cid)
1059 fp.write(data)
1060 crc = _crc32(data, _crc32(cid))
1061 fp.write(o32(crc))
1064class _idat:
1065 # wrap output from the encoder in IDAT chunks
1067 def __init__(self, fp, chunk):
1068 self.fp = fp
1069 self.chunk = chunk
1071 def write(self, data):
1072 self.chunk(self.fp, b"IDAT", data)
1075class _fdat:
1076 # wrap encoder output in fdAT chunks
1078 def __init__(self, fp, chunk, seq_num):
1079 self.fp = fp
1080 self.chunk = chunk
1081 self.seq_num = seq_num
1083 def write(self, data):
1084 self.chunk(self.fp, b"fdAT", o32(self.seq_num), data)
1085 self.seq_num += 1
1088def _write_multiple_frames(im, fp, chunk, rawmode):
1089 default_image = im.encoderinfo.get("default_image", im.info.get("default_image"))
1090 duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
1091 loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
1092 disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
1093 blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))
1095 if default_image:
1096 chain = itertools.chain(im.encoderinfo.get("append_images", []))
1097 else:
1098 chain = itertools.chain([im], im.encoderinfo.get("append_images", []))
1100 im_frames = []
1101 frame_count = 0
1102 for im_seq in chain:
1103 for im_frame in ImageSequence.Iterator(im_seq):
1104 im_frame = im_frame.copy()
1105 if im_frame.mode != im.mode:
1106 if im.mode == "P":
1107 im_frame = im_frame.convert(im.mode, palette=im.palette)
1108 else:
1109 im_frame = im_frame.convert(im.mode)
1110 encoderinfo = im.encoderinfo.copy()
1111 if isinstance(duration, (list, tuple)):
1112 encoderinfo["duration"] = duration[frame_count]
1113 if isinstance(disposal, (list, tuple)):
1114 encoderinfo["disposal"] = disposal[frame_count]
1115 if isinstance(blend, (list, tuple)):
1116 encoderinfo["blend"] = blend[frame_count]
1117 frame_count += 1
1119 if im_frames:
1120 previous = im_frames[-1]
1121 prev_disposal = previous["encoderinfo"].get("disposal")
1122 prev_blend = previous["encoderinfo"].get("blend")
1123 if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2:
1124 prev_disposal = Disposal.OP_BACKGROUND
1126 if prev_disposal == Disposal.OP_BACKGROUND:
1127 base_im = previous["im"]
1128 dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0))
1129 bbox = previous["bbox"]
1130 if bbox:
1131 dispose = dispose.crop(bbox)
1132 else:
1133 bbox = (0, 0) + im.size
1134 base_im.paste(dispose, bbox)
1135 elif prev_disposal == Disposal.OP_PREVIOUS:
1136 base_im = im_frames[-2]["im"]
1137 else:
1138 base_im = previous["im"]
1139 delta = ImageChops.subtract_modulo(
1140 im_frame.convert("RGB"), base_im.convert("RGB")
1141 )
1142 bbox = delta.getbbox()
1143 if (
1144 not bbox
1145 and prev_disposal == encoderinfo.get("disposal")
1146 and prev_blend == encoderinfo.get("blend")
1147 ):
1148 if isinstance(duration, (list, tuple)):
1149 previous["encoderinfo"]["duration"] += encoderinfo["duration"]
1150 continue
1151 else:
1152 bbox = None
1153 im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
1155 # animation control
1156 chunk(
1157 fp,
1158 b"acTL",
1159 o32(len(im_frames)), # 0: num_frames
1160 o32(loop), # 4: num_plays
1161 )
1163 # default image IDAT (if it exists)
1164 if default_image:
1165 ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
1167 seq_num = 0
1168 for frame, frame_data in enumerate(im_frames):
1169 im_frame = frame_data["im"]
1170 if not frame_data["bbox"]:
1171 bbox = (0, 0) + im_frame.size
1172 else:
1173 bbox = frame_data["bbox"]
1174 im_frame = im_frame.crop(bbox)
1175 size = im_frame.size
1176 encoderinfo = frame_data["encoderinfo"]
1177 frame_duration = int(round(encoderinfo.get("duration", duration)))
1178 frame_disposal = encoderinfo.get("disposal", disposal)
1179 frame_blend = encoderinfo.get("blend", blend)
1180 # frame control
1181 chunk(
1182 fp,
1183 b"fcTL",
1184 o32(seq_num), # sequence_number
1185 o32(size[0]), # width
1186 o32(size[1]), # height
1187 o32(bbox[0]), # x_offset
1188 o32(bbox[1]), # y_offset
1189 o16(frame_duration), # delay_numerator
1190 o16(1000), # delay_denominator
1191 o8(frame_disposal), # dispose_op
1192 o8(frame_blend), # blend_op
1193 )
1194 seq_num += 1
1195 # frame data
1196 if frame == 0 and not default_image:
1197 # first frame must be in IDAT chunks for backwards compatibility
1198 ImageFile._save(
1199 im_frame,
1200 _idat(fp, chunk),
1201 [("zip", (0, 0) + im_frame.size, 0, rawmode)],
1202 )
1203 else:
1204 fdat_chunks = _fdat(fp, chunk, seq_num)
1205 ImageFile._save(
1206 im_frame,
1207 fdat_chunks,
1208 [("zip", (0, 0) + im_frame.size, 0, rawmode)],
1209 )
1210 seq_num = fdat_chunks.seq_num
1213def _save_all(im, fp, filename):
1214 _save(im, fp, filename, save_all=True)
1217def _save(im, fp, filename, chunk=putchunk, save_all=False):
1218 # save an image to disk (called by the save method)
1220 mode = im.mode
1222 if mode == "P": 1222 ↛ 1226line 1222 didn't jump to line 1226, because the condition on line 1222 was never true
1224 #
1225 # attempt to minimize storage requirements for palette images
1226 if "bits" in im.encoderinfo:
1227 # number of bits specified by user
1228 colors = min(1 << im.encoderinfo["bits"], 256)
1229 else:
1230 # check palette contents
1231 if im.palette:
1232 colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1)
1233 else:
1234 colors = 256
1236 if colors <= 16:
1237 if colors <= 2:
1238 bits = 1
1239 elif colors <= 4:
1240 bits = 2
1241 else:
1242 bits = 4
1243 mode = f"{mode};{bits}"
1245 # encoder options
1246 im.encoderconfig = (
1247 im.encoderinfo.get("optimize", False),
1248 im.encoderinfo.get("compress_level", -1),
1249 im.encoderinfo.get("compress_type", -1),
1250 im.encoderinfo.get("dictionary", b""),
1251 )
1253 # get the corresponding PNG mode
1254 try:
1255 rawmode, mode = _OUTMODES[mode]
1256 except KeyError as e:
1257 raise OSError(f"cannot write mode {mode} as PNG") from e
1259 #
1260 # write minimal PNG file
1262 fp.write(_MAGIC)
1264 chunk(
1265 fp,
1266 b"IHDR",
1267 o32(im.size[0]), # 0: size
1268 o32(im.size[1]),
1269 mode, # 8: depth/type
1270 b"\0", # 10: compression
1271 b"\0", # 11: filter category
1272 b"\0", # 12: interlace flag
1273 )
1275 chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"]
1277 icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile"))
1278 if icc: 1278 ↛ 1285line 1278 didn't jump to line 1285, because the condition on line 1278 was never true
1279 # ICC profile
1280 # according to PNG spec, the iCCP chunk contains:
1281 # Profile name 1-79 bytes (character string)
1282 # Null separator 1 byte (null character)
1283 # Compression method 1 byte (0)
1284 # Compressed profile n bytes (zlib with deflate compression)
1285 name = b"ICC Profile"
1286 data = name + b"\0\0" + zlib.compress(icc)
1287 chunk(fp, b"iCCP", data)
1289 # You must either have sRGB or iCCP.
1290 # Disallow sRGB chunks when an iCCP-chunk has been emitted.
1291 chunks.remove(b"sRGB")
1293 info = im.encoderinfo.get("pnginfo")
1294 if info: 1294 ↛ 1295line 1294 didn't jump to line 1295, because the condition on line 1294 was never true
1295 chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"]
1296 for info_chunk in info.chunks:
1297 cid, data = info_chunk[:2]
1298 if cid in chunks:
1299 chunks.remove(cid)
1300 chunk(fp, cid, data)
1301 elif cid in chunks_multiple_allowed:
1302 chunk(fp, cid, data)
1303 elif cid[1:2].islower():
1304 # Private chunk
1305 after_idat = info_chunk[2:3]
1306 if not after_idat:
1307 chunk(fp, cid, data)
1309 if im.mode == "P": 1309 ↛ 1310line 1309 didn't jump to line 1310, because the condition on line 1309 was never true
1310 palette_byte_number = colors * 3
1311 palette_bytes = im.im.getpalette("RGB")[:palette_byte_number]
1312 while len(palette_bytes) < palette_byte_number:
1313 palette_bytes += b"\0"
1314 chunk(fp, b"PLTE", palette_bytes)
1316 transparency = im.encoderinfo.get("transparency", im.info.get("transparency", None))
1318 if transparency or transparency == 0: 1318 ↛ 1319line 1318 didn't jump to line 1319, because the condition on line 1318 was never true
1319 if im.mode == "P":
1320 # limit to actual palette size
1321 alpha_bytes = colors
1322 if isinstance(transparency, bytes):
1323 chunk(fp, b"tRNS", transparency[:alpha_bytes])
1324 else:
1325 transparency = max(0, min(255, transparency))
1326 alpha = b"\xFF" * transparency + b"\0"
1327 chunk(fp, b"tRNS", alpha[:alpha_bytes])
1328 elif im.mode in ("1", "L", "I"):
1329 transparency = max(0, min(65535, transparency))
1330 chunk(fp, b"tRNS", o16(transparency))
1331 elif im.mode == "RGB":
1332 red, green, blue = transparency
1333 chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue))
1334 else:
1335 if "transparency" in im.encoderinfo:
1336 # don't bother with transparency if it's an RGBA
1337 # and it's in the info dict. It's probably just stale.
1338 raise OSError("cannot use transparency for this mode")
1339 else:
1340 if im.mode == "P" and im.im.getpalettemode() == "RGBA": 1340 ↛ 1341line 1340 didn't jump to line 1341, because the condition on line 1340 was never true
1341 alpha = im.im.getpalette("RGBA", "A")
1342 alpha_bytes = colors
1343 chunk(fp, b"tRNS", alpha[:alpha_bytes])
1345 dpi = im.encoderinfo.get("dpi")
1346 if dpi: 1346 ↛ 1347line 1346 didn't jump to line 1347, because the condition on line 1346 was never true
1347 chunk(
1348 fp,
1349 b"pHYs",
1350 o32(int(dpi[0] / 0.0254 + 0.5)),
1351 o32(int(dpi[1] / 0.0254 + 0.5)),
1352 b"\x01",
1353 )
1355 if info: 1355 ↛ 1356line 1355 didn't jump to line 1356, because the condition on line 1355 was never true
1356 chunks = [b"bKGD", b"hIST"]
1357 for info_chunk in info.chunks:
1358 cid, data = info_chunk[:2]
1359 if cid in chunks:
1360 chunks.remove(cid)
1361 chunk(fp, cid, data)
1363 exif = im.encoderinfo.get("exif", im.info.get("exif"))
1364 if exif: 1364 ↛ 1365line 1364 didn't jump to line 1365, because the condition on line 1364 was never true
1365 if isinstance(exif, Image.Exif):
1366 exif = exif.tobytes(8)
1367 if exif.startswith(b"Exif\x00\x00"):
1368 exif = exif[6:]
1369 chunk(fp, b"eXIf", exif)
1371 if save_all: 1371 ↛ 1372line 1371 didn't jump to line 1372, because the condition on line 1371 was never true
1372 _write_multiple_frames(im, fp, chunk, rawmode)
1373 else:
1374 ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
1376 if info: 1376 ↛ 1377line 1376 didn't jump to line 1377, because the condition on line 1376 was never true
1377 for info_chunk in info.chunks:
1378 cid, data = info_chunk[:2]
1379 if cid[1:2].islower():
1380 # Private chunk
1381 after_idat = info_chunk[2:3]
1382 if after_idat:
1383 chunk(fp, cid, data)
1385 chunk(fp, b"IEND", b"")
1387 if hasattr(fp, "flush"): 1387 ↛ exitline 1387 didn't return from function '_save', because the condition on line 1387 was never false
1388 fp.flush()
1391# --------------------------------------------------------------------
1392# PNG chunk converter
1395def getchunks(im, **params):
1396 """Return a list of PNG chunks representing this image."""
1398 class collector:
1399 data = []
1401 def write(self, data):
1402 pass
1404 def append(self, chunk):
1405 self.data.append(chunk)
1407 def append(fp, cid, *data):
1408 data = b"".join(data)
1409 crc = o32(_crc32(data, _crc32(cid)))
1410 fp.append((cid, data, crc))
1412 fp = collector()
1414 try:
1415 im.encoderinfo = params
1416 _save(im, fp, None, append)
1417 finally:
1418 del im.encoderinfo
1420 return fp.data
1423# --------------------------------------------------------------------
1424# Registry
1426Image.register_open(PngImageFile.format, PngImageFile, _accept)
1427Image.register_save(PngImageFile.format, _save)
1428Image.register_save_all(PngImageFile.format, _save_all)
1430Image.register_extensions(PngImageFile.format, [".png", ".apng"])
1432Image.register_mime(PngImageFile.format, "image/png")