Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/GifImagePlugin.py: 35%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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
28import itertools
29import math
30import os
31import subprocess
32from enum import IntEnum
33from functools import cached_property
34from typing import Any, NamedTuple, cast
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
50TYPE_CHECKING = False
51if TYPE_CHECKING:
52 from typing import IO, Literal
54 from . import _imaging
55 from ._typing import Buffer
58class LoadingStrategy(IntEnum):
59 """.. versionadded:: 9.1.0"""
61 RGB_AFTER_FIRST = 0
62 RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1
63 RGB_ALWAYS = 2
66#: .. versionadded:: 9.1.0
67LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST
69# --------------------------------------------------------------------
70# Identify/read GIF files
73def _accept(prefix: bytes) -> bool:
74 return prefix.startswith((b"GIF87a", b"GIF89a"))
77##
78# Image plugin for GIF images. This plugin supports both GIF87 and
79# GIF89 images.
82class GifImageFile(ImageFile.ImageFile):
83 format = "GIF"
84 format_description = "Compuserve GIF"
85 _close_exclusive_fp_after_loading = False
87 global_palette = None
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
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
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)
110 self.info["version"] = s[:6]
111 self._size = i16(s, 6), i16(s, 8)
112 flags = s[10]
113 bits = (flags & 7) + 1
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
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
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
141 @cached_property
142 def is_animated(self) -> bool:
143 if self._n_frames is not None:
144 return self._n_frames != 1
146 current = self.tell()
147 if current:
148 return True
150 try:
151 self._seek(1, False)
152 is_animated = True
153 except EOFError:
154 is_animated = False
156 self.seek(current)
157 return is_animated
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)
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
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()
192 if frame != self.__frame + 1:
193 msg = f"cannot seek to frame {frame}"
194 raise ValueError(msg)
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
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)
209 palette: ImagePalette.ImagePalette | Literal[False] | None = None
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
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
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""
251 # Read this comment block
252 while block:
253 comment += block
254 block = self.data()
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
275 elif s == b",":
276 #
277 # local image
278 #
279 s = self.fp.read(9)
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]
290 interlace = (flags & 64) != 0
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
300 # image data
301 bits = self.fp.read(1)[0]
302 self.__offset = self.fp.tell()
303 break
304 s = b""
306 if interlace is None:
307 msg = "image not found in GIF frame"
308 raise EOFError(msg)
310 self.__frame = frame
311 if not update_image:
312 return
314 self.tile = []
316 if self.dispose:
317 self.im.paste(self.dispose, self.dispose_extent)
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"
330 if palette:
331 self.palette = palette
332 elif self.global_palette:
333 from copy import copy
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)
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)
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
371 # only dispose the extent in this frame
372 x0, y0, x1, y1 = self.dispose_extent
373 dispose_size = (x1 - x0, y1 - y0)
375 Image._decompression_bomb_check(dispose_size)
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)
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
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 ]
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]
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)
457 self.im = expanded_im
458 self._mode = temp_mode
459 self._frame_palette = None
461 super().load_prepare()
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)
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")
495 assert self.dispose_extent is not None
496 frame_im = self._crop(frame_im, self.dispose_extent)
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)
505 def tell(self) -> int:
506 return self.__frame
509# --------------------------------------------------------------------
510# Write GIF files
513RAWMODE = {"1": "L", "L": "L", "P": "P"}
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.
521 It may return the original image, or it may return an image converted to
522 palette or 'L' mode.
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")
542_Palette = bytes | bytearray | list[int] | ImagePalette.ImagePalette
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.
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)
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
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
611 assert im.palette is not None
612 im.palette.palette = source_palette
613 return im
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)
627 for s in _get_global_header(im_out, im.encoderinfo):
628 fp.write(s)
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)
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 )
641 fp.write(b"\0") # end of image data
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)
657class _Frame(NamedTuple):
658 im: Image.Image
659 bbox: tuple[int, int, int, int] | None
660 encoderinfo: dict[str, Any]
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"))
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)
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
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))
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
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
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
793def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
794 _save(im, fp, filename, save_all=True)
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)
807 if not save_all or not _write_multiple_frames(im, fp, palette):
808 _write_single_frame(im, fp, palette)
810 fp.write(b";") # end of file
812 if hasattr(fp, "flush"):
813 fp.flush()
816def get_interlace(im: Image.Image) -> int:
817 interlace = im.encoderinfo.get("interlace", 1)
819 # workaround for @PIL153
820 if min(im.size) < 16:
821 interlace = 0
823 return interlace
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
834 if "duration" in im.encoderinfo:
835 duration = int(im.encoderinfo["duration"] / 10)
836 else:
837 duration = 0
839 disposal = int(im.encoderinfo.get("disposal", 0))
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
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 )
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
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
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()
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 )
906 # Allow ppmquant to receive SIGPIPE if ppmtogif exits
907 assert quant_proc.stdout is not None
908 quant_proc.stdout.close()
910 retcode = quant_proc.wait()
911 if retcode:
912 raise subprocess.CalledProcessError(retcode, quant_cmd)
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
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
929def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None:
930 """
931 Palette optimization is a potentially expensive operation.
933 This function determines if the palette should be optimized using
934 some heuristics, then returns the list of palette entries in use.
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.
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.
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)
959 if optimise or max(used_palette_colors) >= len(used_palette_colors):
960 return used_palette_colors
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
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
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
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)
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
1005def _get_palette_bytes(im: Image.Image) -> bytes:
1006 """
1007 Gets the palette for inclusion in the gif header
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""
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
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
1048def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]:
1049 """Return a list of strings representing a GIF header"""
1051 # Header Block
1052 # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
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"
1066 background = _get_background(im, info.get("background"))
1068 palette_bytes = _get_palette_bytes(im)
1069 color_table_size = _get_color_table_size(palette_bytes)
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
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
1105 comment_block += o8(0)
1106 header.append(comment_block)
1107 return header
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
1119 # local image header
1120 _write_local_header(fp, im_frame, offset, 0)
1122 ImageFile._save(
1123 im_frame,
1124 fp,
1125 [ImageFile._Tile("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])],
1126 )
1128 fp.write(b"\0") # end of image data
1129 finally:
1130 del im_frame.encoderinfo
1133# --------------------------------------------------------------------
1134# Legacy GIF utilities
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.
1143 Warning:: May modify image data.
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)
1150 """
1151 if info is None:
1152 info = {}
1154 used_palette_colors = _get_optimize(im, info)
1156 if "background" not in info and "background" in im.info:
1157 info["background"] = im.info["background"]
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)
1164 return header, used_palette_colors
1167def getdata(
1168 im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any
1169) -> list[bytes]:
1170 """
1171 Legacy Method
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.
1177 To specify duration, add the time in milliseconds,
1178 e.g. ``getdata(im_frame, duration=1000)``
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
1185 """
1186 from io import BytesIO
1188 class Collector(BytesIO):
1189 data = []
1191 def write(self, data: Buffer) -> int:
1192 self.data.append(data)
1193 return len(data)
1195 im.load() # make sure raster data is available
1197 fp = Collector()
1199 _write_frame_data(fp, im, offset, params)
1201 return fp.data
1204# --------------------------------------------------------------------
1205# Registry
1207Image.register_open(GifImageFile.format, GifImageFile, _accept)
1208Image.register_save(GifImageFile.format, _save)
1209Image.register_save_all(GifImageFile.format, _save_all)
1210Image.register_extension(GifImageFile.format, ".gif")
1211Image.register_mime(GifImageFile.format, "image/gif")
1213#
1214# Uncomment the following line if you wish to use NETPBM/PBMPLUS
1215# instead of the built-in "uncompressed" GIF encoder
1217# Image.register_save(GifImageFile.format, _save_netpbm)