GifImagePlugin.py 41 KB

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