"""pptx_preview.py: 把 .pptx 渲成 PNG 预览图(无头 Chrome),用于**肉眼验收版面**。

quality_check 只查"越界/溢出/配色"等结构问题,看不出"好不好看"。本脚本把每页
按形状坐标还原成 HTML → Chrome 截图 → PNG,让人(或模型用 Read)真看一眼版面层次、
留白、对齐、配色观感。支持本 skill 用到的形状子集:矩形/圆角矩形/渐变块/文本框/图片。

用法:
    python pptx_preview.py <deck.pptx> -o <out_dir> [--pages 1,4,6]
产物:<out_dir>/p01.png p02.png ...(每页一张,2x 超采样)

依赖:本机 Chrome / Edge(同 render_bg.py)。非本 skill 生成的复杂 pptx 可能还原不全。
"""
from __future__ import annotations

import argparse
import html as _html
import subprocess
import tempfile
from pathlib import Path

from pptx import Presentation
from pptx.enum.dml import MSO_FILL, MSO_COLOR_TYPE
from pptx.enum.shapes import MSO_SHAPE_TYPE
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.oxml.ns import qn

from render_bg import find_browser   # 复用浏览器定位

EMU = 914400
PXI = 96  # px per inch


def _rgb(color):
    try:
        if color.type == MSO_COLOR_TYPE.RGB:
            return "#" + str(color.rgb)
    except (AttributeError, TypeError, KeyError, ValueError):
        pass
    return None


def _fill_css(shape):
    """返回 (css背景, 是否有填充)。支持纯色 / 线性渐变。"""
    try:
        f = shape.fill
        if f.type == MSO_FILL.SOLID:
            c = _rgb(f.fore_color)
            return (c, True) if c else (None, False)
        if f.type == MSO_FILL.GRADIENT:
            stops = []
            for gs in f.gradient_stops:
                c = _rgb(gs.color)
                if c:
                    stops.append(f"{c} {int(gs.position * 100)}%")
            if len(stops) >= 2:
                try:
                    ang = f.gradient_angle
                except (AttributeError, ValueError, TypeError):
                    ang = 90
                # pptx 角度→css:0=左→右 即 css 90deg
                return (f"linear-gradient({90 + (ang or 0)}deg,{','.join(stops)})", True)
    except (AttributeError, TypeError, KeyError, ValueError):
        pass
    return (None, False)


def _round_px(shape, w, h):
    try:
        adj = shape.adjustments[0]
        return adj * min(w, h)
    except (IndexError, AttributeError, ValueError, TypeError):
        return 0


def _line(shape):
    try:
        ln = shape.line
        c = _rgb(ln.color)
        if c and ln.width is not None and ln.width > 0:
            return c, max(1, ln.width / EMU * PXI)
    except (AttributeError, TypeError, KeyError, ValueError):
        pass
    return None, 0


def _anchor_flex(tf):
    a = tf.vertical_anchor
    if a == MSO_ANCHOR.MIDDLE:
        return "center"
    if a == MSO_ANCHOR.BOTTOM:
        return "flex-end"
    return "flex-start"


def _align_css(p):
    return {PP_ALIGN.CENTER: "center", PP_ALIGN.RIGHT: "right"}.get(
        p.alignment, "left")


def _para_html(p):
    align = _align_css(p)
    runs = []
    size = 18
    color = "#1F1F1F"
    bold = False
    for r in p.runs:
        if r.font.size:
            size = r.font.size.pt
        c = _rgb(r.font.color)
        if c:
            color = c
        bold = bool(r.font.bold)
        runs.append(_html.escape(r.text or ""))
    txt = "".join(runs) or _html.escape(p.text or "")
    if not txt.strip():
        return ""
    lh = 1.25
    return (f'<div style="text-align:{align};font-size:{size}px;color:{color};'
            f'font-weight:{"700" if bold else "400"};line-height:{lh}">{txt}</div>')


