gif-encoder/encoder.py
DJP cc14d77257 Initial commit — GIF Encoder tool
PNG-to-animated-GIF batch converter with PHP frontend, Python/Pillow backend,
and JSON API for Figma plugin integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 12:05:09 -05:00

92 lines
3 KiB
Python

#!/usr/bin/env python3
"""PNG to Animated GIF encoder using Pillow."""
import argparse
import json
import sys
from pathlib import Path
from PIL import Image
def create_gif(input_files, output_path, delays=None, quality=256, loop=True):
"""Create an animated GIF from a list of PNG files.
Args:
input_files: List of PNG file paths
output_path: Output GIF file path
delays: List of delays per frame in ms, or single int for uniform delay
quality: Color quantization quality (2-256 colors)
loop: True for infinite loop, False for play once
"""
if not input_files:
return {"success": False, "error": "No input files provided"}
frames = []
for f in input_files:
img = Image.open(f)
if img.mode == "RGBA":
bg = Image.new("RGB", img.size, (255, 255, 255))
bg.paste(img, mask=img.split()[3])
img = bg
elif img.mode != "RGB":
img = img.convert("RGB")
frames.append(img)
if not frames:
return {"success": False, "error": "No valid frames loaded"}
# Resolve delays: per-frame list or single value
if isinstance(delays, list):
# Pad or trim to match frame count
while len(delays) < len(frames):
delays.append(delays[-1] if delays else 500)
delays = delays[:len(frames)]
elif isinstance(delays, int):
delays = [delays] * len(frames)
else:
delays = [500] * len(frames)
colors = max(2, min(256, quality))
quantized = [frame.quantize(colors=colors, method=Image.Quantize.MEDIANCUT) for frame in frames]
output = Path(output_path)
output.parent.mkdir(parents=True, exist_ok=True)
# loop=0 means infinite, loop=1 means play once (no repeat)
loop_count = 0 if loop else 1
quantized[0].save(
str(output),
save_all=True,
append_images=quantized[1:],
duration=delays,
loop=loop_count,
optimize=True,
)
return {"success": True, "output": str(output), "frames": len(frames)}
def main():
parser = argparse.ArgumentParser(description="PNG to Animated GIF encoder")
parser.add_argument("--input", nargs="+", required=True, help="Input PNG files")
parser.add_argument("--output", required=True, help="Output GIF path")
parser.add_argument("--delays", help="Comma-separated delays per frame in ms (e.g. 500,200,800)")
parser.add_argument("--delay", type=int, default=500, help="Uniform delay for all frames (ms)")
parser.add_argument("--quality", type=int, default=256, help="Color count (2-256)")
parser.add_argument("--no-loop", action="store_true", help="Play once instead of looping")
args = parser.parse_args()
if args.delays:
delays = [int(d.strip()) for d in args.delays.split(",")]
else:
delays = args.delay
result = create_gif(args.input, args.output, delays, args.quality, loop=not args.no_loop)
print(json.dumps(result))
sys.exit(0 if result["success"] else 1)
if __name__ == "__main__":
main()