图像-图层合并

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