#!/usr/bin/env python3
"""Merge multiple layer images into one transparent PNG."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from PIL import Image
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}
class MergeError(Exception):
pass
def load_layer(path: Path) -> Image.Image:
if not path.is_file():
raise MergeError(f"Image not found: {path}")
if path.suffix.lower() not in IMAGE_EXTENSIONS:
raise MergeError(f"Unsupported image format: {path.name}")
try:
return Image.open(path).convert("RGBA")
except OSError as exc:
raise MergeError(f"Failed to open image '{path}': {exc}") from exc
def merge_layers(paths: list[Path]) -> Image.Image:
if len(paths) < 2:
raise MergeError("At least 2 images are required")
layers = [load_layer(path) for path in paths]
width, height = layers[0].size
for index, layer in enumerate(layers[1:], start=2):
if layer.size != (width, height):
raise MergeError(
f"Image size mismatch: '{paths[0]}' is {width}x{height}, "
f"but '{paths[index - 1]}' is {layer.width}x{layer.height}"
)
merged = layers[0].copy()
for layer in layers[1:]:
merged = Image.alpha_composite(merged, layer)
return merged
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Merge multiple layer images into one PNG with transparency.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Layers are merged in order: the first image is the bottom layer,\n"
"the last image is the top layer. All images must have the same size.\n"
"\n"
"Example:\n"
" python img_merge.py images/1.png images/2.png images/3.png -o output/merged.png"
),
)
parser.add_argument(
"images",
nargs="+",
help="Layer image paths, from bottom to top",
)
parser.add_argument(
"-o",
"--output",
default="merged.png",
help="Output PNG path (default: merged.png)",
)
return parser
def main() -> int:
parser = build_parser()
args = parser.parse_args()
input_paths = [Path(path).expanduser().resolve() for path in args.images]
output_path = Path(args.output).expanduser().resolve()
try:
merged = merge_layers(input_paths)
output_path.parent.mkdir(parents=True, exist_ok=True)
merged.save(output_path, format="PNG")
print(
f"Merged {len(input_paths)} layers -> {output_path} "
f"({merged.width}x{merged.height})"
)
return 0
except MergeError as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())