Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/PIL/GifImagePlugin.py: 7%
578 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
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#
27import itertools
28import math
29import os
30import subprocess
31from enum import IntEnum
33from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
34from ._binary import i16le as i16
35from ._binary import o8
36from ._binary import o16le as o16
39class LoadingStrategy(IntEnum):
40 """.. versionadded:: 9.1.0"""
42 RGB_AFTER_FIRST = 0
43 RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1
44 RGB_ALWAYS = 2
47#: .. versionadded:: 9.1.0
48LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST
50# --------------------------------------------------------------------
51# Identify/read GIF files
54def _accept(prefix):
55 return prefix[:6] in [b"GIF87a", b"GIF89a"]
58##
59# Image plugin for GIF images. This plugin supports both GIF87 and
60# GIF89 images.
63class GifImageFile(ImageFile.ImageFile):
65 format = "GIF"
66 format_description = "Compuserve GIF"
67 _close_exclusive_fp_after_loading = False
69 global_palette = None
71 def data(self):
72 s = self.fp.read(1)
73 if s and s[0]:
74 return self.fp.read(s[0])
75 return None
77 def _is_palette_needed(self, p):
78 for i in range(0, len(p), 3):
79 if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
80 return True
81 return False
83 def _open(self):
85 # Screen
86 s = self.fp.read(13)
87 if not _accept(s):
88 raise SyntaxError("not a GIF file")
90 self.info["version"] = s[:6]
91 self._size = i16(s, 6), i16(s, 8)
92 self.tile = []
93 flags = s[10]
94 bits = (flags & 7) + 1
96 if flags & 128:
97 # get global palette
98 self.info["background"] = s[11]
99 # check if palette contains colour indices
100 p = self.fp.read(3 << bits)
101 if self._is_palette_needed(p):
102 p = ImagePalette.raw("RGB", p)
103 self.global_palette = self.palette = p
105 self._fp = self.fp # FIXME: hack
106 self.__rewind = self.fp.tell()
107 self._n_frames = None
108 self._is_animated = None
109 self._seek(0) # get ready to read first frame
111 @property
112 def n_frames(self):
113 if self._n_frames is None:
114 current = self.tell()
115 try:
116 while True:
117 self._seek(self.tell() + 1, False)
118 except EOFError:
119 self._n_frames = self.tell() + 1
120 self.seek(current)
121 return self._n_frames
123 @property
124 def is_animated(self):
125 if self._is_animated is None:
126 if self._n_frames is not None:
127 self._is_animated = self._n_frames != 1
128 else:
129 current = self.tell()
130 if current:
131 self._is_animated = True
132 else:
133 try:
134 self._seek(1, False)
135 self._is_animated = True
136 except EOFError:
137 self._is_animated = False
139 self.seek(current)
140 return self._is_animated
142 def seek(self, frame):
143 if not self._seek_check(frame):
144 return
145 if frame < self.__frame:
146 self.im = None
147 self._seek(0)
149 last_frame = self.__frame
150 for f in range(self.__frame + 1, frame + 1):
151 try:
152 self._seek(f)
153 except EOFError as e:
154 self.seek(last_frame)
155 raise EOFError("no more images in GIF file") from e
157 def _seek(self, frame, update_image=True):
159 if frame == 0:
160 # rewind
161 self.__offset = 0
162 self.dispose = None
163 self.__frame = -1
164 self._fp.seek(self.__rewind)
165 self.disposal_method = 0
166 if "comment" in self.info:
167 del self.info["comment"]
168 else:
169 # ensure that the previous frame was loaded
170 if self.tile and update_image:
171 self.load()
173 if frame != self.__frame + 1:
174 raise ValueError(f"cannot seek to frame {frame}")
176 self.fp = self._fp
177 if self.__offset:
178 # backup to last frame
179 self.fp.seek(self.__offset)
180 while self.data():
181 pass
182 self.__offset = 0
184 s = self.fp.read(1)
185 if not s or s == b";":
186 raise EOFError
188 self.tile = []
190 palette = None
192 info = {}
193 frame_transparency = None
194 interlace = None
195 frame_dispose_extent = None
196 while True:
198 if not s:
199 s = self.fp.read(1)
200 if not s or s == b";":
201 break
203 elif s == b"!":
204 #
205 # extensions
206 #
207 s = self.fp.read(1)
208 block = self.data()
209 if s[0] == 249:
210 #
211 # graphic control extension
212 #
213 flags = block[0]
214 if flags & 1:
215 frame_transparency = block[3]
216 info["duration"] = i16(block, 1) * 10
218 # disposal method - find the value of bits 4 - 6
219 dispose_bits = 0b00011100 & flags
220 dispose_bits = dispose_bits >> 2
221 if dispose_bits:
222 # only set the dispose if it is not
223 # unspecified. I'm not sure if this is
224 # correct, but it seems to prevent the last
225 # frame from looking odd for some animations
226 self.disposal_method = dispose_bits
227 elif s[0] == 254:
228 #
229 # comment extension
230 #
231 comment = b""
233 # Read this comment block
234 while block:
235 comment += block
236 block = self.data()
238 if "comment" in info:
239 # If multiple comment blocks in frame, separate with \n
240 info["comment"] += b"\n" + comment
241 else:
242 info["comment"] = comment
243 s = None
244 continue
245 elif s[0] == 255 and frame == 0:
246 #
247 # application extension
248 #
249 info["extension"] = block, self.fp.tell()
250 if block[:11] == b"NETSCAPE2.0":
251 block = self.data()
252 if len(block) >= 3 and block[0] == 1:
253 self.info["loop"] = i16(block, 1)
254 while self.data():
255 pass
257 elif s == b",":
258 #
259 # local image
260 #
261 s = self.fp.read(9)
263 # extent
264 x0, y0 = i16(s, 0), i16(s, 2)
265 x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)
266 if (x1 > self.size[0] or y1 > self.size[1]) and update_image:
267 self._size = max(x1, self.size[0]), max(y1, self.size[1])
268 Image._decompression_bomb_check(self._size)
269 frame_dispose_extent = x0, y0, x1, y1
270 flags = s[8]
272 interlace = (flags & 64) != 0
274 if flags & 128:
275 bits = (flags & 7) + 1
276 p = self.fp.read(3 << bits)
277 if self._is_palette_needed(p):
278 palette = ImagePalette.raw("RGB", p)
280 # image data
281 bits = self.fp.read(1)[0]
282 self.__offset = self.fp.tell()
283 break
285 else:
286 pass
287 # raise OSError, "illegal GIF tag `%x`" % s[0]
288 s = None
290 if interlace is None:
291 # self._fp = None
292 raise EOFError
294 self.__frame = frame
295 if not update_image:
296 return
298 if self.dispose:
299 self.im.paste(self.dispose, self.dispose_extent)
301 self._frame_palette = palette or self.global_palette
302 if frame == 0:
303 if self._frame_palette:
304 self.mode = (
305 "RGB" if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS else "P"
306 )
307 else:
308 self.mode = "L"
310 if not palette and self.global_palette:
311 from copy import copy
313 palette = copy(self.global_palette)
314 self.palette = palette
315 else:
316 self._frame_transparency = frame_transparency
317 if self.mode == "P":
318 if (
319 LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
320 or palette
321 ):
322 self.pyaccess = None
323 if "transparency" in self.info:
324 self.im.putpalettealpha(self.info["transparency"], 0)
325 self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
326 self.mode = "RGBA"
327 del self.info["transparency"]
328 else:
329 self.mode = "RGB"
330 self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
332 def _rgb(color):
333 if self._frame_palette:
334 color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
335 else:
336 color = (color, color, color)
337 return color
339 self.dispose_extent = frame_dispose_extent
340 try:
341 if self.disposal_method < 2:
342 # do not dispose or none specified
343 self.dispose = None
344 elif self.disposal_method == 2:
345 # replace with background colour
347 # only dispose the extent in this frame
348 x0, y0, x1, y1 = self.dispose_extent
349 dispose_size = (x1 - x0, y1 - y0)
351 Image._decompression_bomb_check(dispose_size)
353 # by convention, attempt to use transparency first
354 dispose_mode = "P"
355 color = self.info.get("transparency", frame_transparency)
356 if color is not None:
357 if self.mode in ("RGB", "RGBA"):
358 dispose_mode = "RGBA"
359 color = _rgb(color) + (0,)
360 else:
361 color = self.info.get("background", 0)
362 if self.mode in ("RGB", "RGBA"):
363 dispose_mode = "RGB"
364 color = _rgb(color)
365 self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
366 else:
367 # replace with previous contents
368 if self.im is not None:
369 # only dispose the extent in this frame
370 self.dispose = self._crop(self.im, self.dispose_extent)
371 elif frame_transparency is not None:
372 x0, y0, x1, y1 = self.dispose_extent
373 dispose_size = (x1 - x0, y1 - y0)
375 Image._decompression_bomb_check(dispose_size)
376 dispose_mode = "P"
377 color = frame_transparency
378 if self.mode in ("RGB", "RGBA"):
379 dispose_mode = "RGBA"
380 color = _rgb(frame_transparency) + (0,)
381 self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
382 except AttributeError:
383 pass
385 if interlace is not None:
386 transparency = -1
387 if frame_transparency is not None:
388 if frame == 0:
389 self.info["transparency"] = frame_transparency
390 elif self.mode not in ("RGB", "RGBA"):
391 transparency = frame_transparency
392 self.tile = [
393 (
394 "gif",
395 (x0, y0, x1, y1),
396 self.__offset,
397 (bits, interlace, transparency),
398 )
399 ]
401 if info.get("comment"):
402 self.info["comment"] = info["comment"]
403 for k in ["duration", "extension"]:
404 if k in info:
405 self.info[k] = info[k]
406 elif k in self.info:
407 del self.info[k]
409 def load_prepare(self):
410 temp_mode = "P" if self._frame_palette else "L"
411 self._prev_im = None
412 if self.__frame == 0:
413 if "transparency" in self.info:
414 self.im = Image.core.fill(
415 temp_mode, self.size, self.info["transparency"]
416 )
417 elif self.mode in ("RGB", "RGBA"):
418 self._prev_im = self.im
419 if self._frame_palette:
420 self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
421 self.im.putpalette(*self._frame_palette.getdata())
422 else:
423 self.im = None
424 self.mode = temp_mode
425 self._frame_palette = None
427 super().load_prepare()
429 def load_end(self):
430 if self.__frame == 0:
431 if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
432 self.mode = "RGB"
433 self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
434 return
435 if self.mode == "P" and self._prev_im:
436 if self._frame_transparency is not None:
437 self.im.putpalettealpha(self._frame_transparency, 0)
438 frame_im = self.im.convert("RGBA")
439 else:
440 frame_im = self.im.convert("RGB")
441 else:
442 if not self._prev_im:
443 return
444 frame_im = self.im
445 frame_im = self._crop(frame_im, self.dispose_extent)
447 self.im = self._prev_im
448 self.mode = self.im.mode
449 if frame_im.mode == "RGBA":
450 self.im.paste(frame_im, self.dispose_extent, frame_im)
451 else:
452 self.im.paste(frame_im, self.dispose_extent)
454 def tell(self):
455 return self.__frame
458# --------------------------------------------------------------------
459# Write GIF files
462RAWMODE = {"1": "L", "L": "L", "P": "P"}
465def _normalize_mode(im):
466 """
467 Takes an image (or frame), returns an image in a mode that is appropriate
468 for saving in a Gif.
470 It may return the original image, or it may return an image converted to
471 palette or 'L' mode.
473 :param im: Image object
474 :returns: Image object
475 """
476 if im.mode in RAWMODE:
477 im.load()
478 return im
479 if Image.getmodebase(im.mode) == "RGB":
480 im = im.convert("P", palette=Image.Palette.ADAPTIVE)
481 if im.palette.mode == "RGBA":
482 for rgba in im.palette.colors.keys():
483 if rgba[3] == 0:
484 im.info["transparency"] = im.palette.colors[rgba]
485 break
486 return im
487 return im.convert("L")
490def _normalize_palette(im, palette, info):
491 """
492 Normalizes the palette for image.
493 - Sets the palette to the incoming palette, if provided.
494 - Ensures that there's a palette for L mode images
495 - Optimizes the palette if necessary/desired.
497 :param im: Image object
498 :param palette: bytes object containing the source palette, or ....
499 :param info: encoderinfo
500 :returns: Image object
501 """
502 source_palette = None
503 if palette:
504 # a bytes palette
505 if isinstance(palette, (bytes, bytearray, list)):
506 source_palette = bytearray(palette[:768])
507 if isinstance(palette, ImagePalette.ImagePalette):
508 source_palette = bytearray(palette.palette)
510 if im.mode == "P":
511 if not source_palette:
512 source_palette = im.im.getpalette("RGB")[:768]
513 else: # L-mode
514 if not source_palette:
515 source_palette = bytearray(i // 3 for i in range(768))
516 im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
518 if palette:
519 used_palette_colors = []
520 for i in range(0, len(source_palette), 3):
521 source_color = tuple(source_palette[i : i + 3])
522 try:
523 index = im.palette.colors[source_color]
524 except KeyError:
525 index = None
526 used_palette_colors.append(index)
527 for i, index in enumerate(used_palette_colors):
528 if index is None:
529 for j in range(len(used_palette_colors)):
530 if j not in used_palette_colors:
531 used_palette_colors[i] = j
532 break
533 im = im.remap_palette(used_palette_colors)
534 else:
535 used_palette_colors = _get_optimize(im, info)
536 if used_palette_colors is not None:
537 return im.remap_palette(used_palette_colors, source_palette)
539 im.palette.palette = source_palette
540 return im
543def _write_single_frame(im, fp, palette):
544 im_out = _normalize_mode(im)
545 for k, v in im_out.info.items():
546 im.encoderinfo.setdefault(k, v)
547 im_out = _normalize_palette(im_out, palette, im.encoderinfo)
549 for s in _get_global_header(im_out, im.encoderinfo):
550 fp.write(s)
552 # local image header
553 flags = 0
554 if get_interlace(im):
555 flags = flags | 64
556 _write_local_header(fp, im, (0, 0), flags)
558 im_out.encoderconfig = (8, get_interlace(im))
559 ImageFile._save(im_out, fp, [("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])])
561 fp.write(b"\0") # end of image data
564def _write_multiple_frames(im, fp, palette):
566 duration = im.encoderinfo.get("duration")
567 disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
569 im_frames = []
570 frame_count = 0
571 background_im = None
572 for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
573 for im_frame in ImageSequence.Iterator(imSequence):
574 # a copy is required here since seek can still mutate the image
575 im_frame = _normalize_mode(im_frame.copy())
576 if frame_count == 0:
577 for k, v in im_frame.info.items():
578 if k == "transparency":
579 continue
580 im.encoderinfo.setdefault(k, v)
582 encoderinfo = im.encoderinfo.copy()
583 im_frame = _normalize_palette(im_frame, palette, encoderinfo)
584 if "transparency" in im_frame.info:
585 encoderinfo.setdefault("transparency", im_frame.info["transparency"])
586 if isinstance(duration, (list, tuple)):
587 encoderinfo["duration"] = duration[frame_count]
588 elif duration is None and "duration" in im_frame.info:
589 encoderinfo["duration"] = im_frame.info["duration"]
590 if isinstance(disposal, (list, tuple)):
591 encoderinfo["disposal"] = disposal[frame_count]
592 frame_count += 1
594 if im_frames:
595 # delta frame
596 previous = im_frames[-1]
597 if encoderinfo.get("disposal") == 2:
598 if background_im is None:
599 color = im.encoderinfo.get(
600 "transparency", im.info.get("transparency", (0, 0, 0))
601 )
602 background = _get_background(im_frame, color)
603 background_im = Image.new("P", im_frame.size, background)
604 background_im.putpalette(im_frames[0]["im"].palette)
605 base_im = background_im
606 else:
607 base_im = previous["im"]
608 if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im):
609 delta = ImageChops.subtract_modulo(im_frame, base_im)
610 else:
611 delta = ImageChops.subtract_modulo(
612 im_frame.convert("RGB"), base_im.convert("RGB")
613 )
614 bbox = delta.getbbox()
615 if not bbox:
616 # This frame is identical to the previous frame
617 if duration:
618 previous["encoderinfo"]["duration"] += encoderinfo["duration"]
619 continue
620 else:
621 bbox = None
622 im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
624 if len(im_frames) > 1:
625 for frame_data in im_frames:
626 im_frame = frame_data["im"]
627 if not frame_data["bbox"]:
628 # global header
629 for s in _get_global_header(im_frame, frame_data["encoderinfo"]):
630 fp.write(s)
631 offset = (0, 0)
632 else:
633 # compress difference
634 if not palette:
635 frame_data["encoderinfo"]["include_color_table"] = True
637 im_frame = im_frame.crop(frame_data["bbox"])
638 offset = frame_data["bbox"][:2]
639 _write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"])
640 return True
641 elif "duration" in im.encoderinfo and isinstance(
642 im.encoderinfo["duration"], (list, tuple)
643 ):
644 # Since multiple frames will not be written, add together the frame durations
645 im.encoderinfo["duration"] = sum(im.encoderinfo["duration"])
648def _save_all(im, fp, filename):
649 _save(im, fp, filename, save_all=True)
652def _save(im, fp, filename, save_all=False):
653 # header
654 if "palette" in im.encoderinfo or "palette" in im.info:
655 palette = im.encoderinfo.get("palette", im.info.get("palette"))
656 else:
657 palette = None
658 im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)
660 if not save_all or not _write_multiple_frames(im, fp, palette):
661 _write_single_frame(im, fp, palette)
663 fp.write(b";") # end of file
665 if hasattr(fp, "flush"):
666 fp.flush()
669def get_interlace(im):
670 interlace = im.encoderinfo.get("interlace", 1)
672 # workaround for @PIL153
673 if min(im.size) < 16:
674 interlace = 0
676 return interlace
679def _write_local_header(fp, im, offset, flags):
680 transparent_color_exists = False
681 try:
682 if "transparency" in im.encoderinfo:
683 transparency = im.encoderinfo["transparency"]
684 else:
685 transparency = im.info["transparency"]
686 transparency = int(transparency)
687 except (KeyError, ValueError):
688 pass
689 else:
690 # optimize the block away if transparent color is not used
691 transparent_color_exists = True
693 used_palette_colors = _get_optimize(im, im.encoderinfo)
694 if used_palette_colors is not None:
695 # adjust the transparency index after optimize
696 try:
697 transparency = used_palette_colors.index(transparency)
698 except ValueError:
699 transparent_color_exists = False
701 if "duration" in im.encoderinfo:
702 duration = int(im.encoderinfo["duration"] / 10)
703 else:
704 duration = 0
706 disposal = int(im.encoderinfo.get("disposal", 0))
708 if transparent_color_exists or duration != 0 or disposal:
709 packed_flag = 1 if transparent_color_exists else 0
710 packed_flag |= disposal << 2
711 if not transparent_color_exists:
712 transparency = 0
714 fp.write(
715 b"!"
716 + o8(249) # extension intro
717 + o8(4) # length
718 + o8(packed_flag) # packed fields
719 + o16(duration) # duration
720 + o8(transparency) # transparency index
721 + o8(0)
722 )
724 include_color_table = im.encoderinfo.get("include_color_table")
725 if include_color_table:
726 palette_bytes = _get_palette_bytes(im)
727 color_table_size = _get_color_table_size(palette_bytes)
728 if color_table_size:
729 flags = flags | 128 # local color table flag
730 flags = flags | color_table_size
732 fp.write(
733 b","
734 + o16(offset[0]) # offset
735 + o16(offset[1])
736 + o16(im.size[0]) # size
737 + o16(im.size[1])
738 + o8(flags) # flags
739 )
740 if include_color_table and color_table_size:
741 fp.write(_get_header_palette(palette_bytes))
742 fp.write(o8(8)) # bits
745def _save_netpbm(im, fp, filename):
747 # Unused by default.
748 # To use, uncomment the register_save call at the end of the file.
749 #
750 # If you need real GIF compression and/or RGB quantization, you
751 # can use the external NETPBM/PBMPLUS utilities. See comments
752 # below for information on how to enable this.
753 tempfile = im._dump()
755 try:
756 with open(filename, "wb") as f:
757 if im.mode != "RGB":
758 subprocess.check_call(
759 ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL
760 )
761 else:
762 # Pipe ppmquant output into ppmtogif
763 # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename)
764 quant_cmd = ["ppmquant", "256", tempfile]
765 togif_cmd = ["ppmtogif"]
766 quant_proc = subprocess.Popen(
767 quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
768 )
769 togif_proc = subprocess.Popen(
770 togif_cmd,
771 stdin=quant_proc.stdout,
772 stdout=f,
773 stderr=subprocess.DEVNULL,
774 )
776 # Allow ppmquant to receive SIGPIPE if ppmtogif exits
777 quant_proc.stdout.close()
779 retcode = quant_proc.wait()
780 if retcode:
781 raise subprocess.CalledProcessError(retcode, quant_cmd)
783 retcode = togif_proc.wait()
784 if retcode:
785 raise subprocess.CalledProcessError(retcode, togif_cmd)
786 finally:
787 try:
788 os.unlink(tempfile)
789 except OSError:
790 pass
793# Force optimization so that we can test performance against
794# cases where it took lots of memory and time previously.
795_FORCE_OPTIMIZE = False
798def _get_optimize(im, info):
799 """
800 Palette optimization is a potentially expensive operation.
802 This function determines if the palette should be optimized using
803 some heuristics, then returns the list of palette entries in use.
805 :param im: Image object
806 :param info: encoderinfo
807 :returns: list of indexes of palette entries in use, or None
808 """
809 if im.mode in ("P", "L") and info and info.get("optimize", 0):
810 # Potentially expensive operation.
812 # The palette saves 3 bytes per color not used, but palette
813 # lengths are restricted to 3*(2**N) bytes. Max saving would
814 # be 768 -> 6 bytes if we went all the way down to 2 colors.
815 # * If we're over 128 colors, we can't save any space.
816 # * If there aren't any holes, it's not worth collapsing.
817 # * If we have a 'large' image, the palette is in the noise.
819 # create the new palette if not every color is used
820 optimise = _FORCE_OPTIMIZE or im.mode == "L"
821 if optimise or im.width * im.height < 512 * 512:
822 # check which colors are used
823 used_palette_colors = []
824 for i, count in enumerate(im.histogram()):
825 if count:
826 used_palette_colors.append(i)
828 if optimise or max(used_palette_colors) >= len(used_palette_colors):
829 return used_palette_colors
831 num_palette_colors = len(im.palette.palette) // Image.getmodebands(
832 im.palette.mode
833 )
834 current_palette_size = 1 << (num_palette_colors - 1).bit_length()
835 if (
836 # check that the palette would become smaller when saved
837 len(used_palette_colors) <= current_palette_size // 2
838 # check that the palette is not already the smallest possible size
839 and current_palette_size > 2
840 ):
841 return used_palette_colors
844def _get_color_table_size(palette_bytes):
845 # calculate the palette size for the header
846 if not palette_bytes:
847 return 0
848 elif len(palette_bytes) < 9:
849 return 1
850 else:
851 return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1
854def _get_header_palette(palette_bytes):
855 """
856 Returns the palette, null padded to the next power of 2 (*3) bytes
857 suitable for direct inclusion in the GIF header
859 :param palette_bytes: Unpadded palette bytes, in RGBRGB form
860 :returns: Null padded palette
861 """
862 color_table_size = _get_color_table_size(palette_bytes)
864 # add the missing amount of bytes
865 # the palette has to be 2<<n in size
866 actual_target_size_diff = (2 << color_table_size) - len(palette_bytes) // 3
867 if actual_target_size_diff > 0:
868 palette_bytes += o8(0) * 3 * actual_target_size_diff
869 return palette_bytes
872def _get_palette_bytes(im):
873 """
874 Gets the palette for inclusion in the gif header
876 :param im: Image object
877 :returns: Bytes, len<=768 suitable for inclusion in gif header
878 """
879 return im.palette.palette
882def _get_background(im, info_background):
883 background = 0
884 if info_background:
885 background = info_background
886 if isinstance(background, tuple):
887 # WebPImagePlugin stores an RGBA value in info["background"]
888 # So it must be converted to the same format as GifImagePlugin's
889 # info["background"] - a global color table index
890 try:
891 background = im.palette.getcolor(background, im)
892 except ValueError as e:
893 if str(e) == "cannot allocate more than 256 colors":
894 # If all 256 colors are in use,
895 # then there is no need for the background color
896 return 0
897 else:
898 raise
899 return background
902def _get_global_header(im, info):
903 """Return a list of strings representing a GIF header"""
905 # Header Block
906 # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
908 version = b"87a"
909 if im.info.get("version") == b"89a" or (
910 info
911 and (
912 "transparency" in info
913 or "loop" in info
914 or info.get("duration")
915 or info.get("comment")
916 )
917 ):
918 version = b"89a"
920 background = _get_background(im, info.get("background"))
922 palette_bytes = _get_palette_bytes(im)
923 color_table_size = _get_color_table_size(palette_bytes)
925 header = [
926 b"GIF" # signature
927 + version # version
928 + o16(im.size[0]) # canvas width
929 + o16(im.size[1]), # canvas height
930 # Logical Screen Descriptor
931 # size of global color table + global color table flag
932 o8(color_table_size + 128), # packed fields
933 # background + reserved/aspect
934 o8(background) + o8(0),
935 # Global Color Table
936 _get_header_palette(palette_bytes),
937 ]
938 if "loop" in info:
939 header.append(
940 b"!"
941 + o8(255) # extension intro
942 + o8(11)
943 + b"NETSCAPE2.0"
944 + o8(3)
945 + o8(1)
946 + o16(info["loop"]) # number of loops
947 + o8(0)
948 )
949 if info.get("comment"):
950 comment_block = b"!" + o8(254) # extension intro
952 comment = info["comment"]
953 if isinstance(comment, str):
954 comment = comment.encode()
955 for i in range(0, len(comment), 255):
956 subblock = comment[i : i + 255]
957 comment_block += o8(len(subblock)) + subblock
959 comment_block += o8(0)
960 header.append(comment_block)
961 return header
964def _write_frame_data(fp, im_frame, offset, params):
965 try:
966 im_frame.encoderinfo = params
968 # local image header
969 _write_local_header(fp, im_frame, offset, 0)
971 ImageFile._save(
972 im_frame, fp, [("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])]
973 )
975 fp.write(b"\0") # end of image data
976 finally:
977 del im_frame.encoderinfo
980# --------------------------------------------------------------------
981# Legacy GIF utilities
984def getheader(im, palette=None, info=None):
985 """
986 Legacy Method to get Gif data from image.
988 Warning:: May modify image data.
990 :param im: Image object
991 :param palette: bytes object containing the source palette, or ....
992 :param info: encoderinfo
993 :returns: tuple of(list of header items, optimized palette)
995 """
996 used_palette_colors = _get_optimize(im, info)
998 if info is None:
999 info = {}
1001 if "background" not in info and "background" in im.info:
1002 info["background"] = im.info["background"]
1004 im_mod = _normalize_palette(im, palette, info)
1005 im.palette = im_mod.palette
1006 im.im = im_mod.im
1007 header = _get_global_header(im, info)
1009 return header, used_palette_colors
1012def getdata(im, offset=(0, 0), **params):
1013 """
1014 Legacy Method
1016 Return a list of strings representing this image.
1017 The first string is a local image header, the rest contains
1018 encoded image data.
1020 To specify duration, add the time in milliseconds,
1021 e.g. ``getdata(im_frame, duration=1000)``
1023 :param im: Image object
1024 :param offset: Tuple of (x, y) pixels. Defaults to (0, 0)
1025 :param \\**params: e.g. duration or other encoder info parameters
1026 :returns: List of bytes containing GIF encoded frame data
1028 """
1030 class Collector:
1031 data = []
1033 def write(self, data):
1034 self.data.append(data)
1036 im.load() # make sure raster data is available
1038 fp = Collector()
1040 _write_frame_data(fp, im, offset, params)
1042 return fp.data
1045# --------------------------------------------------------------------
1046# Registry
1048Image.register_open(GifImageFile.format, GifImageFile, _accept)
1049Image.register_save(GifImageFile.format, _save)
1050Image.register_save_all(GifImageFile.format, _save_all)
1051Image.register_extension(GifImageFile.format, ".gif")
1052Image.register_mime(GifImageFile.format, "image/gif")
1054#
1055# Uncomment the following line if you wish to use NETPBM/PBMPLUS
1056# instead of the built-in "uncompressed" GIF encoder
1058# Image.register_save(GifImageFile.format, _save_netpbm)