GifImagePlugin.py 41 KB

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