GifImagePlugin.py 36 KB

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