Skip to content

Instantly share code, notes, and snippets.

@almost
Forked from BigglesZX/gifextract.py
Last active June 17, 2019 14:26
Show Gist options
  • Save almost/d2832d0998ad9dfec2cacef934e7d247 to your computer and use it in GitHub Desktop.
Save almost/d2832d0998ad9dfec2cacef934e7d247 to your computer and use it in GitHub Desktop.
Extract frames from an animated GIF and return them from a generator, correctly handling palettes and frame update modes
import os
from PIL import Image
# Based on https://gist.github.com/BigglesZX/4016539 (but adapted to be a
# generator that yields frames instead of a function that saves out frames)
'''
I searched high and low for solutions to the "extract animated GIF frames in Python"
problem, and after much trial and error came up with the following solution based
on several partial examples around the web (mostly Stack Overflow).
There are two pitfalls that aren't often mentioned when dealing with animated GIFs -
firstly that some files feature per-frame local palettes while some have one global
palette for all frames, and secondly that some GIFs replace the entire image with
each new frame ('full' mode in the code below), and some only update a specific
region ('partial').
This code deals with both those cases by examining the palette and redraw
instructions of each frame. In the latter case this requires a preliminary (usually
partial) iteration of the frames before processing, since the redraw mode needs to
be consistently applied across all frames. I found a couple of examples of
partial-mode GIFs containing the occasional full-frame redraw, which would result
in bad renders of those frames if the mode assessment was only done on a
single-frame basis.
Nov 2012
'''
def analyseImage(im):
'''
Pre-process pass over the image to determine the mode (full or additive).
Necessary as assessing single frames isn't reliable. Need to know the mode
before processing all frames.
'''
results = {
'size': im.size,
'mode': 'full',
}
try:
while True:
if im.tile:
tile = im.tile[0]
update_region = tile[1]
update_region_dimensions = update_region[2:]
if update_region_dimensions != im.size:
results['mode'] = 'partial'
break
im.seek(im.tell() + 1)
except EOFError:
pass
im.seek(0)
return results
def getFrames(im):
'''
Iterate the GIF, extracting each frame.
'''
mode = analyseImage(im)['mode']
p = im.getpalette()
last_frame = im.convert('RGBA')
try:
while True:
'''
If the GIF uses local colour tables, each frame will have its own palette.
If not, we need to apply the global palette to the new frame.
'''
if not im.getpalette():
im.putpalette(p)
new_frame = Image.new('RGBA', im.size)
'''
Is this file a "partial"-mode GIF where frames update a region of a different size to the entire image?
If so, we need to construct the new frame by pasting it on top of the preceding frames.
'''
if mode == 'partial':
new_frame.paste(last_frame)
new_frame.paste(im, (0,0), im.convert('RGBA'))
yield new_frame
last_frame = new_frame
im.seek(im.tell() + 1)
except EOFError:
pass
def processImage(path):
im = Image.open(path)
for (i, frame) in enumerate(getFrames(im)):
print("saving %s frame %d, %s %s" % (path, i, im.size, im.tile))
frame.save('%s-%d.png' % (''.join(os.path.basename(path).split('.')[:-1]), i), 'PNG')
def main():
processImage('test.gif')
if __name__ == "__main__":
main()
@makew0rld
Copy link

makew0rld commented Apr 8, 2017

Great, I prefer it to the original, but I would add a line so I can pass an image path to the getFrames function without having to convert it to an image myself. Maybe im = Image.open(im) at the top of the function?

@JankesJanco
Copy link

the script still not correctly handle all GIFs, see my comment to the original script here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment