#!/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()