PythonIntermediate · Windows, macOS, or Linux with Python 3.12+

Build a GIF from equal-size images

Combine an explicit ordered image list into a looping GIF.

Before you begin

  • Python 3.12 or newer
  • Pillow installed in the same Python environment
What this script changesCreates new image files or a new frame directory. Inputs and outputs are limited to 20 million pixels per image; animated tasks also cap frame count. Ordinary transforms process the first frame and normalize EXIF orientation. Color profiles and original metadata are not preserved reliably; use source copies for archival work.

Set up your example

  1. Save the complete code as image-gif-build.py in a working folder.
  2. Save the provided sample files in that same folder; run commands from that folder.
  3. Install Pillow: python -m pip install Pillow
  4. Generate the original fixtures once: python "make_samples.py". Choose an empty folder so the fixture files do not exist.

Sample files in this download:

  • make_samples.py

Download the ZIP, extract it, and open a terminal inside the extracted folder. The source file is image-gif-build.py.

Run the script

Run command
python "image-gif-build.py" --input "sample.png" --extra "second.png" --duration 200 --output "animation.gif"

If your system uses python3 instead of python, use that command for Python examples. Keep paths containing spaces in quotes. Required inputs and optional settings are described in the code and setup steps.

What to expect

Combine an explicit ordered image list into a looping GIF. The fixtures alternate every approximately 200 milliseconds.

The complete script

image-gif-build.py
#!/usr/bin/env python3
'Build a GIF from equal-size images'
import argparse
import json
from pathlib import Path

def write_json(path, value):
    data = json.dumps(value, indent=2, ensure_ascii=False, allow_nan=False) + "\n"
    with Path(path).open("x", encoding="utf-8", newline="") as handle:
        handle.write(data)

import math

try:
    from PIL import Image, ImageOps, ImageEnhance, ImageFilter, ImageDraw, ImageFont, ImageChops
except ImportError:
    raise SystemExit("Install Pillow first: python -m pip install Pillow")
MAX_PIXELS=20_000_000
def check_size(size):
    if min(size)<1 or size[0]*size[1]>MAX_PIXELS:
        raise ValueError("Image dimensions must be positive and at most 20 million pixels.")
def read_image(path):
    with Image.open(path) as original:
        check_size(original.size)
        return ImageOps.exif_transpose(original).convert("RGBA")
def save_image(path,image,format="PNG"):
    check_size(image.size)
    with Path(path).open("xb") as handle:
        image.save(handle,format=format)

def main():
    parser = argparse.ArgumentParser(description='Build a GIF from equal-size images')
    parser.add_argument("--input",required=True,help="Local source image")
    parser.add_argument("--output",required=True,help="New output file or directory")
    parser.add_argument("--extra",nargs="+",required=True)
    parser.add_argument("--duration",type=int,default=200)
    args = parser.parse_args()
    paths=[args.input]+args.extra
    if len(paths)>100 or not 20<=args.duration<=10000: raise ValueError("Use <=100 frames and duration 20..10000 ms.")
    total=0
    for path in paths:
        with Image.open(path) as preview:
            check_size(preview.size)
            total+=preview.width*preview.height
    if total>40_000_000: raise ValueError("Total animation pixels exceed 40 million.")
    frames=[read_image(p).convert("RGB") for p in paths]
    if len({im.size for im in frames})!=1: raise ValueError("All frames must have equal dimensions.")
    if sum(im.width*im.height for im in frames)>40_000_000: raise ValueError("Total animation pixels exceed 40 million.")
    with Path(args.output).open("xb") as handle:
        frames[0].save(handle,format="GIF",save_all=True,append_images=frames[1:],duration=args.duration,loop=0,disposal=2)

if __name__ == "__main__":
    try:
        main()
    except (OSError, ValueError, KeyError, TypeError, ArithmeticError, IndexError, LookupError) as exc:
        raise SystemExit(str(exc))

Open source as plain text · Download source and sample files

How it works

  1. Pillow reads the local image; the source is left unchanged and existing destinations are refused.
  2. Every frame must have the same size; GIF palette reduction and timing granularity can change color and exact timing. Alpha is dropped.
  3. The fixtures alternate every approximately 200 milliseconds.

Make it your own

--duration

Use --duration 500 for half-second frames.

Adapt the processing

Edit main's loop=0 to loop=1 if one repeat is desired.

Change one setting at a time, run it again on sample data, and compare the output. The customization guide walks through this process.

If something goes wrong

Frame dimensions differ.

Resize or pad each frame to the same dimensions before assembly.

The destination already exists.

Choose a new --output path. The script deliberately refuses to replace an existing result.

Reference & next steps

These official references explain the underlying tools. The Scriptage example and guide are original instructional content. See verification notes for what has been checked and the limits of those checks.

Copies a template for your own notes. It does not send anything.