Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
PngImagePlugin.py1564 linesDownload Raw Back to PIL
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)

Showing the first 1,200 of 1564 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai