#!/usr/bin/env python3
"""
Trim transparent padding around PNG images.

Usage:
  python3 trim_transparent_padding.py input.png [output.png]

Options:
  --threshold N   Alpha threshold (0-255, default: 0).
                   Pixels with alpha <= N are considered transparent.
  --pad N         Extra padding (in pixels) to keep around trimmed content (default: 0).
  --in-place      Overwrite the input file instead of writing to a new file.

Behavior:
  - Detects the minimal bounding box containing all pixels whose alpha > threshold.
  - Crops to that box (plus optional padding) and saves as PNG.
  - If the image has no alpha channel or no non-transparent pixels are found, the image is left unchanged.
"""

from __future__ import annotations

import argparse
from pathlib import Path
from typing import Optional, Tuple

try:
    from PIL import Image  # type: ignore
except Exception as e:
    raise SystemExit("Pillow is required. Install with: pip install --upgrade pillow")


def compute_bbox_with_alpha(img: Image.Image, threshold: int) -> Optional[Tuple[int, int, int, int]]:
    """
    Return (left, upper, right, lower) bbox of pixels with alpha > threshold.
    If no alpha channel or no non-transparent pixels, return None.
    """
    if img.mode in ("LA", "RGBA"):
        alpha = img.getchannel("A")
    elif img.mode == "P":
        # Convert palette (could include transparency) to RGBA
        img = img.convert("RGBA")
        alpha = img.getchannel("A")
    else:
        # No alpha channel
        return None

    # Build a binary mask: 255 for alpha > threshold else 0
    mask = alpha.point(lambda p: 255 if p > threshold else 0, mode='L')
    bbox = mask.getbbox()
    return bbox  # None if fully transparent (or fully <= threshold)


def trim_png(src: Path, dst: Path, threshold: int, pad: int, in_place: bool) -> bool:
    try:
        with Image.open(src) as im:
            im.load()
            bbox = compute_bbox_with_alpha(im, threshold)
            if not bbox:
                # Nothing to trim; copy or overwrite
                if in_place or src.resolve() == dst.resolve():
                    # Overwrite: just save a copy (may normalize format)
                    im.save(src, format="PNG")
                else:
                    im.save(dst, format="PNG")
                print(f"[INFO] {src.name}: no trim needed")
                return True

            left, upper, right, lower = bbox
            # Apply padding while clamping to image bounds
            left = max(0, left - pad)
            upper = max(0, upper - pad)
            right = min(im.width, right + pad)
            lower = min(im.height, lower + pad)
            if right <= left or lower <= upper:
                print(f"[WARN] {src.name}: invalid crop box after padding; skipping")
                return False

            cropped = im.crop((left, upper, right, lower))
            out_path = src if in_place else dst
            cropped.save(out_path, format="PNG")
            print(f"[OK] {src.name} -> {out_path.name}  ({im.width}x{im.height} -> {cropped.width}x{cropped.height})")
            return True
    except Exception as e:
        print(f"[ERROR] Failed to process {src}: {e}")
        return False


def main() -> int:
    ap = argparse.ArgumentParser(description="Trim transparent padding of PNGs")
    ap.add_argument("input", help="Input PNG file")
    ap.add_argument("output", nargs="?", help="Output PNG file (default: <input>.trim.png)")
    ap.add_argument("--threshold", type=int, default=0, help="Alpha threshold (0-255), default: 0")
    ap.add_argument("--pad", type=int, default=0, help="Extra padding in pixels, default: 0")
    ap.add_argument("--in-place", action="store_true", help="Overwrite the input file")
    args = ap.parse_args()

    src = Path(args.input)
    if not src.exists():
        print(f"[ERROR] Input not found: {src}")
        return 2
    if src.suffix.lower() != ".png":
        print(f"[WARN] Input is not .png: {src.name}")

    if args.in_place:
        dst = src
    else:
        if args.output:
            dst = Path(args.output)
        else:
            dst = src.with_name(src.stem + ".trim.png")

    if args.threshold < 0 or args.threshold > 255:
        print("[ERROR] threshold must be 0..255")
        return 2
    if args.pad < 0:
        print("[ERROR] pad must be >= 0")
        return 2

    ok = trim_png(src, dst, args.threshold, args.pad, args.in_place)
    return 0 if ok else 1


if __name__ == "__main__":
    raise SystemExit(main())

