Multi-frame images and animation

Pillow · Advanced · 12 min

Without frame handling you silently process only the first frame of animations and multi-page scans.

Some files hold several frames: animated GIF, APNG and WebP, multi-page TIFF, ICO with several sizes. Image.open positions you on frame 0; most operations then see only that frame.

Check getattr(im, "n_frames", 1) and getattr(im, "is_animated", False). Move with im.seek(i) and read the position with im.tell(). Seeking past the end raises EOFError. The simplest loop uses `ImageSequence.Iterator(im)` (or ImageSequence.all_frames(im, func)).

Each frame object is the same Image re-pointed at a new position, so .copy() frames you want to keep, and .convert("RGBA") them because GIF frames are palette images whose palettes and transparency can differ per frame.

Timing per frame is in im.info["duration"] (milliseconds), looping in im.info["loop"] (0 = forever).

To write an animation, save the first frame with save_all=True and pass the rest:

frames[0].save("out.gif", save_all=True, append_images=frames[1:],
               duration=80, loop=0)

duration can be a list with one value per frame. GIF is limited to 256 colours per frame, so photos band and dither; animated WebP (.webp) or APNG (.png) give better quality. The same save_all pattern writes multi-page TIFFs and PDFs.

Example

from PIL import Image, ImageDraw, ImageSequence

frames = []
for i in range(5):
    f = Image.new("RGB", (60, 40), "white")
    ImageDraw.Draw(f).ellipse([i * 10, 10, i * 10 + 20, 30], fill="red")
    frames.append(f)
frames[0].save("ball.gif", save_all=True, append_images=frames[1:],
               duration=100, loop=0)

im = Image.open("ball.gif")
print(im.n_frames, im.is_animated, im.info["duration"])   # 5 True 100

kept = [fr.copy().convert("RGB") for fr in ImageSequence.Iterator(im)]
print(len(kept), kept[4].getpixel((50, 20)))              # 5 (255, 0, 0)

im.seek(2)
print(im.tell())                                          # 2
try:
    im.seek(99)
except EOFError:
    print("no frame 99")

frames[0].save("pages.pdf", save_all=True, append_images=frames[1:])  # 5-page PDF

Remember

  • Check n_frames / is_animated; open() only shows frame 0
  • Iterate with ImageSequence.Iterator and .copy() frames you keep
  • Save animations with save_all=True, append_images, duration and loop
  • Prefer animated WebP or APNG over GIF for photo quality

Watch out for

  • Resizing an animated GIF with a single resize() call and saving only its first frame.

Check yourself

  1. You loop `for fr in ImageSequence.Iterator(im): frames.append(fr)` and all stored frames look like the last one. Why?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pillow.readthedocs.io/en/stable/reference/ImageSequence.html