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>
92 lines
3 KiB
Python
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()
|