Pillow
| Language | Python 3 |
|---|---|
| License | MIT-CMU (permissive, HPND-style) |
| Repository | github.com/python-pillow/Pillow |
| Documentation | GIF in Pillow's format docs |
| Depends on | nothing for GIF (built-in LZW codec) |
| Read / write | yes / yes; animation, loop count, durations, disposal, transparency, comments, interlacing |
Pillow opens a GIF as an image positioned on the first frame. im.n_frames gives the frame count, im.seek(i) or ImageSequence.Iterator(im) moves through frames. Pillow composites each frame with the previous ones following the disposal method, so every frame you get is the full canvas; frames after the first are returned in RGB or RGBA mode (configurable with GifImagePlugin.LOADING_STRATEGY). im.info holds duration (ms) of the current frame, loop, transparency, background and comment.
Saving with save_all=True writes an animation from a list of images; Pillow quantizes RGB frames to 256 colors and, with optimize=True, crops frames to the changed area.
Installation#
pip install pillow
Basic usage: read frames and durations#
from PIL import Image, ImageSequence
with Image.open("anim.gif") as im:
print(im.size, im.n_frames, "frames, loop =", im.info.get("loop")) # loop 0 = forever
for i, frame in enumerate(ImageSequence.Iterator(im)):
print(i, frame.mode, frame.info.get("duration"), "ms")
How to create an animated GIF#
from PIL import Image, ImageDraw
frames = []
for i in range(20):
im = Image.new("RGB", (120, 120), "white")
ImageDraw.Draw(im).ellipse((i * 5, 40, i * 5 + 30, 70), fill="red")
frames.append(im)
frames[0].save(
"anim.gif",
save_all=True,
append_images=frames[1:],
duration=50, # ms per frame, or a list with one value per frame
loop=0, # 0 = loop forever; omit to play once
disposal=2, # restore to background between frames
optimize=True,
)
How to resize an animated GIF#
from PIL import Image, ImageSequence
with Image.open("anim.gif") as im:
durations = []
frames = []
for frame in ImageSequence.Iterator(im):
durations.append(frame.info.get("duration", 100))
frames.append(frame.convert("RGBA").resize((im.width // 2, im.height // 2), Image.Resampling.LANCZOS))
loop = im.info.get("loop", 0)
frames[0].save("half.gif", save_all=True, append_images=frames[1:],
duration=durations, loop=loop, disposal=2)
How to extract GIF frames as PNG#
from PIL import Image, ImageSequence
with Image.open("anim.gif") as im:
for i, frame in enumerate(ImageSequence.Iterator(im)):
frame.convert("RGBA").save(f"frame-{i:03}.png")
How to control the palette and dithering#
Quantize frames yourself to choose the number of colors and the dithering:
from PIL import Image
im = Image.open("photo.png").convert("RGB")
pal = im.quantize(colors=64, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE)
pal.save("photo.gif", optimize=True)
dither=Image.Dither.FLOYDSTEINBERG (the default) looks smoother but compresses worse.
Tips#
Image.open("anim.gif").convert("RGB")gives only the first frame; useImageSequenceorseekfor the rest.- Pillow merges identical consecutive frames when saving and adds their durations; pass distinct frames if you need exact frame counts.
- To write a GIF with transparency, use
RGBAframes or passtransparency=<palette index>forPmode images.