GifImagePlugin.py 40 KB

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