GifImagePlugin.py 41 KB

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