Create Animated GIFs with Python and Pillow
Pillow lets you create, edit, and extract frames from animated GIFs directly in Python. You can also create GIFs from static images, screenshots, or video files using imageio.
Installation
pip install pillow imageio imageio-ffmpeg
Create a GIF from Images
from PIL import Image
from pathlib import Path
def images_to_gif(image_paths, output_gif, duration_ms=200, loop=0):
frames = [Image.open(p).convert("RGBA") for p in image_paths]
if not frames:
print("No images provided")
return
frames[0].save(
output_gif,
format="GIF",
save_all=True,
append_images=frames[1:],
duration=duration_ms, # ms per frame
loop=loop, # 0 = infinite
optimize=True
)
size = Path(output_gif).stat().st_size // 1024
print(f"GIF created: {output_gif} | {len(frames)} frames | {size} KB")
images_to_gif(
["frame_01.png", "frame_02.png", "frame_03.png", "frame_04.png"],
"animation.gif",
duration_ms=150
)
Create GIF from a Folder of Images
from PIL import Image
from pathlib import Path
def folder_to_gif(folder, output_gif, duration_ms=200, width=None):
paths = sorted(Path(folder).glob("*.png")) + sorted(Path(folder).glob("*.jpg"))
if not paths:
print("No images found")
return
frames = []
for path in paths:
img = Image.open(path).convert("RGBA")
if width:
ratio = width / img.width
img = img.resize((width, int(img.height * ratio)), Image.LANCZOS)
frames.append(img)
frames[0].save(
output_gif,
save_all=True,
append_images=frames[1:],
duration=duration_ms,
loop=0,
optimize=True
)
print(f"GIF: {output_gif} | {len(frames)} frames | {Path(output_gif).stat().st_size//1024} KB")
folder_to_gif("screenshots/", "demo.gif", duration_ms=100, width=800)
Extract Frames from a GIF
from PIL import Image
from pathlib import Path
def extract_gif_frames(gif_path, output_dir="frames"):
Path(output_dir).mkdir(exist_ok=True)
gif = Image.open(gif_path)
n = 0
try:
while True:
frame = gif.copy().convert("RGBA")
frame.save(f"{output_dir}/frame_{n:04d}.png")
print(f" Frame {n}: {frame.size}")
n += 1
gif.seek(gif.tell() + 1)
except EOFError:
pass
print(f"Extracted {n} frames to {output_dir}/")
return n
extract_gif_frames("animation.gif")
Create Animated Text GIF
from PIL import Image, ImageDraw, ImageFont
def countdown_gif(count_to, output, width=300, height=150, duration=500):
frames = []
try:
font = ImageFont.truetype("arial.ttf", 60)
except:
font = ImageFont.load_default()
for i in range(count_to + 1):
img = Image.new("RGBA", (width, height), (30, 30, 30, 255))
draw = ImageDraw.Draw(img)
text = str(i)
bbox = draw.textbbox((0,0), text, font=font)
x = (width - (bbox[2]-bbox[0])) // 2
y = (height - (bbox[3]-bbox[1])) // 2
draw.text((x, y), text, font=font, fill=(100, 200, 255, 255))
frames.append(img)
frames[0].save(
output,
save_all=True,
append_images=frames[1:],
duration=duration,
loop=0
)
print(f"Countdown GIF: {output}")
countdown_gif(10, "countdown.gif", duration=400)
Video to GIF with imageio
import imageio
from PIL import Image
def video_to_gif(video_path, output_gif, fps_out=10, width=480, start_sec=0, duration_sec=5):
reader = imageio.get_reader(video_path)
fps_in = reader.get_meta_data().get("fps", 30)
step = max(1, int(fps_in / fps_out))
start_f = int(fps_in * start_sec)
end_f = start_f + int(fps_in * duration_sec)
frames = []
for i, frame in enumerate(reader):
if i < start_f: continue
if i >= end_f: break
if (i - start_f) % step == 0:
img = Image.fromarray(frame).convert("RGBA")
if width:
ratio = width / img.width
img = img.resize((width, int(img.height * ratio)), Image.LANCZOS)
frames.append(img)
reader.close()
frames[0].save(
output_gif,
save_all=True,
append_images=frames[1:],
duration=int(1000 / fps_out),
loop=0,
optimize=True
)
import os
print(f"GIF: {output_gif} | {len(frames)} frames | {os.path.getsize(output_gif)//1024} KB")
video_to_gif("clip.mp4", "clip.gif", fps_out=12, width=480, duration_sec=4)
Optimize GIF File Size
from PIL import Image
import os
def optimize_gif(input_gif, output_gif, colors=128, max_width=600):
gif = Image.open(input_gif)
frames = []
durations = []
try:
while True:
frame = gif.copy().convert("P", palette=Image.ADAPTIVE, colors=colors)
if max_width and gif.width > max_width:
ratio = max_width / gif.width
frame = frame.resize((max_width, int(gif.height * ratio)), Image.LANCZOS)
frames.append(frame)
durations.append(gif.info.get("duration", 100))
gif.seek(gif.tell() + 1)
except EOFError:
pass
frames[0].save(output_gif, save_all=True, append_images=frames[1:],
duration=durations, loop=0, optimize=True)
orig = os.path.getsize(input_gif)
dest = os.path.getsize(output_gif)
print(f"Optimized: {orig//1024}KB -> {dest//1024}KB (saved {100-dest*100//orig:.0f}%)")
optimize_gif("large.gif", "compressed.gif", colors=64)
Additional Resource
For converting GIFs to MP4, WebM or PNG without any coding, use KaijuConverter — free and no registration required.
Related conversions
Most teams that read this guide convert images in one of these directions: