zcbot/skills/ppt/scripts/render_bg.py

136 lines
5.4 KiB
Python

"""render_bg.py: 用无头 Chrome/Edge 把主题化 HTML 背景渲成高清 PNG。
混合方案专用 —— 封面/章节页:先用本脚本渲一张杂志级背景图,build_deck 里
`P.add_picture_bg(slide, png)` 整页铺,再叠原生可编辑文字。背景不可改但文字能改,
是 editable 前提下能拿到的最高观感(DrawingML 渐变做不出 mesh 渐变 + 模糊光晕)。
用法:
python render_bg.py --out cover.png --kind cover --primary C00000
python render_bg.py --out sec.png --kind section --primary C00000 --accent FFC107
python render_bg.py --out x.png --html mybg.html # 渲任意 HTML
依赖:本机装了 Chrome 或 Edge(无需 pip 包)。两者都没有则报错退出。
产物默认 2560x1440(16:9 高清,2x 超采样),嵌进 13.33in 画布够清晰。
"""
from __future__ import annotations
import argparse
import subprocess
import sys
import tempfile
from pathlib import Path
_CHROME_CANDIDATES = [
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
]
def find_browser() -> str:
for c in _CHROME_CANDIDATES:
if Path(c).exists():
return c
# PATH 兜底
import shutil
for name in ("chrome", "chrome.exe", "msedge", "msedge.exe"):
p = shutil.which(name)
if p:
return p
raise SystemExit("[fatal] 未找到 Chrome / Edge,无法渲染背景图。改用 DrawingML 渐变背景(apply_brand)。")
def _hex(h: str) -> tuple[int, int, int]:
h = h.lstrip("#")
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
def _mix(c, d, t):
return tuple(round(a + (b - a) * t) for a, b in zip(c, d))
def _css(c) -> str:
return f"rgb({c[0]},{c[1]},{c[2]})"
def build_html(kind: str, primary: str, accent: str) -> str:
p = _hex(primary)
a = _hex(accent)
dark = _mix(p, (0, 0, 0), 0.55) # 深端
deep = _mix(p, (0, 0, 0), 0.30)
glow = _mix(p, (255, 255, 255), 0.25) # 亮红光晕
pc, dc, dpc, gc, ac = _css(p), _css(dark), _css(deep), _css(glow), _css(a)
# 公共:mesh 渐变(多点径向叠加)+ 模糊光斑 + 细点纹理。文字由 build_deck 叠。
# cover:左侧加暗罩,让左置白字更稳;section:整页深,中心略亮。
overlay = (
"radial-gradient(1200px 900px at 18% 50%, rgba(0,0,0,.34), transparent 60%),"
if kind == "cover" else
"radial-gradient(1000px 800px at 50% 42%, rgba(255,255,255,.06), transparent 60%),"
)
return f"""<!doctype html><html><head><meta charset="utf-8"><style>
html,body{{margin:0;padding:0}}
.bg{{width:1280px;height:720px;position:relative;overflow:hidden;
background:
{overlay}
radial-gradient(700px 520px at 82% 16%, {gc}, transparent 58%),
radial-gradient(900px 700px at 92% 96%, {dc}, transparent 55%),
radial-gradient(620px 620px at 12% 8%, rgba(255,255,255,.10), transparent 60%),
linear-gradient(135deg, {pc} 0%, {dpc} 58%, {dc} 100%);
}}
.blob{{position:absolute;border-radius:50%;filter:blur(64px)}}
.b1{{width:420px;height:420px;right:-60px;top:-110px;background:{ac};opacity:.30}}
.b2{{width:360px;height:360px;right:160px;bottom:-130px;background:{gc};opacity:.40}}
.grid{{position:absolute;inset:0;opacity:.07;
background-image:linear-gradient(rgba(255,255,255,.6) 1px,transparent 1px),
linear-gradient(90deg,rgba(255,255,255,.6) 1px,transparent 1px);
background-size:54px 54px}}
.bar{{position:absolute;left:0;top:0;width:8px;height:720px;background:{ac};opacity:.9}}
</style></head><body><div class="bg">
<div class="grid"></div>
<div class="blob b1"></div>
<div class="blob b2"></div>
<div class="bar"></div>
</div></body></html>"""
def render(html: str, out: Path, w: int, h: int) -> None:
browser = find_browser()
with tempfile.TemporaryDirectory() as td:
hp = Path(td) / "bg.html"
hp.write_text(html, encoding="utf-8")
url = hp.resolve().as_uri()
# 用 1/2 窗口 + 2x 缩放 = 超采样,边缘/模糊更干净
cmd = [
browser, "--headless", "--disable-gpu", "--hide-scrollbars",
"--default-background-color=00000000",
f"--force-device-scale-factor=2",
f"--window-size={w // 2},{h // 2}",
f"--screenshot={out}", url,
]
subprocess.run(cmd, check=False,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if not out.exists():
raise SystemExit(f"[fatal] 渲染失败,未生成 {out}(浏览器: {browser})")
print(f"[ok] {out} ({out.stat().st_size // 1024} KB)")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", type=Path, required=True)
ap.add_argument("--kind", choices=["cover", "section"], default="cover")
ap.add_argument("--primary", default="C00000")
ap.add_argument("--accent", default="FFC107")
ap.add_argument("--html", type=Path, default=None, help="渲任意 HTML 文件(忽略 kind)")
ap.add_argument("--w", type=int, default=2560)
ap.add_argument("--h", type=int, default=1440)
args = ap.parse_args()
args.out.parent.mkdir(parents=True, exist_ok=True)
html = (args.html.read_text(encoding="utf-8") if args.html
else build_html(args.kind, args.primary, args.accent))
render(html, args.out, args.w, args.h)
if __name__ == "__main__":
main()