Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/GifImagePlugin.py: 36%
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
28__lazy_modules__ = {
29 "PIL._binary",
30 "PIL._util",
31 "itertools",
32 "math",
33 "subprocess",
34}
36import itertools
37import math
38from enum import IntEnum
39from functools import cached_property
40from typing import NamedTuple, cast
42from . import (
43 Image,
44 ImageChops,
45 ImageFile,
46 ImagePalette,
47 ImageSequence,
48)
49from ._binary import i16le as i16
50from ._binary import o8
51from ._binary import o16le as o16
52from ._util import DeferredError
54TYPE_CHECKING = False
55if TYPE_CHECKING:
56 from typing import IO, Any, Literal
58 from . import _imaging
59 from ._typing import Buffer
62class LoadingStrategy(IntEnum):
63 """.. versionadded:: 9.1.0"""
65 RGB_AFTER_FIRST = 0
66 RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1
67 RGB_ALWAYS = 2
70#: .. versionadded:: 9.1.0
71LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST
73# --------------------------------------------------------------------
74# Identify/read GIF files
77def _accept(prefix: bytes) -> bool:
78 return prefix.startswith((b"GIF87a", b"GIF89a"))
81##
82# Image plugin for GIF images. This plugin supports both GIF87 and
83# GIF89 images.
86class GifImageFile(ImageFile.ImageFile):
87 format = "GIF"
88 format_description = "Compuserve GIF"
89 _close_exclusive_fp_after_loading = False
91 global_palette = None
93 def data(self) -> bytes | None:
94 assert self.fp is not None
95 s = self.fp.read(1)
96 if s and s[0]:
97 return self.fp.read(s[0])
98 return None
100 def _is_palette_needed(self, p: bytes) -> bool:
101 for i in range(0, len(p), 3):
102 if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
103 return True
104 return False
106 def _open(self) -> None:
107 # Screen
108 assert self.fp is not None
109 s = self.fp.read(13)
110 if not _accept(s):
111 msg = "not a GIF file"
112 raise SyntaxError(msg)
114 self.info["version"] = s[:6]
115 self._size = i16(s, 6), i16(s, 8)
116 flags = s[10]
117 bits = (flags & 7) + 1
119 if flags & 128:
120 # get global palette
121 self.info["background"] = s[11]
122 # check if palette contains colour indices
123 p = self.fp.read(3 << bits)
124 if self._is_palette_needed(p):
125 palette = ImagePalette.raw("RGB", p)
126 self.global_palette = palette
128 self._fp = self.fp # FIXME: hack
129 self.__rewind = self.fp.tell()
130 self._n_frames: int | None = None
131 self._seek(0) # get ready to read first frame
133 @property
134 def n_frames(self) -> int:
135 if self._n_frames is None:
136 current = self.tell()
137 try:
138 while True:
139 self._seek(self.tell() + 1, False)
140 except EOFError:
141 self._n_frames = self.tell() + 1
142 self.seek(current)
143 return self._n_frames
145 @cached_property
146 def is_animated(self) -> bool:
147 if self._n_frames is not None:
148 return self._n_frames != 1
150 current = self.tell()
151 if current:
152 return True
154 try:
155 self._seek(1, False)
156 is_animated = True
157 except EOFError:
158 is_animated = False
160 self.seek(current)
161 return is_animated
163 def seek(self, frame: int) -> None:
164 if not self._seek_check(frame):
165 return
166 if frame < self.__frame:
167 self._im = None
168 self._seek(0)
170 last_frame = self.__frame
171 try:
172 for f in range(self.__frame + 1, frame + 1):
173 self._seek(f)
174 except EOFError as e:
175 self.seek(last_frame)
176 msg = "no more images in GIF file"
177 raise EOFError(msg) from e
179 def _seek(self, frame: int, update_image: bool = True) -> None:
180 if isinstance(self._fp, DeferredError):
181 raise self._fp.ex
182 if frame == 0:
183 # rewind
184 self.__offset = 0
185 self.dispose: _imaging.ImagingCore | None = None
186 self.__frame = -1
187 self._fp.seek(self.__rewind)
188 self.disposal_method = 0
189 if "comment" in self.info:
190 del self.info["comment"]
191 else:
192 # ensure that the previous frame was loaded
193 if self.tile and update_image:
194 self.load()
196 if frame != self.__frame + 1:
197 msg = f"cannot seek to frame {frame}"
198 raise ValueError(msg)
200 self.fp = self._fp
201 if self.__offset:
202 # backup to last frame
203 self.fp.seek(self.__offset)
204 while self.data():
205 pass
206 self.__offset = 0
208 s = self.fp.read(1)
209 if not s or s == b";":
210 msg = "no more images in GIF file"
211 raise EOFError(msg)
213 palette: ImagePalette.ImagePalette | Literal[False] | None = None
215 info: dict[str, Any] = {}
216 comment_blocks: list[bytes] = []
217 frame_transparency = None
218 interlace = None
219 frame_dispose_extent = None
220 while True:
221 if not s:
222 s = self.fp.read(1)
223 if not s or s == b";":
224 break
226 elif s == b"!":
227 #
228 # extensions
229 #
230 s = self.fp.read(1)
231 block = self.data()
232 if s[0] == 249 and block is not None:
233 #
234 # graphic control extension
235 #
236 flags = block[0]
237 if flags & 1:
238 frame_transparency = block[3]
239 info["duration"] = i16(block, 1) * 10
241 # disposal method - find the value of bits 4 - 6
242 dispose_bits = 0b00011100 & flags
243 dispose_bits = dispose_bits >> 2
244 if dispose_bits:
245 # only set the dispose if it is not
246 # unspecified. I'm not sure if this is
247 # correct, but it seems to prevent the last
248 # frame from looking odd for some animations
249 self.disposal_method = dispose_bits
250 elif s[0] == 254:
251 #
252 # comment extension
253 #
254 if block and comment_blocks:
255 # If multiple comment blocks in frame, separate with \n
256 comment_blocks.append(b"\n")
257 while block:
258 comment_blocks.append(block)
259 block = self.data()
260 s = b""
261 continue
262 elif s[0] == 255 and frame == 0 and block is not None:
263 #
264 # application extension
265 #
266 info["extension"] = block, self.fp.tell()
267 if block.startswith(b"NETSCAPE2.0"):
268 block = self.data()
269 if block and len(block) >= 3 and block[0] == 1:
270 self.info["loop"] = i16(block, 1)
271 while self.data():
272 pass
274 elif s == b",":
275 #
276 # local image
277 #
278 s = self.fp.read(9)
280 # extent
281 x0, y0 = i16(s, 0), i16(s, 2)
282 x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)
283 if (x1 > self.size[0] or y1 > self.size[1]) and update_image:
284 size = max(x1, self.size[0]), max(y1, self.size[1])
285 Image._decompression_bomb_check(size)
286 self._size = 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 self.palette = None
323 if self._frame_palette:
324 if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
325 self._mode = "RGBA" if frame_transparency is not None else "RGB"
326 else:
327 self._mode = "P"
328 if palette:
329 self.palette = palette
330 elif self.global_palette:
331 from copy import copy
333 self.palette = copy(self.global_palette)
334 else:
335 self._mode = "L"
336 elif self.mode == "P" and (
337 LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
338 or palette
339 ):
340 if "transparency" in self.info:
341 self.im.putpalettealpha(self.info["transparency"], 0)
342 self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
343 self._mode = "RGBA"
344 del self.info["transparency"]
345 else:
346 self._mode = "RGB"
347 self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
348 self.palette = None
350 def _rgb(color: int) -> tuple[int, int, int]:
351 if self._frame_palette:
352 if color * 3 + 3 > len(self._frame_palette.palette):
353 color = 0
354 return cast(
355 "tuple[int, int, int]",
356 tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]),
357 )
358 else:
359 return (color, color, color)
361 self.dispose = None
362 self.dispose_extent: tuple[int, int, int, int] | None = frame_dispose_extent
363 if self.dispose_extent and self.disposal_method >= 2:
364 try:
365 if self.disposal_method == 2:
366 # replace with background colour
368 # only dispose the extent in this frame
369 x0, y0, x1, y1 = self.dispose_extent
370 dispose_size = (x1 - x0, y1 - y0)
372 Image._decompression_bomb_check(dispose_size)
374 # by convention, attempt to use transparency first
375 dispose_mode = "P"
376 color = self.info.get("transparency", frame_transparency)
377 if color is not None:
378 if self.mode in {"RGB", "RGBA"}:
379 dispose_mode = "RGBA"
380 color = _rgb(color) + (0,)
381 else:
382 color = self.info.get("background", 0)
383 if self.mode in {"RGB", "RGBA"}:
384 dispose_mode = "RGB"
385 color = _rgb(color)
386 self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
387 else:
388 # replace with previous contents
389 if self._im is not None:
390 # only dispose the extent in this frame
391 self.dispose = self._crop(self.im, self.dispose_extent)
392 elif frame_transparency is not None:
393 x0, y0, x1, y1 = self.dispose_extent
394 dispose_size = (x1 - x0, y1 - y0)
396 Image._decompression_bomb_check(dispose_size)
397 dispose_mode = "P"
398 color = frame_transparency
399 if self.mode in {"RGB", "RGBA"}:
400 dispose_mode = "RGBA"
401 color = _rgb(frame_transparency) + (0,)
402 self.dispose = Image.core.fill(
403 dispose_mode, dispose_size, color
404 )
405 except AttributeError:
406 pass
408 if interlace is not None:
409 transparency = -1
410 if frame_transparency is not None:
411 if frame == 0:
412 if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS:
413 self.info["transparency"] = frame_transparency
414 elif self.mode not in {"RGB", "RGBA"}:
415 transparency = frame_transparency
416 self.tile = [
417 ImageFile._Tile(
418 "gif",
419 (x0, y0, x1, y1),
420 self.__offset,
421 (bits, interlace, transparency),
422 )
423 ]
425 if comment_blocks:
426 self.info["comment"] = b"".join(comment_blocks)
427 for k in ["duration", "extension"]:
428 if k in info:
429 self.info[k] = info[k]
430 elif k in self.info:
431 del self.info[k]
433 def load_prepare(self) -> None:
434 temp_mode = "P" if self._frame_palette else "L"
435 self._prev_im = None
436 if self.__frame == 0:
437 self.im = Image.core.fill(
438 temp_mode, self.size, self._frame_transparency or 0
439 )
440 if temp_mode == "P":
441 assert self._frame_palette
442 self.im.putpalette("RGB", *self._frame_palette.getdata())
443 elif self.mode in {"RGB", "RGBA"}:
444 self._prev_im = self.im
445 if self._frame_palette:
446 self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
447 self.im.putpalette("RGB", *self._frame_palette.getdata())
448 else:
449 self._im = None
450 if not self._prev_im and self._im is not None and self.size != self.im.size:
451 expanded_im = Image.core.fill(self.im.mode, self.size)
452 if self._frame_palette:
453 expanded_im.putpalette("RGB", *self._frame_palette.getdata())
454 expanded_im.paste(self.im, (0, 0, *self.im.size))
456 self.im = expanded_im
457 self._mode = temp_mode
458 self._frame_palette = None
460 super().load_prepare()
462 def load_end(self) -> None:
463 if self.__frame == 0:
464 if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
465 if self._frame_transparency is not None:
466 self.im.putpalettealpha(self._frame_transparency, 0)
467 self._mode = "RGBA"
468 else:
469 self._mode = "RGB"
470 self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)
471 return
472 if not self._prev_im:
473 return
474 if self.size != self._prev_im.size:
475 if self._frame_transparency is not None:
476 expanded_im = Image.core.fill("RGBA", self.size)
477 else:
478 expanded_im = Image.core.fill("P", self.size)
479 expanded_im.putpalette("RGB", "RGB", self.im.getpalette())
480 expanded_im = expanded_im.convert("RGB")
481 expanded_im.paste(self._prev_im, (0, 0, *self._prev_im.size))
483 self._prev_im = expanded_im
484 assert self._prev_im is not None
485 if self._frame_transparency is not None:
486 if self.mode == "L":
487 frame_im = self.im.convert_transparent("LA", self._frame_transparency)
488 else:
489 self.im.putpalettealpha(self._frame_transparency, 0)
490 frame_im = self.im.convert("RGBA")
491 else:
492 frame_im = self.im.convert("RGB")
494 assert self.dispose_extent is not None
495 frame_im = self._crop(frame_im, self.dispose_extent)
497 self.im = self._prev_im
498 self._mode = self.im.mode
499 if frame_im.mode in {"LA", "RGBA"}:
500 self.im.paste(frame_im, self.dispose_extent, frame_im)
501 else:
502 self.im.paste(frame_im, self.dispose_extent)
504 def tell(self) -> int:
505 return self.__frame
508# --------------------------------------------------------------------
509# Write GIF files
512RAWMODE = {"1": "L", "L": "L", "P": "P"}
515def _normalize_mode(im: Image.Image) -> Image.Image:
516 """
517 Takes an image (or frame), returns an image in a mode that is appropriate
518 for saving in a Gif.
520 It may return the original image, or it may return an image converted to
521 palette or 'L' mode.
523 :param im: Image object
524 :returns: Image object
525 """
526 if im.mode in RAWMODE:
527 im.load()
528 return im
529 if Image.getmodebase(im.mode) == "RGB":
530 if im.mode not in {"RGB", "RGBA"}:
531 # Of the RGB-based modes, quantization only supports RGB and RGBA
532 im = im.convert("RGBA" if im.has_transparency_data else "RGB")
533 im = im.convert("P", palette=Image.Palette.ADAPTIVE)
534 assert im.palette is not None
535 if im.palette.mode == "RGBA":
536 for rgba in im.palette.colors:
537 if rgba[3] == 0:
538 im.info["transparency"] = im.palette.colors[rgba]
539 break
540 return im
541 return im.convert("L")
544_Palette = bytes | bytearray | list[int] | ImagePalette.ImagePalette
547def _normalize_palette(
548 im: Image.Image, palette: _Palette | None, info: dict[str, Any]
549) -> Image.Image:
550 """
551 Normalizes the palette for image.
552 - Sets the palette to the incoming palette, if provided.
553 - Ensures that there's a palette for L mode images
554 - Optimizes the palette if necessary/desired.
556 :param im: Image object
557 :param palette: bytes object containing the source palette, or ....
558 :param info: encoderinfo
559 :returns: Image object
560 """
561 source_palette = None
562 if palette:
563 # a bytes palette
564 if isinstance(palette, (bytes, bytearray, list)):
565 source_palette = bytearray(palette[:768])
566 if isinstance(palette, ImagePalette.ImagePalette):
567 source_palette = bytearray(palette.palette)
569 if im.mode == "P":
570 if not source_palette:
571 im_palette = im.getpalette(None)
572 assert im_palette is not None
573 source_palette = bytearray(im_palette)
574 else: # L-mode
575 if not source_palette:
576 source_palette = bytearray(i // 3 for i in range(768))
577 im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
578 assert source_palette is not None
580 if palette:
581 used_palette_colors: list[int | None] = []
582 assert im.palette is not None
583 for i in range(0, len(source_palette), 3):
584 source_color = tuple(source_palette[i : i + 3])
585 index = im.palette.colors.get(source_color)
586 if index in used_palette_colors:
587 index = None
588 used_palette_colors.append(index)
589 for i, index in enumerate(used_palette_colors):
590 if index is None:
591 for j in range(len(used_palette_colors)):
592 if j not in used_palette_colors:
593 used_palette_colors[i] = j
594 break
595 dest_map: list[int] = []
596 for index in used_palette_colors:
597 assert index is not None
598 dest_map.append(index)
599 im = im.remap_palette(dest_map)
600 else:
601 optimized_palette_colors = _get_optimize(im, info)
602 if optimized_palette_colors is not None:
603 im = im.remap_palette(optimized_palette_colors, source_palette)
604 if "transparency" in info:
605 try:
606 info["transparency"] = optimized_palette_colors.index(
607 info["transparency"]
608 )
609 except ValueError:
610 del info["transparency"]
611 return im
613 assert im.palette is not None
614 im.palette.palette = source_palette
615 return im
618def _write_single_frame(
619 im: Image.Image,
620 fp: IO[bytes],
621 palette: _Palette | None,
622) -> None:
623 im_out = _normalize_mode(im)
624 for k, v in im_out.info.items():
625 if isinstance(k, str):
626 im.encoderinfo.setdefault(k, v)
627 im_out = _normalize_palette(im_out, palette, im.encoderinfo)
629 for s in _get_global_header(im_out, im.encoderinfo):
630 fp.write(s)
632 # local image header
633 flags = 0
634 if get_interlace(im):
635 flags = flags | 64
636 _write_local_header(fp, im, (0, 0), flags)
638 im_out.encoderconfig = (8, get_interlace(im))
639 ImageFile._save(im_out, fp, [ImageFile._Tile("gif", args=RAWMODE[im_out.mode])])
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")
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"] = (
704 im_frames[-1].encoderinfo.get("duration", 0)
705 + encoderinfo["duration"]
706 )
707 continue
708 if im_frames[-1].encoderinfo.get("disposal") == 2:
709 # To appear correctly in viewers using a convention,
710 # only consider transparency, and not background color
711 color = im.encoderinfo.get(
712 "transparency", im.info.get("transparency")
713 )
714 if color is not None:
715 if background_im is None:
716 background = _get_background(im_frame, color)
717 background_im = Image.new("P", im_frame.size, background)
718 first_palette = im_frames[0].im.palette
719 assert first_palette is not None
720 background_im.putpalette(first_palette)
721 bbox = _getbbox(background_im, im_frame)[1]
722 else:
723 bbox = (0, 0, *im_frame.size)
724 elif encoderinfo.get("optimize") and im_frame.mode != "1":
725 if "transparency" not in encoderinfo:
726 assert im_frame.palette is not None
727 try:
728 encoderinfo["transparency"] = (
729 im_frame.palette._new_color_index(im_frame)
730 )
731 except ValueError:
732 pass
733 if "transparency" in encoderinfo:
734 # When the delta is zero, fill the image with transparency
735 if delta.mode == "RGBA":
736 # Each pixel is unchanged only if all four bands are zero
737 r, g, b, a = delta.split()
738 delta = ImageChops.lighter(
739 ImageChops.lighter(r, g),
740 ImageChops.lighter(b, a),
741 )
742 # Map zero pixels (palette indices, for "P")
743 # to a "1" mask without consulting the palette.
744 unchanged = delta.point([255, *(0,) * 255], "1")
745 diff_frame = im_frame.copy()
746 diff_frame.paste(encoderinfo["transparency"], mask=unchanged)
747 else:
748 bbox = None
749 previous_im = im_frame
750 im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo))
752 if len(im_frames) == 1:
753 if "duration" in im_frames[0].encoderinfo:
754 # Since multiple frames will not be written, use the combined duration
755 im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"]
756 return False
758 for frame_data in im_frames:
759 im_frame = frame_data.im
760 if not frame_data.bbox:
761 # global header
762 for s in _get_global_header(im_frame, frame_data.encoderinfo):
763 fp.write(s)
764 offset = (0, 0)
765 else:
766 # compress difference
767 if not palette:
768 frame_data.encoderinfo["include_color_table"] = True
770 if frame_data.bbox != (0, 0, *im_frame.size):
771 im_frame = im_frame.crop(frame_data.bbox)
772 offset = frame_data.bbox[:2]
773 _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo)
774 return True
777def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
778 _save(im, fp, filename, save_all=True)
781def _save(
782 im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
783) -> None:
784 # header
785 if "palette" in im.encoderinfo or "palette" in im.info:
786 palette = im.encoderinfo.get("palette", im.info.get("palette"))
787 else:
788 palette = None
789 im.encoderinfo.setdefault("optimize", True)
791 if not save_all or not _write_multiple_frames(im, fp, palette):
792 _write_single_frame(im, fp, palette)
794 fp.write(b";") # end of file
796 if hasattr(fp, "flush"):
797 fp.flush()
800def get_interlace(im: Image.Image) -> int:
801 interlace = im.encoderinfo.get("interlace", 1)
803 # workaround for @PIL153
804 if min(im.size) < 16:
805 interlace = 0
807 return interlace
810def _write_local_header(
811 fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int
812) -> None:
813 try:
814 transparency = im.encoderinfo["transparency"]
815 except KeyError:
816 transparency = None
818 if "duration" in im.encoderinfo:
819 duration = int(im.encoderinfo["duration"] / 10)
820 else:
821 duration = 0
823 disposal = int(im.encoderinfo.get("disposal", 0))
825 if transparency is not None or duration != 0 or disposal:
826 packed_flag = 1 if transparency is not None else 0
827 packed_flag |= disposal << 2
829 fp.write(
830 b"!"
831 + o8(249) # extension intro
832 + o8(4) # length
833 + o8(packed_flag) # packed fields
834 + o16(duration) # duration
835 + o8(transparency or 0) # transparency index
836 + o8(0)
837 )
839 include_color_table = im.encoderinfo.get("include_color_table")
840 if include_color_table:
841 palette_bytes = _get_palette_bytes(im)
842 color_table_size = _get_color_table_size(palette_bytes)
843 if color_table_size:
844 flags = flags | 128 # local color table flag
845 flags = flags | color_table_size
847 fp.write(
848 b","
849 + o16(offset[0]) # offset
850 + o16(offset[1])
851 + o16(im.size[0]) # size
852 + o16(im.size[1])
853 + o8(flags) # flags
854 )
855 if include_color_table and color_table_size:
856 fp.write(_get_header_palette(palette_bytes))
857 fp.write(o8(8)) # bits
860# Force optimization so that we can test performance against
861# cases where it took lots of memory and time previously.
862_FORCE_OPTIMIZE = False
865def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None:
866 """
867 Palette optimization is a potentially expensive operation.
869 This function determines if the palette should be optimized using
870 some heuristics, then returns the list of palette entries in use.
872 :param im: Image object
873 :param info: encoderinfo
874 :returns: list of indexes of palette entries in use, or None
875 """
876 if (
877 im.mode in {"L", "P"}
878 and info
879 and info.get("optimize")
880 and im.width != 0
881 and im.height != 0
882 ):
883 # Potentially expensive operation.
885 # The palette saves 3 bytes per color not used, but palette
886 # lengths are restricted to 3*(2**N) bytes. Max saving would
887 # be 768 -> 6 bytes if we went all the way down to 2 colors.
888 # * If we're over 128 colors, we can't save any space.
889 # * If there aren't any holes, it's not worth collapsing.
890 # * If we have a 'large' image, the palette is in the noise.
892 # create the new palette if not every color is used
893 optimise = _FORCE_OPTIMIZE or im.mode == "L"
894 if optimise or im.width * im.height < 512 * 512:
895 # check which colors are used
896 used_palette_colors = []
897 for i, count in enumerate(im.histogram()):
898 if count:
899 used_palette_colors.append(i)
901 if optimise or max(used_palette_colors) >= len(used_palette_colors):
902 return used_palette_colors
904 assert im.palette is not None
905 num_palette_colors = len(im.palette.palette) // Image.getmodebands(
906 im.palette.mode
907 )
908 current_palette_size = 1 << (num_palette_colors - 1).bit_length()
909 if (
910 # check that the palette would become smaller when saved
911 len(used_palette_colors) <= current_palette_size // 2
912 # check that the palette is not already the smallest possible size
913 and current_palette_size > 2
914 ):
915 return used_palette_colors
916 return None
919def _get_color_table_size(palette_bytes: bytes) -> int:
920 # calculate the palette size for the header
921 if not palette_bytes:
922 return 0
923 elif len(palette_bytes) < 9:
924 return 1
925 else:
926 return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1
929def _get_header_palette(palette_bytes: bytes) -> bytes:
930 """
931 Returns the palette, null padded to the next power of 2 (*3) bytes
932 suitable for direct inclusion in the GIF header
934 :param palette_bytes: Unpadded palette bytes, in RGBRGB form
935 :returns: Null padded palette
936 """
937 color_table_size = _get_color_table_size(palette_bytes)
939 # add the missing amount of bytes
940 # the palette has to be 2<<n in size
941 actual_target_size_diff = (2 << color_table_size) - len(palette_bytes) // 3
942 if actual_target_size_diff > 0:
943 palette_bytes += o8(0) * 3 * actual_target_size_diff
944 return palette_bytes
947def _get_palette_bytes(im: Image.Image) -> bytes:
948 """
949 Gets the palette for inclusion in the gif header
951 :param im: Image object
952 :returns: Bytes, len<=768 suitable for inclusion in gif header
953 """
954 if not im.palette:
955 return b""
957 palette = bytes(im.palette.palette)
958 if im.palette.mode == "RGBA":
959 palette = b"".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3))
960 return palette
963def _get_background(
964 im: Image.Image,
965 info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None,
966) -> int:
967 background = 0
968 if info_background:
969 if isinstance(info_background, tuple):
970 # WebPImagePlugin stores an RGBA value in info["background"]
971 # So it must be converted to the same format as GifImagePlugin's
972 # info["background"] - a global color table index
973 assert im.palette is not None
974 try:
975 background = im.palette.getcolor(info_background, im)
976 except ValueError as e:
977 if str(e) not in (
978 # If all 256 colors are in use,
979 # then there is no need for the background color
980 "cannot allocate more than 256 colors",
981 # Ignore non-opaque WebP background
982 "cannot add non-opaque RGBA color to RGB palette",
983 ):
984 raise
985 else:
986 background = info_background
987 return background
990def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]:
991 """Return a list of strings representing a GIF header"""
993 # Header Block
994 # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
996 version = b"87a"
997 if im.info.get("version") == b"89a" or (
998 info
999 and (
1000 "transparency" in info
1001 or info.get("loop") is not None
1002 or info.get("duration")
1003 or info.get("comment")
1004 )
1005 ):
1006 version = b"89a"
1008 background = _get_background(im, info.get("background"))
1010 palette_bytes = _get_palette_bytes(im)
1011 color_table_size = _get_color_table_size(palette_bytes)
1013 header = [
1014 b"GIF" # signature
1015 + version # version
1016 + o16(im.size[0]) # canvas width
1017 + o16(im.size[1]), # canvas height
1018 # Logical Screen Descriptor
1019 # size of global color table + global color table flag
1020 o8(color_table_size + 128), # packed fields
1021 # background + reserved/aspect
1022 o8(background) + o8(0),
1023 # Global Color Table
1024 _get_header_palette(palette_bytes),
1025 ]
1026 if info.get("loop") is not None:
1027 header.append(
1028 b"!"
1029 + o8(255) # extension intro
1030 + o8(11)
1031 + b"NETSCAPE2.0"
1032 + o8(3)
1033 + o8(1)
1034 + o16(info["loop"]) # number of loops
1035 + o8(0)
1036 )
1037 if info.get("comment"):
1038 header.append(b"!" + o8(254)) # extension intro
1040 comment = info["comment"]
1041 if isinstance(comment, str):
1042 comment = comment.encode()
1043 for i in range(0, len(comment), 255):
1044 subblock = comment[i : i + 255]
1045 header.append(o8(len(subblock)) + subblock)
1047 header.append(o8(0))
1048 return header
1051def _write_frame_data(
1052 fp: IO[bytes],
1053 im_frame: Image.Image,
1054 offset: tuple[int, int],
1055 params: dict[str, Any],
1056) -> None:
1057 try:
1058 im_frame.encoderinfo = params
1060 # local image header
1061 _write_local_header(fp, im_frame, offset, 0)
1063 ImageFile._save(
1064 im_frame,
1065 fp,
1066 [ImageFile._Tile("gif", (0, 0, *im_frame.size), 0, RAWMODE[im_frame.mode])],
1067 )
1069 fp.write(b"\0") # end of image data
1070 finally:
1071 del im_frame.encoderinfo
1074# --------------------------------------------------------------------
1075# Legacy GIF utilities
1078def getheader(
1079 im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None
1080) -> tuple[list[bytes], list[int] | None]:
1081 """
1082 Legacy Method to get Gif data from image.
1084 Warning:: May modify image data.
1086 :param im: Image object
1087 :param palette: bytes object containing the source palette, or ....
1088 :param info: encoderinfo
1089 :returns: tuple of(list of header items, optimized palette)
1091 """
1092 if info is None:
1093 info = {}
1095 used_palette_colors = _get_optimize(im, info)
1097 if "background" not in info and "background" in im.info:
1098 info["background"] = im.info["background"]
1100 im_mod = _normalize_palette(im, palette, info)
1101 im.palette = im_mod.palette
1102 im.im = im_mod.im
1103 header = _get_global_header(im, info)
1105 return header, used_palette_colors
1108def getdata(
1109 im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any
1110) -> list[bytes]:
1111 """
1112 Legacy Method
1114 Return a list of strings representing this image.
1115 The first string is a local image header, the rest contains
1116 encoded image data.
1118 To specify duration, add the time in milliseconds,
1119 e.g. ``getdata(im_frame, duration=1000)``
1121 :param im: Image object
1122 :param offset: Tuple of (x, y) pixels. Defaults to (0, 0)
1123 :param \\**params: e.g. duration or other encoder info parameters
1124 :returns: List of bytes containing GIF encoded frame data
1126 """
1127 from io import BytesIO
1129 class Collector(BytesIO):
1130 data = []
1132 def write(self, data: Buffer) -> int:
1133 self.data.append(data)
1134 return len(data)
1136 im.load() # make sure raster data is available
1138 fp = Collector()
1140 _write_frame_data(fp, im, offset, params)
1142 return fp.data
1145# --------------------------------------------------------------------
1146# Registry
1148Image.register_open(GifImageFile.format, GifImageFile, _accept)
1149Image.register_save(GifImageFile.format, _save)
1150Image.register_save_all(GifImageFile.format, _save_all)
1151Image.register_extension(GifImageFile.format, ".gif")
1152Image.register_mime(GifImageFile.format, "image/gif")