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