#!/usr/bin/env python3
"""multigrid-poster compose — Pillow 版渲染器

输入：layout.json + N 张 cell 图片 + 标题文字 + caption 文字
输出：1242×1660 PNG（小红书封面标准）

用法：
  python compose.py \
    --layout <SKILL_DIR>/layouts/2x2.json \
    --cells cell_0.png cell_1.png cell_2.png cell_3.png \
    --title-line "26岁一个人创业" \
    --title-line "跨境电商月入10w+" \
    --caption-line "只需要一部手机就可以完成！" \
    --caption-line "跨境人的必备app推荐" \
    --output cover.png

依赖：Pillow (容器自带，无需额外安装)
字体：从 SKILL_DIR/shared/fonts/ 加载（layout.json 里指定）
"""
import argparse
import json
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont


def compose(layout_path: Path, cell_paths: list[Path],
            title_lines: list[str], caption_lines: list[str],
            output_path: Path) -> Path:
    """根据 layout.json 渲染海报。

    layout.json 字段：
      canvas_size       [w, h]
      cells.positions   [[x,y], ...] cell 左上角
      cells.sizes       [[w,h], ...] cell 尺寸
      text_zones.title  {lines: [...], size, color, stroke_w, stroke_color, font}
      text_zones.caption {同上}
    """
    layout = json.loads(layout_path.read_text(encoding="utf-8"))
    # 约定:layout 必须放在 <skill>/layouts/ 下,字体路径相对 <skill>/ 解析
    skill_dir = layout_path.parent.parent

    # 文本行数 vs layout 配置行数:行多了 zip 会静默截断,显式 fail-fast
    # 错误信息用英文 — sys.exit 走 stderr,Windows GBK locale 下中文会乱码
    for zone_key, lines in [("title", title_lines), ("caption", caption_lines)]:
        zone = layout.get("text_zones", {}).get(zone_key)
        if not zone:
            continue
        max_lines = len(zone["lines"])
        if len(lines) > max_lines:
            sys.exit(f"too many {zone_key} lines: got {len(lines)}, layout supports {max_lines}")

    # Canvas
    canvas = Image.new("RGB", tuple(layout["canvas_size"]), "white")

    # Cells
    cell_count = len(layout["cells"]["positions"])
    if len(cell_paths) != cell_count:
        sys.exit(f"cell count mismatch: layout expects {cell_count}, got {len(cell_paths)}")
    for cp, pos, size in zip(cell_paths,
                              layout["cells"]["positions"],
                              layout["cells"]["sizes"]):
        img = Image.open(cp).convert("RGB").resize(tuple(size), Image.LANCZOS)
        canvas.paste(img, tuple(pos))

    draw = ImageDraw.Draw(canvas)

    # Text zones (title + caption 通用绘制)
    for zone_key, lines in [("title", title_lines), ("caption", caption_lines)]:
        if zone_key not in layout["text_zones"]:
            continue
        zone = layout["text_zones"][zone_key]
        font_path = skill_dir / zone["font"]
        font = ImageFont.truetype(str(font_path), zone["size"])
        for text, line_cfg in zip(lines, zone["lines"]):
            if not text:
                continue
            draw.text(
                tuple(line_cfg["position"]), text,
                fill=zone["color"], font=font,
                stroke_width=zone.get("stroke_w", 0),
                stroke_fill=zone.get("stroke_color", zone["color"]),
                anchor=line_cfg.get("anchor", "la"),
            )

    canvas.save(output_path, optimize=True)
    return output_path


def main():
    ap = argparse.ArgumentParser(description=__doc__,
                                  formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--layout", required=True, type=Path,
                    help="layout.json 路径（如 SKILL_DIR/layouts/2x2.json）")
    ap.add_argument("--cells", required=True, nargs="+", type=Path,
                    help="N 张 cell 图片路径，顺序对应 layout.cells.positions")
    ap.add_argument("--title-line", action="append", default=[],
                    help="标题行（可重复）")
    ap.add_argument("--caption-line", action="append", default=[],
                    help="底部 caption 行（可重复）")
    ap.add_argument("--output", required=True, type=Path,
                    help="输出 PNG 路径")
    args = ap.parse_args()

    out = compose(
        layout_path=args.layout,
        cell_paths=args.cells,
        title_lines=args.title_line,
        caption_lines=args.caption_line,
        output_path=args.output,
    )
    print(f"saved {out} ({out.stat().st_size // 1024} KB)")


if __name__ == "__main__":
    main()
