GifImagePlugin.py 35 KB

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