GifImagePlugin.py 28 KB

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