CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
GifImagePlugin.py1218 linesDownload Raw Back to PIL
1#
2# The Python Imaging Library.
3# $Id$
4#
5# GIF file handling
6#
7# History:
8# 1995-09-01 fl   Created
9# 1996-12-14 fl   Added interlace support
10# 1996-12-30 fl   Added animation support
11# 1997-01-05 fl   Added write support, fixed local colour map bug
12# 1997-02-23 fl   Make sure to load raster data in getdata()
13# 1997-07-05 fl   Support external decoder (0.4)
14# 1998-07-09 fl   Handle all modes when saving (0.5)
15# 1998-07-15 fl   Renamed offset attribute to avoid name clash
16# 2001-04-16 fl   Added rewind support (seek to frame 0) (0.6)
17# 2001-04-17 fl   Added palette optimization (0.7)
18# 2002-06-06 fl   Added transparency support for save (0.8)
19# 2004-02-24 fl   Disable interlacing for small images
20#
21# Copyright (c) 1997-2004 by Secret Labs AB
22# Copyright (c) 1995-2004 by Fredrik Lundh
23#
24# See the README file for information on usage and redistribution.
25#
26from __future__ import annotations
27
28import itertools
29import math
30import os
31import subprocess
32from enum import IntEnum
33from functools import cached_property
34from typing import Any, NamedTuple, cast
35
36from . import (
37    Image,
38    ImageChops,
39    ImageFile,
40    ImageMath,
41    ImageOps,
42    ImagePalette,
43    ImageSequence,
44)
45from ._binary import i16le as i16
46from ._binary import o8
47from ._binary import o16le as o16
48from ._util import DeferredError
49
50TYPE_CHECKING = False
51if TYPE_CHECKING:
52    from typing import IO, Literal
53
54    from . import _imaging
55    from ._typing import Buffer
56
57
58class LoadingStrategy(IntEnum):
59    """.. versionadded:: 9.1.0"""
60
61    RGB_AFTER_FIRST = 0
62    RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1
63    RGB_ALWAYS = 2
64
65
66#: .. versionadded:: 9.1.0
67LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST
68
69# --------------------------------------------------------------------
70# Identify/read GIF files
71
72
73def _accept(prefix: bytes) -> bool:
74    return prefix.startswith((b"GIF87a", b"GIF89a"))
75
76
77##
78# Image plugin for GIF images.  This plugin supports both GIF87 and
79# GIF89 images.
80
81
82class GifImageFile(ImageFile.ImageFile):
83    format = "GIF"
84    format_description = "Compuserve GIF"
85    _close_exclusive_fp_after_loading = False
86
87    global_palette = None
88
89    def data(self) -> bytes | None:
90        assert self.fp is not None
91        s = self.fp.read(1)
92        if s and s[0]:
93            return self.fp.read(s[0])
94        return None
95
96    def _is_palette_needed(self, p: bytes) -> bool:
97        for i in range(0, len(p), 3):
98            if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
99                return True
100        return False
101
102    def _open(self) -> None:
103        # Screen
104        assert self.fp is not None
105        s = self.fp.read(13)
106        if not _accept(s):
107            msg = "not a GIF file"
108            raise SyntaxError(msg)
109
110        self.info["version"] = s[:6]
111        self._size = i16(s, 6), i16(s, 8)
112        flags = s[10]
113        bits = (flags & 7) + 1
114
115        if flags & 128:
116            # get global palette
117            self.info["background"] = s[11]
118            # check if palette contains colour indices
119            p = self.fp.read(3 << bits)
120            if self._is_palette_needed(p):
121                palette = ImagePalette.raw("RGB", p)
122                self.global_palette = self.palette = palette
123
124        self._fp = self.fp  # FIXME: hack
125        self.__rewind = self.fp.tell()
126        self._n_frames: int | None = None
127        self._seek(0)  # get ready to read first frame
128
129    @property
130    def n_frames(self) -> int:
131        if self._n_frames is None:
132            current = self.tell()
133            try:
134                while True:
135                    self._seek(self.tell() + 1, False)
136            except EOFError:
137                self._n_frames = self.tell() + 1
138            self.seek(current)
139        return self._n_frames
140
141    @cached_property
142    def is_animated(self) -> bool:
143        if self._n_frames is not None:
144            return self._n_frames != 1
145
146        current = self.tell()
147        if current:
148            return True
149
150        try:
151            self._seek(1, False)
152            is_animated = True
153        except EOFError:
154            is_animated = False
155
156        self.seek(current)
157        return is_animated
158
159    def seek(self, frame: int) -> None:
160        if not self._seek_check(frame):
161            return
162        if frame < self.__frame:
163            self._im = None
164            self._seek(0)
165
166        last_frame = self.__frame
167        for f in range(self.__frame + 1, frame + 1):
168            try:
169                self._seek(f)
170            except EOFError as e:
171                self.seek(last_frame)
172                msg = "no more images in GIF file"
173                raise EOFError(msg) from e
174
175    def _seek(self, frame: int, update_image: bool = True) -> None:
176        if isinstance(self._fp, DeferredError):
177            raise self._fp.ex
178        if frame == 0:
179            # rewind
180            self.__offset = 0
181            self.dispose: _imaging.ImagingCore | None = None
182            self.__frame = -1
183            self._fp.seek(self.__rewind)
184            self.disposal_method = 0
185            if "comment" in self.info:
186                del self.info["comment"]
187        else:
188            # ensure that the previous frame was loaded
189            if self.tile and update_image:
190                self.load()
191
192        if frame != self.__frame + 1:
193            msg = f"cannot seek to frame {frame}"
194            raise ValueError(msg)
195
196        self.fp = self._fp
197        if self.__offset:
198            # backup to last frame
199            self.fp.seek(self.__offset)
200            while self.data():
201                pass
202            self.__offset = 0
203
204        s = self.fp.read(1)
205        if not s or s == b";":
206            msg = "no more images in GIF file"
207            raise EOFError(msg)
208
209        palette: ImagePalette.ImagePalette | Literal[False] | None = None
210
211        info: dict[str, Any] = {}
212        frame_transparency = None
213        interlace = None
214        frame_dispose_extent = None
215        while True:
216            if not s:
217                s = self.fp.read(1)
218            if not s or s == b";":
219                break
220
221            elif s == b"!":
222                #
223                # extensions
224                #
225                s = self.fp.read(1)
226                block = self.data()
227                if s[0] == 249 and block is not None:
228                    #
229                    # graphic control extension
230                    #
231                    flags = block[0]
232                    if flags & 1:
233                        frame_transparency = block[3]
234                    info["duration"] = i16(block, 1) * 10
235
236                    # disposal method - find the value of bits 4 - 6
237                    dispose_bits = 0b00011100 & flags
238                    dispose_bits = dispose_bits >> 2
239                    if dispose_bits:
240                        # only set the dispose if it is not
241                        # unspecified. I'm not sure if this is
242                        # correct, but it seems to prevent the last
243                        # frame from looking odd for some animations
244                        self.disposal_method = dispose_bits
245                elif s[0] == 254:
246                    #
247                    # comment extension
248                    #
249                    comment = b""
250
251                    # Read this comment block
252                    while block:
253                        comment += block
254                        block = self.data()
255
256                    if "comment" in info:
257                        # If multiple comment blocks in frame, separate with \n
258                        info["comment"] += b"\n" + comment
259                    else:
260                        info["comment"] = comment
261                    s = b""
262                    continue
263                elif s[0] == 255 and frame == 0 and block is not None:
264                    #
265                    # application extension
266                    #
267                    info["extension"] = block, self.fp.tell()
268                    if block.startswith(b"NETSCAPE2.0"):
269                        block = self.data()
270                        if block and len(block) >= 3 and block[0] == 1:
271                            self.info["loop"] = i16(block, 1)
272                while self.data():
273                    pass
274
275            elif s == b",":
276                #
277                # local image
278                #
279                s = self.fp.read(9)
280
281                # extent
282                x0, y0 = i16(s, 0), i16(s, 2)
283                x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)
284                if (x1 > self.size[0] or y1 > self.size[1]) and update_image:
285                    self._size = max(x1, self.size[0]), max(y1, self.size[1])
286                    Image._decompression_bomb_check(self._size)
287                frame_dispose_extent = x0, y0, x1, y1
288                flags = s[8]
289
290                interlace = (flags & 64) != 0
291
292                if flags & 128:
293                    bits = (flags & 7) + 1
294                    p = self.fp.read(3 << bits)
295                    if self._is_palette_needed(p):
296                        palette = ImagePalette.raw("RGB", p)
297                    else:
298                        palette = False
299
300                # image data
301                bits = self.fp.read(1)[0]
302                self.__offset = self.fp.tell()
303                break
304            s = b""
305
306        if interlace is None:
307            msg = "image not found in GIF frame"
308            raise EOFError(msg)
309
310        self.__frame = frame
311        if not update_image:
312            return
313
314        self.tile = []
315
316        if self.dispose:
317            self.im.paste(self.dispose, self.dispose_extent)
318
319        self._frame_palette = palette if palette is not None else self.global_palette
320        self._frame_transparency = frame_transparency
321        if frame == 0:
322            if self._frame_palette:
323                if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
324                    self._mode = "RGBA" if frame_transparency is not None else "RGB"
325                else:
326                    self._mode = "P"
327            else:
328                self._mode = "L"
329
330            if palette:
331                self.palette = palette
332            elif self.global_palette:
333                from copy import copy
334
335                self.palette = copy(self.global_palette)
336            else:
337                self.palette = None
338        else:
339            if self.mode == "P":
340                if (
341                    LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
342                    or palette
343                ):
344                    if "transparency" in self.info:
345                        self.im.putpalettealpha(self.info["transparency"], 0)
346                        self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
347                        self._mode = "RGBA"
348                        del self.info["transparency"]
349                    else:
350                        self._mode = "RGB"
351                        self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
352
353        def _rgb(color: int) -> tuple[int, int, int]:
354            if self._frame_palette:
355                if color * 3 + 3 > len(self._frame_palette.palette):
356                    color = 0
357                return cast(
358                    tuple[int, int, int],
359                    tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]),
360                )
361            else:
362                return (color, color, color)
363
364        self.dispose = None
365        self.dispose_extent: tuple[int, int, int, int] | None = frame_dispose_extent
366        if self.dispose_extent and self.disposal_method >= 2:
367            try:
368                if self.disposal_method == 2:
369                    # replace with background colour
370
371                    # only dispose the extent in this frame
372                    x0, y0, x1, y1 = self.dispose_extent
373                    dispose_size = (x1 - x0, y1 - y0)
374
375                    Image._decompression_bomb_check(dispose_size)
376
377                    # by convention, attempt to use transparency first
378                    dispose_mode = "P"
379                    color = self.info.get("transparency", frame_transparency)
380                    if color is not None:
381                        if self.mode in ("RGB", "RGBA"):
382                            dispose_mode = "RGBA"
383                            color = _rgb(color) + (0,)
384                    else:
385                        color = self.info.get("background", 0)
386                        if self.mode in ("RGB", "RGBA"):
387                            dispose_mode = "RGB"
388                            color = _rgb(color)
389                    self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
390                else:
391                    # replace with previous contents
392                    if self._im is not None:
393                        # only dispose the extent in this frame
394                        self.dispose = self._crop(self.im, self.dispose_extent)
395                    elif frame_transparency is not None:
396                        x0, y0, x1, y1 = self.dispose_extent
397                        dispose_size = (x1 - x0, y1 - y0)
398
399                        Image._decompression_bomb_check(dispose_size)
400                        dispose_mode = "P"
401                        color = frame_transparency
402                        if self.mode in ("RGB", "RGBA"):
403                            dispose_mode = "RGBA"
404                            color = _rgb(frame_transparency) + (0,)
405                        self.dispose = Image.core.fill(
406                            dispose_mode, dispose_size, color
407                        )
408            except AttributeError:
409                pass
410
411        if interlace is not None:
412            transparency = -1
413            if frame_transparency is not None:
414                if frame == 0:
415                    if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS:
416                        self.info["transparency"] = frame_transparency
417                elif self.mode not in ("RGB", "RGBA"):
418                    transparency = frame_transparency
419            self.tile = [
420                ImageFile._Tile(
421                    "gif",
422                    (x0, y0, x1, y1),
423                    self.__offset,
424                    (bits, interlace, transparency),
425                )
426            ]
427
428        if info.get("comment"):
429            self.info["comment"] = info["comment"]
430        for k in ["duration", "extension"]:
431            if k in info:
432                self.info[k] = info[k]
433            elif k in self.info:
434                del self.info[k]
435
436    def load_prepare(self) -> None:
437        temp_mode = "P" if self._frame_palette else "L"
438        self._prev_im = None
439        if self.__frame == 0:
440            if self._frame_transparency is not None:
441                self.im = Image.core.fill(
442                    temp_mode, self.size, self._frame_transparency
443                )
444        elif self.mode in ("RGB", "RGBA"):
445            self._prev_im = self.im
446            if self._frame_palette:
447                self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
448                self.im.putpalette("RGB", *self._frame_palette.getdata())
449            else:
450                self._im = None
451        if not self._prev_im and self._im is not None and self.size != self.im.size:
452            expanded_im = Image.core.fill(self.im.mode, self.size)
453            if self._frame_palette:
454                expanded_im.putpalette("RGB", *self._frame_palette.getdata())
455            expanded_im.paste(self.im, (0, 0) + self.im.size)
456
457            self.im = expanded_im
458        self._mode = temp_mode
459        self._frame_palette = None
460
461        super().load_prepare()
462
463    def load_end(self) -> None:
464        if self.__frame == 0:
465            if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
466                if self._frame_transparency is not None:
467                    self.im.putpalettealpha(self._frame_transparency, 0)
468                    self._mode = "RGBA"
469                else:
470                    self._mode = "RGB"
471                self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)
472            return
473        if not self._prev_im:
474            return
475        if self.size != self._prev_im.size:
476            if self._frame_transparency is not None:
477                expanded_im = Image.core.fill("RGBA", self.size)
478            else:
479                expanded_im = Image.core.fill("P", self.size)
480                expanded_im.putpalette("RGB", "RGB", self.im.getpalette())
481                expanded_im = expanded_im.convert("RGB")
482            expanded_im.paste(self._prev_im, (0, 0) + self._prev_im.size)
483
484            self._prev_im = expanded_im
485            assert self._prev_im is not None
486        if self._frame_transparency is not None:
487            if self.mode == "L":
488                frame_im = self.im.convert_transparent("LA", self._frame_transparency)
489            else:
490                self.im.putpalettealpha(self._frame_transparency, 0)
491                frame_im = self.im.convert("RGBA")
492        else:
493            frame_im = self.im.convert("RGB")
494
495        assert self.dispose_extent is not None
496        frame_im = self._crop(frame_im, self.dispose_extent)
497
498        self.im = self._prev_im
499        self._mode = self.im.mode
500        if frame_im.mode in ("LA", "RGBA"):
501            self.im.paste(frame_im, self.dispose_extent, frame_im)
502        else:
503            self.im.paste(frame_im, self.dispose_extent)
504
505    def tell(self) -> int:
506        return self.__frame
507
508
509# --------------------------------------------------------------------
510# Write GIF files
511
512
513RAWMODE = {"1": "L", "L": "L", "P": "P"}
514
515
516def _normalize_mode(im: Image.Image) -> Image.Image:
517    """
518    Takes an image (or frame), returns an image in a mode that is appropriate
519    for saving in a Gif.
520
521    It may return the original image, or it may return an image converted to
522    palette or 'L' mode.
523
524    :param im: Image object
525    :returns: Image object
526    """
527    if im.mode in RAWMODE:
528        im.load()
529        return im
530    if Image.getmodebase(im.mode) == "RGB":
531        im = im.convert("P", palette=Image.Palette.ADAPTIVE)
532        assert im.palette is not None
533        if im.palette.mode == "RGBA":
534            for rgba in im.palette.colors:
535                if rgba[3] == 0:
536                    im.info["transparency"] = im.palette.colors[rgba]
537                    break
538        return im
539    return im.convert("L")
540
541
542_Palette = bytes | bytearray | list[int] | ImagePalette.ImagePalette
543
544
545def _normalize_palette(
546    im: Image.Image, palette: _Palette | None, info: dict[str, Any]
547) -> Image.Image:
548    """
549    Normalizes the palette for image.
550      - Sets the palette to the incoming palette, if provided.
551      - Ensures that there's a palette for L mode images
552      - Optimizes the palette if necessary/desired.
553
554    :param im: Image object
555    :param palette: bytes object containing the source palette, or ....
556    :param info: encoderinfo
557    :returns: Image object
558    """
559    source_palette = None
560    if palette:
561        # a bytes palette
562        if isinstance(palette, (bytes, bytearray, list)):
563            source_palette = bytearray(palette[:768])
564        if isinstance(palette, ImagePalette.ImagePalette):
565            source_palette = bytearray(palette.palette)
566
567    if im.mode == "P":
568        if not source_palette:
569            im_palette = im.getpalette(None)
570            assert im_palette is not None
571            source_palette = bytearray(im_palette)
572    else:  # L-mode
573        if not source_palette:
574            source_palette = bytearray(i // 3 for i in range(768))
575        im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
576    assert source_palette is not None
577
578    if palette:
579        used_palette_colors: list[int | None] = []
580        assert im.palette is not None
581        for i in range(0, len(source_palette), 3):
582            source_color = tuple(source_palette[i : i + 3])
583            index = im.palette.colors.get(source_color)
584            if index in used_palette_colors:
585                index = None
586            used_palette_colors.append(index)
587        for i, index in enumerate(used_palette_colors):
588            if index is None:
589                for j in range(len(used_palette_colors)):
590                    if j not in used_palette_colors:
591                        used_palette_colors[i] = j
592                        break
593        dest_map: list[int] = []
594        for index in used_palette_colors:
595            assert index is not None
596            dest_map.append(index)
597        im = im.remap_palette(dest_map)
598    else:
599        optimized_palette_colors = _get_optimize(im, info)
600        if optimized_palette_colors is not None:
601            im = im.remap_palette(optimized_palette_colors, source_palette)
602            if "transparency" in info:
603                try:
604                    info["transparency"] = optimized_palette_colors.index(
605                        info["transparency"]
606                    )
607                except ValueError:
608                    del info["transparency"]
609            return im
610
611    assert im.palette is not None
612    im.palette.palette = source_palette
613    return im
614
615
616def _write_single_frame(
617    im: Image.Image,
618    fp: IO[bytes],
619    palette: _Palette | None,
620) -> None:
621    im_out = _normalize_mode(im)
622    for k, v in im_out.info.items():
623        if isinstance(k, str):
624            im.encoderinfo.setdefault(k, v)
625    im_out = _normalize_palette(im_out, palette, im.encoderinfo)
626
627    for s in _get_global_header(im_out, im.encoderinfo):
628        fp.write(s)
629
630    # local image header
631    flags = 0
632    if get_interlace(im):
633        flags = flags | 64
634    _write_local_header(fp, im, (0, 0), flags)
635
636    im_out.encoderconfig = (8, get_interlace(im))
637    ImageFile._save(
638        im_out, fp, [ImageFile._Tile("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]
639    )
640
641    fp.write(b"\0")  # end of image data
642
643
644def _getbbox(
645    base_im: Image.Image, im_frame: Image.Image
646) -> tuple[Image.Image, tuple[int, int, int, int] | None]:
647    palette_bytes = [
648        bytes(im.palette.palette) if im.palette else b"" for im in (base_im, im_frame)
649    ]
650    if palette_bytes[0] != palette_bytes[1]:
651        im_frame = im_frame.convert("RGBA")
652        base_im = base_im.convert("RGBA")
653    delta = ImageChops.subtract_modulo(im_frame, base_im)
654    return delta, delta.getbbox(alpha_only=False)
655
656
657class _Frame(NamedTuple):
658    im: Image.Image
659    bbox: tuple[int, int, int, int] | None
660    encoderinfo: dict[str, Any]
661
662
663def _write_multiple_frames(
664    im: Image.Image, fp: IO[bytes], palette: _Palette | None
665) -> bool:
666    duration = im.encoderinfo.get("duration")
667    disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
668
669    im_frames: list[_Frame] = []
670    previous_im: Image.Image | None = None
671    frame_count = 0
672    background_im = None
673    for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
674        for im_frame in ImageSequence.Iterator(imSequence):
675            # a copy is required here since seek can still mutate the image
676            im_frame = _normalize_mode(im_frame.copy())
677            if frame_count == 0:
678                for k, v in im_frame.info.items():
679                    if k == "transparency":
680                        continue
681                    if isinstance(k, str):
682                        im.encoderinfo.setdefault(k, v)
683
684            encoderinfo = im.encoderinfo.copy()
685            if "transparency" in im_frame.info:
686                encoderinfo.setdefault("transparency", im_frame.info["transparency"])
687            im_frame = _normalize_palette(im_frame, palette, encoderinfo)
688            if isinstance(duration, (list, tuple)):
689                encoderinfo["duration"] = duration[frame_count]
690            elif duration is None and "duration" in im_frame.info:
691                encoderinfo["duration"] = im_frame.info["duration"]
692            if isinstance(disposal, (list, tuple)):
693                encoderinfo["disposal"] = disposal[frame_count]
694            frame_count += 1
695
696            diff_frame = None
697            if im_frames and previous_im:
698                # delta frame
699                delta, bbox = _getbbox(previous_im, im_frame)
700                if not bbox:
701                    # This frame is identical to the previous frame
702                    if encoderinfo.get("duration"):
703                        im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"]
704                    continue
705                if im_frames[-1].encoderinfo.get("disposal") == 2:
706                    # To appear correctly in viewers using a convention,
707                    # only consider transparency, and not background color
708                    color = im.encoderinfo.get(
709                        "transparency", im.info.get("transparency")
710                    )
711                    if color is not None:
712                        if background_im is None:
713                            background = _get_background(im_frame, color)
714                            background_im = Image.new("P", im_frame.size, background)
715                            first_palette = im_frames[0].im.palette
716                            assert first_palette is not None
717                            background_im.putpalette(first_palette, first_palette.mode)
718                        bbox = _getbbox(background_im, im_frame)[1]
719                    else:
720                        bbox = (0, 0) + im_frame.size
721                elif encoderinfo.get("optimize") and im_frame.mode != "1":
722                    if "transparency" not in encoderinfo:
723                        assert im_frame.palette is not None
724                        try:
725                            encoderinfo["transparency"] = (
726                                im_frame.palette._new_color_index(im_frame)
727                            )
728                        except ValueError:
729                            pass
730                    if "transparency" in encoderinfo:
731                        # When the delta is zero, fill the image with transparency
732                        diff_frame = im_frame.copy()
733                        fill = Image.new("P", delta.size, encoderinfo["transparency"])
734                        if delta.mode == "RGBA":
735                            r, g, b, a = delta.split()
736                            mask = ImageMath.lambda_eval(
737                                lambda args: args["convert"](
738                                    args["max"](
739                                        args["max"](
740                                            args["max"](args["r"], args["g"]), args["b"]
741                                        ),
742                                        args["a"],
743                                    )
744                                    * 255,
745                                    "1",
746                                ),
747                                r=r,
748                                g=g,
749                                b=b,
750                                a=a,
751                            )
752                        else:
753                            if delta.mode == "P":
754                                # Convert to L without considering palette
755                                delta_l = Image.new("L", delta.size)
756                                delta_l.putdata(delta.get_flattened_data())
757                                delta = delta_l
758                            mask = ImageMath.lambda_eval(
759                                lambda args: args["convert"](args["im"] * 255, "1"),
760                                im=delta,
761                            )
762                        diff_frame.paste(fill, mask=ImageOps.invert(mask))
763            else:
764                bbox = None
765            previous_im = im_frame
766            im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo))
767
768    if len(im_frames) == 1:
769        if "duration" in im.encoderinfo:
770            # Since multiple frames will not be written, use the combined duration
771            im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"]
772        return False
773
774    for frame_data in im_frames:
775        im_frame = frame_data.im
776        if not frame_data.bbox:
777            # global header
778            for s in _get_global_header(im_frame, frame_data.encoderinfo):
779                fp.write(s)
780            offset = (0, 0)
781        else:
782            # compress difference
783            if not palette:
784                frame_data.encoderinfo["include_color_table"] = True
785
786            if frame_data.bbox != (0, 0) + im_frame.size:
787                im_frame = im_frame.crop(frame_data.bbox)
788            offset = frame_data.bbox[:2]
789        _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo)
790    return True
791
792
793def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
794    _save(im, fp, filename, save_all=True)
795
796
797def _save(
798    im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
799) -> None:
800    # header
801    if "palette" in im.encoderinfo or "palette" in im.info:
802        palette = im.encoderinfo.get("palette", im.info.get("palette"))
803    else:
804        palette = None
805        im.encoderinfo.setdefault("optimize", True)
806
807    if not save_all or not _write_multiple_frames(im, fp, palette):
808        _write_single_frame(im, fp, palette)
809
810    fp.write(b";")  # end of file
811
812    if hasattr(fp, "flush"):
813        fp.flush()
814
815
816def get_interlace(im: Image.Image) -> int:
817    interlace = im.encoderinfo.get("interlace", 1)
818
819    # workaround for @PIL153
820    if min(im.size) < 16:
821        interlace = 0
822
823    return interlace
824
825
826def _write_local_header(
827    fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int
828) -> None:
829    try:
830        transparency = im.encoderinfo["transparency"]
831    except KeyError:
832        transparency = None
833
834    if "duration" in im.encoderinfo:
835        duration = int(im.encoderinfo["duration"] / 10)
836    else:
837        duration = 0
838
839    disposal = int(im.encoderinfo.get("disposal", 0))
840
841    if transparency is not None or duration != 0 or disposal:
842        packed_flag = 1 if transparency is not None else 0
843        packed_flag |= disposal << 2
844
845        fp.write(
846            b"!"
847            + o8(249)  # extension intro
848            + o8(4)  # length
849            + o8(packed_flag)  # packed fields
850            + o16(duration)  # duration
851            + o8(transparency or 0)  # transparency index
852            + o8(0)
853        )
854
855    include_color_table = im.encoderinfo.get("include_color_table")
856    if include_color_table:
857        palette_bytes = _get_palette_bytes(im)
858        color_table_size = _get_color_table_size(palette_bytes)
859        if color_table_size:
860            flags = flags | 128  # local color table flag
861            flags = flags | color_table_size
862
863    fp.write(
864        b","
865        + o16(offset[0])  # offset
866        + o16(offset[1])
867        + o16(im.size[0])  # size
868        + o16(im.size[1])
869        + o8(flags)  # flags
870    )
871    if include_color_table and color_table_size:
872        fp.write(_get_header_palette(palette_bytes))
873    fp.write(o8(8))  # bits
874
875
876def _save_netpbm(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
877    # Unused by default.
878    # To use, uncomment the register_save call at the end of the file.
879    #
880    # If you need real GIF compression and/or RGB quantization, you
881    # can use the external NETPBM/PBMPLUS utilities.  See comments
882    # below for information on how to enable this.
883    tempfile = im._dump()
884
885    try:
886        with open(filename, "wb") as f:
887            if im.mode != "RGB":
888                subprocess.check_call(
889                    ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL
890                )
891            else:
892                # Pipe ppmquant output into ppmtogif
893                # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename)
894                quant_cmd = ["ppmquant", "256", tempfile]
895                togif_cmd = ["ppmtogif"]
896                quant_proc = subprocess.Popen(
897                    quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
898                )
899                togif_proc = subprocess.Popen(
900                    togif_cmd,
901                    stdin=quant_proc.stdout,
902                    stdout=f,
903                    stderr=subprocess.DEVNULL,
904                )
905
906                # Allow ppmquant to receive SIGPIPE if ppmtogif exits
907                assert quant_proc.stdout is not None
908                quant_proc.stdout.close()
909
910                retcode = quant_proc.wait()
911                if retcode:
912                    raise subprocess.CalledProcessError(retcode, quant_cmd)
913
914                retcode = togif_proc.wait()
915                if retcode:
916                    raise subprocess.CalledProcessError(retcode, togif_cmd)
917    finally:
918        try:
919            os.unlink(tempfile)
920        except OSError:
921            pass
922
923
924# Force optimization so that we can test performance against
925# cases where it took lots of memory and time previously.
926_FORCE_OPTIMIZE = False
927
928
929def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None:
930    """
931    Palette optimization is a potentially expensive operation.
932
933    This function determines if the palette should be optimized using
934    some heuristics, then returns the list of palette entries in use.
935
936    :param im: Image object
937    :param info: encoderinfo
938    :returns: list of indexes of palette entries in use, or None
939    """
940    if im.mode in ("P", "L") and info and info.get("optimize"):
941        # Potentially expensive operation.
942
943        # The palette saves 3 bytes per color not used, but palette
944        # lengths are restricted to 3*(2**N) bytes. Max saving would
945        # be 768 -> 6 bytes if we went all the way down to 2 colors.
946        # * If we're over 128 colors, we can't save any space.
947        # * If there aren't any holes, it's not worth collapsing.
948        # * If we have a 'large' image, the palette is in the noise.
949
950        # create the new palette if not every color is used
951        optimise = _FORCE_OPTIMIZE or im.mode == "L"
952        if optimise or im.width * im.height < 512 * 512:
953            # check which colors are used
954            used_palette_colors = []
955            for i, count in enumerate(im.histogram()):
956                if count:
957                    used_palette_colors.append(i)
958
959            if optimise or max(used_palette_colors) >= len(used_palette_colors):
960                return used_palette_colors
961
962            assert im.palette is not None
963            num_palette_colors = len(im.palette.palette) // Image.getmodebands(
964                im.palette.mode
965            )
966            current_palette_size = 1 << (num_palette_colors - 1).bit_length()
967            if (
968                # check that the palette would become smaller when saved
969                len(used_palette_colors) <= current_palette_size // 2
970                # check that the palette is not already the smallest possible size
971                and current_palette_size > 2
972            ):
973                return used_palette_colors
974    return None
975
976
977def _get_color_table_size(palette_bytes: bytes) -> int:
978    # calculate the palette size for the header
979    if not palette_bytes:
980        return 0
981    elif len(palette_bytes) < 9:
982        return 1
983    else:
984        return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1
985
986
987def _get_header_palette(palette_bytes: bytes) -> bytes:
988    """
989    Returns the palette, null padded to the next power of 2 (*3) bytes
990    suitable for direct inclusion in the GIF header
991
992    :param palette_bytes: Unpadded palette bytes, in RGBRGB form
993    :returns: Null padded palette
994    """
995    color_table_size = _get_color_table_size(palette_bytes)
996
997    # add the missing amount of bytes
998    # the palette has to be 2<<n in size
999    actual_target_size_diff = (2 << color_table_size) - len(palette_bytes) // 3
1000    if actual_target_size_diff > 0:
1001        palette_bytes += o8(0) * 3 * actual_target_size_diff
1002    return palette_bytes
1003
1004
1005def _get_palette_bytes(im: Image.Image) -> bytes:
1006    """
1007    Gets the palette for inclusion in the gif header
1008
1009    :param im: Image object
1010    :returns: Bytes, len<=768 suitable for inclusion in gif header
1011    """
1012    if not im.palette:
1013        return b""
1014
1015    palette = bytes(im.palette.palette)
1016    if im.palette.mode == "RGBA":
1017        palette = b"".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3))
1018    return palette
1019
1020
1021def _get_background(
1022    im: Image.Image,
1023    info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None,
1024) -> int:
1025    background = 0
1026    if info_background:
1027        if isinstance(info_background, tuple):
1028            # WebPImagePlugin stores an RGBA value in info["background"]
1029            # So it must be converted to the same format as GifImagePlugin's
1030            # info["background"] - a global color table index
1031            assert im.palette is not None
1032            try:
1033                background = im.palette.getcolor(info_background, im)
1034            except ValueError as e:
1035                if str(e) not in (
1036                    # If all 256 colors are in use,
1037                    # then there is no need for the background color
1038                    "cannot allocate more than 256 colors",
1039                    # Ignore non-opaque WebP background
1040                    "cannot add non-opaque RGBA color to RGB palette",
1041                ):
1042                    raise
1043        else:
1044            background = info_background
1045    return background
1046
1047
1048def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]:
1049    """Return a list of strings representing a GIF header"""
1050
1051    # Header Block
1052    # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
1053
1054    version = b"87a"
1055    if im.info.get("version") == b"89a" or (
1056        info
1057        and (
1058            "transparency" in info
1059            or info.get("loop") is not None
1060            or info.get("duration")
1061            or info.get("comment")
1062        )
1063    ):
1064        version = b"89a"
1065
1066    background = _get_background(im, info.get("background"))
1067
1068    palette_bytes = _get_palette_bytes(im)
1069    color_table_size = _get_color_table_size(palette_bytes)
1070
1071    header = [
1072        b"GIF"  # signature
1073        + version  # version
1074        + o16(im.size[0])  # canvas width
1075        + o16(im.size[1]),  # canvas height
1076        # Logical Screen Descriptor
1077        # size of global color table + global color table flag
1078        o8(color_table_size + 128),  # packed fields
1079        # background + reserved/aspect
1080        o8(background) + o8(0),
1081        # Global Color Table
1082        _get_header_palette(palette_bytes),
1083    ]
1084    if info.get("loop") is not None:
1085        header.append(
1086            b"!"
1087            + o8(255)  # extension intro
1088            + o8(11)
1089            + b"NETSCAPE2.0"
1090            + o8(3)
1091            + o8(1)
1092            + o16(info["loop"])  # number of loops
1093            + o8(0)
1094        )
1095    if info.get("comment"):
1096        comment_block = b"!" + o8(254)  # extension intro
1097
1098        comment = info["comment"]
1099        if isinstance(comment, str):
1100            comment = comment.encode()
1101        for i in range(0, len(comment), 255):
1102            subblock = comment[i : i + 255]
1103            comment_block += o8(len(subblock)) + subblock
1104
1105        comment_block += o8(0)
1106        header.append(comment_block)
1107    return header
1108
1109
1110def _write_frame_data(
1111    fp: IO[bytes],
1112    im_frame: Image.Image,
1113    offset: tuple[int, int],
1114    params: dict[str, Any],
1115) -> None:
1116    try:
1117        im_frame.encoderinfo = params
1118
1119        # local image header
1120        _write_local_header(fp, im_frame, offset, 0)
1121
1122        ImageFile._save(
1123            im_frame,
1124            fp,
1125            [ImageFile._Tile("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])],
1126        )
1127
1128        fp.write(b"\0")  # end of image data
1129    finally:
1130        del im_frame.encoderinfo
1131
1132
1133# --------------------------------------------------------------------
1134# Legacy GIF utilities
1135
1136
1137def getheader(
1138    im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None
1139) -> tuple[list[bytes], list[int] | None]:
1140    """
1141    Legacy Method to get Gif data from image.
1142
1143    Warning:: May modify image data.
1144
1145    :param im: Image object
1146    :param palette: bytes object containing the source palette, or ....
1147    :param info: encoderinfo
1148    :returns: tuple of(list of header items, optimized palette)
1149
1150    """
1151    if info is None:
1152        info = {}
1153
1154    used_palette_colors = _get_optimize(im, info)
1155
1156    if "background" not in info and "background" in im.info:
1157        info["background"] = im.info["background"]
1158
1159    im_mod = _normalize_palette(im, palette, info)
1160    im.palette = im_mod.palette
1161    im.im = im_mod.im
1162    header = _get_global_header(im, info)
1163
1164    return header, used_palette_colors
1165
1166
1167def getdata(
1168    im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any
1169) -> list[bytes]:
1170    """
1171    Legacy Method
1172
1173    Return a list of strings representing this image.
1174    The first string is a local image header, the rest contains
1175    encoded image data.
1176
1177    To specify duration, add the time in milliseconds,
1178    e.g. ``getdata(im_frame, duration=1000)``
1179
1180    :param im: Image object
1181    :param offset: Tuple of (x, y) pixels. Defaults to (0, 0)
1182    :param \\**params: e.g. duration or other encoder info parameters
1183    :returns: List of bytes containing GIF encoded frame data
1184
1185    """
1186    from io import BytesIO
1187
1188    class Collector(BytesIO):
1189        data = []
1190
1191        def write(self, data: Buffer) -> int:
1192            self.data.append(data)
1193            return len(data)
1194
1195    im.load()  # make sure raster data is available
1196
1197    fp = Collector()
1198
1199    _write_frame_data(fp, im, offset, params)
1200

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

Aluode/PerceptionLabPortable · CoolFace