-
-
Save Fr6jDJF/b5e5775e57c5babb89136fc1752e6822 to your computer and use it in GitHub Desktop.
Generator to iterate over frames in an animated GIF, correctly handling palettes and frame update modes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
from PIL import Image | |
''' | |
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(path): | |
''' | |
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. | |
''' | |
im = Image.open(path) | |
results = { | |
'size': im.size, | |
'mode': 'full', | |
} | |
try: | |
while True: | |
if im.tile: | |
tile = im.tile[0] | |
update_region = tile[1] | |
if update_region != (0, 0,) + im.size: | |
results['mode'] = 'partial' | |
break | |
im.seek(im.tell() + 1) | |
except EOFError: | |
pass | |
return results | |
def imageFrames(path, pastePrevious=True): | |
''' | |
Iterate the GIF, extracting each frame. | |
''' | |
mode = analyseImage(path)['mode'] | |
im = Image.open(path) | |
i = 0 | |
p = im.getpalette() | |
last_frame = im.convert('RGBA') | |
try: | |
while True: | |
print "saving %s (%s) frame %d, %s %s" % (path, mode, im.tell(), im.size, im.tile) | |
''' We have to set our potential crop size here because putpalette() erases tile data ''' | |
if im.tile: | |
box = im.tile[0][1] | |
else: | |
box = (0,0,)+im.size | |
''' | |
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) | |
''' | |
If the user wants output the current appearance (rather than the actual current frame data), we | |
paste the previous frames. Defaults to true. | |
''' | |
if pastePrevious: | |
new_frame.paste(last_frame) | |
''' | |
In the partial case we have to crop because PIL leaves data from previous frames in the background | |
outside the bounding box. (test out the else case to see) | |
''' | |
if mode == 'partial': | |
new_frame.paste(im.crop(box), box[:2], im.convert('RGBA').crop(box)) | |
else: | |
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 saveAllFrames(path, pastePrevious=True): | |
for (i,f) in enumerate(imageFrames(path, pastePrevious)): | |
f.save('%s-%d.png' % (''.join(os.path.basename(path).split('.')[:-1]), i), 'PNG') | |
def main(): | |
saveAllFrames('foo.gif') | |
saveAllFrames('bar.gif') | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment