GifImagePlugin.py 34 KB

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