def slide_html(slide, imgdir: Path, idx: int) -> str:
    parts = []
    for s_i, sh in enumerate(slide.shapes):
        try:
            l = sh.left / EMU * PXI
            t = sh.top / EMU * PXI
            w = sh.width / EMU * PXI
            h = sh.height / EMU * PXI
        except (TypeError, AttributeError):
            continue
        base = (f"position:absolute;left:{l:.1f}px;top:{t:.1f}px;"
                f"width:{w:.1f}px;height:{h:.1f}px;box-sizing:border-box;")
        # 图片
        try:
            is_pic = sh.shape_type == MSO_SHAPE_TYPE.PICTURE
        except (AttributeError, ValueError):
            is_pic = False
        if is_pic:
            try:
                blob = sh.image.blob
                ext = sh.image.ext
                fp = imgdir / f"p{idx}_{s_i}.{ext}"
                fp.write_bytes(blob)
                parts.append(f'<img src="{fp.as_uri()}" style="{base}'
                             f'object-fit:cover"/>')
            except (AttributeError, KeyError, ValueError):
                pass
            continue
        # 形状填充 / 圆角 / 描边
        css = base
        bg, has = _fill_css(sh)
        if has:
            css += f"background:{bg};"
        # 椭圆/圆 → 50% 圆角(badge/dot/hub 才显示成圆,不然是方块)
        prst = None
        try:
            g = sh._element.spPr.find(qn("a:prstGeom"))
            prst = g.get("prst") if g is not None else None
        except (AttributeError, TypeError):
            pass
        if prst == "ellipse":
            css += "border-radius:50%;"
        else:
            r = _round_px(sh, w, h)
            if r > 0:
                css += f"border-radius:{r:.1f}px;"
        lc, lw = _line(sh)
        if lc:
            css += f"border:{lw:.1f}px solid {lc};"
        # 文本
        inner = ""
        if sh.has_text_frame and (sh.text_frame.text or "").strip():
            tf = sh.text_frame
            css += ("display:flex;flex-direction:column;padding:2px 6px;"
                    f"justify-content:{_anchor_flex(tf)};")
            inner = "".join(_para_html(p) for p in tf.paragraphs)
        if has or r > 0 or lc or inner:
            parts.append(f'<div style="{css}">{inner}</div>')
    body = "\n".join(parts)
    return (f'<div style="position:relative;width:1280px;height:720px;'
            f'overflow:hidden;background:#fff;font-family:\'Microsoft YaHei\','
            f'Arial,sans-serif">{body}</div>')


def render(html_str: str, out: Path):
    browser = find_browser()
    with tempfile.TemporaryDirectory() as td:
        hp = Path(td) / "s.html"
        hp.write_text(f"<!doctype html><meta charset=utf-8>"
                      f"<style>html,body{{margin:0}}</style>{html_str}",
                      encoding="utf-8")
        subprocess.run([browser, "--headless", "--disable-gpu",
                        "--hide-scrollbars", "--force-device-scale-factor=2",
                        "--window-size=1280,720", f"--screenshot={out}",
                        hp.resolve().as_uri()], check=False,
                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("pptx", type=Path)
    ap.add_argument("-o", "--out", type=Path, required=True)
    ap.add_argument("--pages", default=None, help="如 1,4,6;省略=全部")
    args = ap.parse_args()
    args.out.mkdir(parents=True, exist_ok=True)
    imgdir = args.out / "_img"
    imgdir.mkdir(exist_ok=True)
    prs = Presentation(str(args.pptx))
    want = (set(int(x) for x in args.pages.split(",")) if args.pages else None)
    for i, slide in enumerate(prs.slides, 1):
        if want and i not in want:
            continue
        out = args.out / f"p{i:02d}.png"
        render(slide_html(slide, imgdir, i), out)
        print(f"[ok] {out}" if out.exists() else f"[fail] p{i}")


if __name__ == "__main__":
    main()
