136 lines
5.4 KiB
Python
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()
|