"""将 index.html 导出为 PDF,画面四边各留恰好 1em。 页面为 28 × 44 格文字画,1em 即一个汉字格。导出页尺寸为 30em × 46em, 场景四周各留 1em;底色铺满整页。 用法: uv run python scripts/export_pdf.py uv run python scripts/export_pdf.py --em 16 -o dist/封面demo-16.pdf """ from __future__ import annotations import argparse import pathlib from playwright.sync_api import sync_playwright ROOT = pathlib.Path(__file__).resolve().parents[1] HTML = ROOT / "index.html" COLS = 28 ROWS = 44 MARGIN_EM = 1 def export_css(em: float) -> tuple[str, float, float]: page_w = (COLS + MARGIN_EM * 2) * em page_h = (ROWS + MARGIN_EM * 2) * em css = f""" @page {{ size: {page_w:g}px {page_h:g}px; margin: 0; }} html, body {{ width: {page_w:g}px !important; height: {page_h:g}px !important; min-height: 0 !important; margin: 0 !important; display: block !important; overflow: hidden !important; background: #000 !important; }} body {{ padding: {em:g}px !important; }} .scene {{ font-size: {em:g}px !important; width: {COLS}em !important; height: {ROWS}em !important; margin: 0 !important; }} * {{ -webkit-print-color-adjust: exact; print-color-adjust: exact; }} """ return css, page_w, page_h def launch_browser(pw): for name, kwargs in ( ("Microsoft Edge", {"channel": "msedge"}), ("Google Chrome", {"channel": "chrome"}), ("Playwright Chromium", {}), ): try: return pw.chromium.launch(**kwargs), name except Exception: continue raise SystemExit( "未找到可用浏览器。请先运行:uv run playwright install chromium" ) def export(em: float, out: pathlib.Path) -> None: css, page_w, page_h = export_css(em) with sync_playwright() as pw: browser, browser_name = launch_browser(pw) page = browser.new_page( viewport={"width": round(page_w), "height": round(page_h)}, device_scale_factor=1, ) page.goto(HTML.as_uri(), wait_until="load") page.evaluate("() => document.fonts.ready") if not page.evaluate("document.fonts.check('16px Zpix')"): raise SystemExit("Zpix 字体未加载,无法导出。") page.add_style_tag(content=css) page.wait_for_timeout(200) metrics = page.evaluate( """() => { const scene = document.querySelector('.scene').getBoundingClientRect(); const body = getComputedStyle(document.body); return { x: scene.x, y: scene.y, w: scene.width, h: scene.height, pad: [body.paddingTop, body.paddingRight, body.paddingBottom, body.paddingLeft], }; }""" ) expected_pad = f"{em:g}px" if ( abs(metrics["x"] - em) > 0.5 or abs(metrics["y"] - em) > 0.5 or abs(metrics["w"] - COLS * em) > 0.5 or abs(metrics["h"] - ROWS * em) > 0.5 or any(v != expected_pad for v in metrics["pad"]) ): raise SystemExit(f"导出布局校验失败:{metrics}") out.parent.mkdir(parents=True, exist_ok=True) page.pdf( path=str(out), width=f"{page_w:g}px", height=f"{page_h:g}px", margin={"top": "0", "right": "0", "bottom": "0", "left": "0"}, print_background=True, prefer_css_page_size=True, ) browser.close() mm_w = page_w / 96 * 25.4 mm_h = page_h / 96 * 25.4 print(f"浏览器:{browser_name}") print(f"页面:{page_w:g} × {page_h:g}px(约 {mm_w:.0f} × {mm_h:.0f} mm),四边各 {em:g}px(1em)") print(f"已导出:{out.relative_to(ROOT)}") def main() -> None: parser = argparse.ArgumentParser(description="将文字画页面导出为 PDF(四周各留 1em)") parser.add_argument( "--em", type=float, default=24, help="导出时 1em(一个汉字格)的像素大小,默认 24", ) parser.add_argument( "-o", "--out", type=pathlib.Path, default=ROOT / "dist" / "封面demo.pdf", help="输出文件路径,默认 dist/封面demo.pdf", ) args = parser.parse_args() if args.em <= 0: parser.error("--em 必须大于 0") export(args.em, args.out.resolve()) if __name__ == "__main__": main()