GifImagePlugin.py 39 KB

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