Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

GifImagePlugin.py 35KB

1 year ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064
  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("RGB"), base_im.convert("RGB")
  489. )
  490. return delta.getbbox()
  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. if "transparency" in im.encoderinfo:
  583. transparency = im.encoderinfo["transparency"]
  584. else:
  585. transparency = im.info["transparency"]
  586. transparency = int(transparency)
  587. except (KeyError, ValueError):
  588. pass
  589. else:
  590. # optimize the block away if transparent color is not used
  591. transparent_color_exists = True
  592. used_palette_colors = _get_optimize(im, im.encoderinfo)
  593. if used_palette_colors is not None:
  594. # adjust the transparency index after optimize
  595. try:
  596. transparency = used_palette_colors.index(transparency)
  597. except ValueError:
  598. transparent_color_exists = False
  599. if "duration" in im.encoderinfo:
  600. duration = int(im.encoderinfo["duration"] / 10)
  601. else:
  602. duration = 0
  603. disposal = int(im.encoderinfo.get("disposal", 0))
  604. if transparent_color_exists or duration != 0 or disposal:
  605. packed_flag = 1 if transparent_color_exists else 0
  606. packed_flag |= disposal << 2
  607. if not transparent_color_exists:
  608. transparency = 0
  609. fp.write(
  610. b"!"
  611. + o8(249) # extension intro
  612. + o8(4) # length
  613. + o8(packed_flag) # packed fields
  614. + o16(duration) # duration
  615. + o8(transparency) # transparency index
  616. + o8(0)
  617. )
  618. include_color_table = im.encoderinfo.get("include_color_table")
  619. if include_color_table:
  620. palette_bytes = _get_palette_bytes(im)
  621. color_table_size = _get_color_table_size(palette_bytes)
  622. if color_table_size:
  623. flags = flags | 128 # local color table flag
  624. flags = flags | color_table_size
  625. fp.write(
  626. b","
  627. + o16(offset[0]) # offset
  628. + o16(offset[1])
  629. + o16(im.size[0]) # size
  630. + o16(im.size[1])
  631. + o8(flags) # flags
  632. )
  633. if include_color_table and color_table_size:
  634. fp.write(_get_header_palette(palette_bytes))
  635. fp.write(o8(8)) # bits
  636. def _save_netpbm(im, fp, filename):
  637. # Unused by default.
  638. # To use, uncomment the register_save call at the end of the file.
  639. #
  640. # If you need real GIF compression and/or RGB quantization, you
  641. # can use the external NETPBM/PBMPLUS utilities. See comments
  642. # below for information on how to enable this.
  643. tempfile = im._dump()
  644. try:
  645. with open(filename, "wb") as f:
  646. if im.mode != "RGB":
  647. subprocess.check_call(
  648. ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL
  649. )
  650. else:
  651. # Pipe ppmquant output into ppmtogif
  652. # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename)
  653. quant_cmd = ["ppmquant", "256", tempfile]
  654. togif_cmd = ["ppmtogif"]
  655. quant_proc = subprocess.Popen(
  656. quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
  657. )
  658. togif_proc = subprocess.Popen(
  659. togif_cmd,
  660. stdin=quant_proc.stdout,
  661. stdout=f,
  662. stderr=subprocess.DEVNULL,
  663. )
  664. # Allow ppmquant to receive SIGPIPE if ppmtogif exits
  665. quant_proc.stdout.close()
  666. retcode = quant_proc.wait()
  667. if retcode:
  668. raise subprocess.CalledProcessError(retcode, quant_cmd)
  669. retcode = togif_proc.wait()
  670. if retcode:
  671. raise subprocess.CalledProcessError(retcode, togif_cmd)
  672. finally:
  673. try:
  674. os.unlink(tempfile)
  675. except OSError:
  676. pass
  677. # Force optimization so that we can test performance against
  678. # cases where it took lots of memory and time previously.
  679. _FORCE_OPTIMIZE = False
  680. def _get_optimize(im, info):
  681. """
  682. Palette optimization is a potentially expensive operation.
  683. This function determines if the palette should be optimized using
  684. some heuristics, then returns the list of palette entries in use.
  685. :param im: Image object
  686. :param info: encoderinfo
  687. :returns: list of indexes of palette entries in use, or None
  688. """
  689. if im.mode in ("P", "L") and info and info.get("optimize", 0):
  690. # Potentially expensive operation.
  691. # The palette saves 3 bytes per color not used, but palette
  692. # lengths are restricted to 3*(2**N) bytes. Max saving would
  693. # be 768 -> 6 bytes if we went all the way down to 2 colors.
  694. # * If we're over 128 colors, we can't save any space.
  695. # * If there aren't any holes, it's not worth collapsing.
  696. # * If we have a 'large' image, the palette is in the noise.
  697. # create the new palette if not every color is used
  698. optimise = _FORCE_OPTIMIZE or im.mode == "L"
  699. if optimise or im.width * im.height < 512 * 512:
  700. # check which colors are used
  701. used_palette_colors = []
  702. for i, count in enumerate(im.histogram()):
  703. if count:
  704. used_palette_colors.append(i)
  705. if optimise or max(used_palette_colors) >= len(used_palette_colors):
  706. return used_palette_colors
  707. num_palette_colors = len(im.palette.palette) // Image.getmodebands(
  708. im.palette.mode
  709. )
  710. current_palette_size = 1 << (num_palette_colors - 1).bit_length()
  711. if (
  712. # check that the palette would become smaller when saved
  713. len(used_palette_colors) <= current_palette_size // 2
  714. # check that the palette is not already the smallest possible size
  715. and current_palette_size > 2
  716. ):
  717. return used_palette_colors
  718. def _get_color_table_size(palette_bytes):
  719. # calculate the palette size for the header
  720. if not palette_bytes:
  721. return 0
  722. elif len(palette_bytes) < 9:
  723. return 1
  724. else:
  725. return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1
  726. def _get_header_palette(palette_bytes):
  727. """
  728. Returns the palette, null padded to the next power of 2 (*3) bytes
  729. suitable for direct inclusion in the GIF header
  730. :param palette_bytes: Unpadded palette bytes, in RGBRGB form
  731. :returns: Null padded palette
  732. """
  733. color_table_size = _get_color_table_size(palette_bytes)
  734. # add the missing amount of bytes
  735. # the palette has to be 2<<n in size
  736. actual_target_size_diff = (2 << color_table_size) - len(palette_bytes) // 3
  737. if actual_target_size_diff > 0:
  738. palette_bytes += o8(0) * 3 * actual_target_size_diff
  739. return palette_bytes
  740. def _get_palette_bytes(im):
  741. """
  742. Gets the palette for inclusion in the gif header
  743. :param im: Image object
  744. :returns: Bytes, len<=768 suitable for inclusion in gif header
  745. """
  746. return im.palette.palette
  747. def _get_background(im, info_background):
  748. background = 0
  749. if info_background:
  750. if isinstance(info_background, tuple):
  751. # WebPImagePlugin stores an RGBA value in info["background"]
  752. # So it must be converted to the same format as GifImagePlugin's
  753. # info["background"] - a global color table index
  754. try:
  755. background = im.palette.getcolor(info_background, im)
  756. except ValueError as e:
  757. if str(e) not in (
  758. # If all 256 colors are in use,
  759. # then there is no need for the background color
  760. "cannot allocate more than 256 colors",
  761. # Ignore non-opaque WebP background
  762. "cannot add non-opaque RGBA color to RGB palette",
  763. ):
  764. raise
  765. else:
  766. background = info_background
  767. return background
  768. def _get_global_header(im, info):
  769. """Return a list of strings representing a GIF header"""
  770. # Header Block
  771. # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
  772. version = b"87a"
  773. if im.info.get("version") == b"89a" or (
  774. info
  775. and (
  776. "transparency" in info
  777. or "loop" in info
  778. or info.get("duration")
  779. or info.get("comment")
  780. )
  781. ):
  782. version = b"89a"
  783. background = _get_background(im, info.get("background"))
  784. palette_bytes = _get_palette_bytes(im)
  785. color_table_size = _get_color_table_size(palette_bytes)
  786. header = [
  787. b"GIF" # signature
  788. + version # version
  789. + o16(im.size[0]) # canvas width
  790. + o16(im.size[1]), # canvas height
  791. # Logical Screen Descriptor
  792. # size of global color table + global color table flag
  793. o8(color_table_size + 128), # packed fields
  794. # background + reserved/aspect
  795. o8(background) + o8(0),
  796. # Global Color Table
  797. _get_header_palette(palette_bytes),
  798. ]
  799. if "loop" in info:
  800. header.append(
  801. b"!"
  802. + o8(255) # extension intro
  803. + o8(11)
  804. + b"NETSCAPE2.0"
  805. + o8(3)
  806. + o8(1)
  807. + o16(info["loop"]) # number of loops
  808. + o8(0)
  809. )
  810. if info.get("comment"):
  811. comment_block = b"!" + o8(254) # extension intro
  812. comment = info["comment"]
  813. if isinstance(comment, str):
  814. comment = comment.encode()
  815. for i in range(0, len(comment), 255):
  816. subblock = comment[i : i + 255]
  817. comment_block += o8(len(subblock)) + subblock
  818. comment_block += o8(0)
  819. header.append(comment_block)
  820. return header
  821. def _write_frame_data(fp, im_frame, offset, params):
  822. try:
  823. im_frame.encoderinfo = params
  824. # local image header
  825. _write_local_header(fp, im_frame, offset, 0)
  826. ImageFile._save(
  827. im_frame, fp, [("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])]
  828. )
  829. fp.write(b"\0") # end of image data
  830. finally:
  831. del im_frame.encoderinfo
  832. # --------------------------------------------------------------------
  833. # Legacy GIF utilities
  834. def getheader(im, palette=None, info=None):
  835. """
  836. Legacy Method to get Gif data from image.
  837. Warning:: May modify image data.
  838. :param im: Image object
  839. :param palette: bytes object containing the source palette, or ....
  840. :param info: encoderinfo
  841. :returns: tuple of(list of header items, optimized palette)
  842. """
  843. used_palette_colors = _get_optimize(im, info)
  844. if info is None:
  845. info = {}
  846. if "background" not in info and "background" in im.info:
  847. info["background"] = im.info["background"]
  848. im_mod = _normalize_palette(im, palette, info)
  849. im.palette = im_mod.palette
  850. im.im = im_mod.im
  851. header = _get_global_header(im, info)
  852. return header, used_palette_colors
  853. def getdata(im, offset=(0, 0), **params):
  854. """
  855. Legacy Method
  856. Return a list of strings representing this image.
  857. The first string is a local image header, the rest contains
  858. encoded image data.
  859. To specify duration, add the time in milliseconds,
  860. e.g. ``getdata(im_frame, duration=1000)``
  861. :param im: Image object
  862. :param offset: Tuple of (x, y) pixels. Defaults to (0, 0)
  863. :param \\**params: e.g. duration or other encoder info parameters
  864. :returns: List of bytes containing GIF encoded frame data
  865. """
  866. class Collector:
  867. data = []
  868. def write(self, data):
  869. self.data.append(data)
  870. im.load() # make sure raster data is available
  871. fp = Collector()
  872. _write_frame_data(fp, im, offset, params)
  873. return fp.data
  874. # --------------------------------------------------------------------
  875. # Registry
  876. Image.register_open(GifImageFile.format, GifImageFile, _accept)
  877. Image.register_save(GifImageFile.format, _save)
  878. Image.register_save_all(GifImageFile.format, _save_all)
  879. Image.register_extension(GifImageFile.format, ".gif")
  880. Image.register_mime(GifImageFile.format, "image/gif")
  881. #
  882. # Uncomment the following line if you wish to use NETPBM/PBMPLUS
  883. # instead of the built-in "uncompressed" GIF encoder
  884. # Image.register_save(GifImageFile.format, _save_netpbm)