For the benefit of people who don't want to rely on a third party module like Pillow, here is an entirely python 2 and 3 native solution:
import sys
is_py2 = sys.version_info[0] == 2
def is_animated_gif(image_path):
"""Return true if image is an animated gif
primarily used this great deep dive into the structure of an animated gif
to figure out how to parse it:
http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
Other links that also helped:
https://en.wikipedia.org/wiki/GIF#Animated_GIF
https://www.w3.org/Graphics/GIF/spec-gif89a.txt
https://stackoverflow.com/a/1412644/5006
:param image_path: string, assumed to be a path to a gif file
:returns: boolean, True if the image is an animated gif
"""
ret = False
image_count = 0
def skip_color_table(fp, packed_byte):
"""this will fp.seek() completely passed the color table"""
if is_py2:
packed_byte = int(packed_byte.encode("hex"), 16)
has_gct = (packed_byte & 0b10000000) >> 7
gct_size = packed_byte & 0b00000111
if has_gct:
global_color_table = fp.read(3 * pow(2, gct_size + 1))
def skip_image_data(fp):
"""skips the image data, which is basically just a series of sub blocks
with the addition of the lzw minimum code to decompress the file data"""
lzw_minimum_code_size = fp.read(1)
skip_sub_blocks(fp)
def skip_sub_blocks(fp):
"""skips over the sub blocks
the first byte of the sub block tells you how big that sub block is, then
you read those, then read the next byte, which will tell you how big
the next sub block is, you keep doing this until you get a sub block
size of zero"""
num_sub_blocks = ord(fp.read(1))
while num_sub_blocks != 0x00:
fp.read(num_sub_blocks)
num_sub_blocks = ord(fp.read(1))
with open(image_path, "rb") as fp:
header = fp.read(6)
if header == b"GIF89a": # GIF87a doesn't support animation
logical_screen_descriptor = fp.read(7)
skip_color_table(fp, logical_screen_descriptor[4])
b = ord(fp.read(1))
while b != 0x3B: # 3B is always the last byte in the gif
if b == 0x21: # 21 is the extension block byte
b = ord(fp.read(1))
if b == 0xF9: # graphic control extension
block_size = ord(fp.read(1))
fp.read(block_size)
b = ord(fp.read(1))
if b != 0x00:
raise ValueError("GCT should end with 0x00")
elif b == 0xFF: # application extension
block_size = ord(fp.read(1))
fp.read(block_size)
skip_sub_blocks(fp)
elif b == 0x01: # plain text extension
block_size = ord(fp.read(1))
fp.read(block_size)
skip_sub_blocks(fp)
elif b == 0xFE: # comment extension
skip_sub_blocks(fp)
elif b == 0x2C: # Image descriptor
# if we've seen more than one image it's animated
image_count += 1
if image_count > 1:
ret = True
break
# total size is 10 bytes, we already have the first byte so
# let's grab the other 9 bytes
image_descriptor = fp.read(9)
skip_color_table(fp, image_descriptor[-1])
skip_image_data(fp)
b = ord(fp.read(1))
return ret
The is_animated_gif()
function works by skipping over all the extensions and color information and counting the actual images in the file, when it finds the second image it can safely assume the gif is animated and its work is done.
It doesn't rely on any shortcuts like checking for the existence of an application extension block because it didn't seem like those were required for the gif to be animated, and I didn't want to assume anything.