Creating animated GIFs with Pillow

By Martin McBride, 2021-11-09
Tags: image processing animation gif animated gif
Categories: pillow


GIF format images were a common feature in the early days of the web. They aren't used so much these days because PNG images are better than GIFs in almost every way.

You might also be interested in this article that shows how to create animated GIFs using the generativepy library and vector imaging.

There is still one thing that GIFs can do that most other image formats can't - they can create animations. They aren't a replacement for video, but they can be useful for small, short, low frame rate animations. They are easy to create and will run in any browser - just use an animated GIF file in the same way you would use an ordinary image, and the browser will run it automatically.

Modern video formats such as MP4 etc use sophisticated compression algorithms to reduce the size of the video file. Animated GIFs just use a simple image compression algorithm. If you tried to store a long, full-screen video in GIF format, the file would be huge.

What is a GIF animation

A GIF file normally stores a single image, but the format allows multiple images to be stored in one file. The format also has parameters that can be used to sequence the images - that is, to display each image for a short time then replace it with the next one.

This technique was used a lot in the early days of the web to make animated banner ads - usually gaudy, flashing monstrosities that distracted you from the page you were trying to read. Most sites don't do stuff like that anymore.

But if you include 20 or so time-lapse images, and set a duration of 0.1 seconds or less, you can get a reasonable 2 second video clip or animated diagram.

Creating an animated GIF with Pillow

We will assume you know how the basics of reading and writing image files in Pillow. Assuming im is a previously opened Pillow Image, here is how you would save a single image as a GIF:

im.save('test.gif')

If we want to create an animated GIF, we need a group of images that make up the individual frames. Assuming images[] is a list of opened Pillow Image objects, here is how you would save it:

images[0].save('anitest.gif',
               save_all=True,
               append_images=images[1:],
               duration=100,
               loop=0)

The way Pillow works is that we save the first image in the group, and add a list of the extra images as a parameter in the save call. There are several extra parameters:

  • save_all tells Pillow that we want to save multiple images in the same output file.
  • append_images is a list of the extra images to save. Since the first image (images[0]) is used to make the call, the extra images consist of images[1] and beyond. We use a slice to select those images from the full list.
  • duration controls how long each frame will be displayed for, in milliseconds. It is the inverse of the frame rate. Setting it to 100 milliseconds gives 10 frames per second. You can decrease the duration to give a smoother animation.
  • loop controls how many times the animation repeats. The default, 1, means that the animation will play once and then stop (displaying the last frame). 2 means it will repeat twice before stopping, and so on. A value of 0 means that the animation will repeat forever.

Example - a moving circle

Here is a really simple example, a moving circle. We will create this in 2 steps:

  • Create a set of single image GIF files, each containing 1 frame of the animation
  • Combine all the individual frames into a single image

To start with we will create a list of file names for our temporary single iamge gifs. We will allow 20 frames (2 sec at 10 frames per second), and call the files img00.gif, img01.gif etc:

names = ['img{:02d}.gif'.format(i) for i in range(20)]

Now we will create our frames. We start with a blank green background image (that we will reuse by creating a copy each time). We will draw a red circle on the green background, at position (pos, pos). The value of pos will be updated every time through the loop, so the circle moves on each frame:

# Create the individual frames as png images
im = Image.new("RGB", (200, 200), 'green')

pos = 0
for n in names:
    frame = im.copy()
    draw = ImageDraw.Draw(frame)
    draw.ellipse((pos, pos, 50+pos, 50+pos),
                 'red')
    frame.save(n)
    pos += 10

Here is how the code works. After creating the background image, we loop over all the filenames in the list (we previously created 20 filenames):

  • take a copy of the background image and store it in the variable frame
  • create a Draw item, attached to the frame images, using ImageDraw.Draw
  • draw an ellipse with the Draw item - it will appear on the frame image
  • save the image as a file called imgxx.gif
  • increment pos to move the image for the next time around the loop

You should be able to look at the individual images and see the red circle in a different place each time.

Finally, we create the animated GIF:

# Open all the frames
images = []

for n in names:
    frame = Image.open(n)
    images.append(frame)

# Save the frames as an animated GIF
images[0].save('anicircle.gif',
               save_all=True,
               append_images=images[1:],
               duration=100,
               loop=0)

We create an empty list of images. Then we loop over all the file names for a second time, this time opening the newly created GIF file. This creates an Image object that we append to the image list.

Finally, we save the animated GIF file as described earlier. Here it is:

Things to try

Pillow doesn't have a lot of great drawing tools, you might be better with something like Pycairo to create your initial GIFs, then use Pillow to create the animated GIF.

If you found this article useful, you might be interested in the book NumPy Recipes or other books by the same author.

Join the PythonInformer Newsletter

Sign up using this form to receive an email when new content is added:

Popular tags

2d arrays abstract data type alignment and angle animation arc array arrays bar chart bar style behavioural pattern bezier curve built-in function callable object chain circle classes clipping close closure cmyk colour combinations comparison operator comprehension context context manager conversion count creational pattern data science data types decorator design pattern device space dictionary drawing duck typing efficiency ellipse else encryption enumerate fill filter font font style for loop formula function function composition function plot functools game development generativepy tutorial generator geometry gif global variable gradient greyscale higher order function hsl html image image processing imagesurface immutable object in operator index inner function input installing iter iterable iterator itertools join l system lambda function latex len lerp line line plot line style linear gradient linspace list list comprehension logical operator lru_cache magic method mandelbrot mandelbrot set map marker style matplotlib monad mutability named parameter numeric python numpy object open operator optimisation optional parameter or pandas partial application path pattern permutations pie chart pil pillow polygon pong positional parameter print product programming paradigms programming techniques pure function python standard library radial gradient range recipes rectangle recursion reduce regular polygon repeat rgb rotation roundrect scaling scatter plot scipy sector segment sequence setup shape singleton slice slicing sound spirograph sprite square str stream string stroke structural pattern subpath symmetric encryption template tex text text metrics tinkerbell fractal transform translation transparency triangle truthy value tuple turtle unpacking user space vectorisation webserver website while loop zip zip_longest