"""将文字画页面导出为 PDF,画面四边各留恰好 1em。 页面为 28 × 44 格文字画,1em 即一个汉字格。导出页尺寸为 30em × 46em, 场景四周各留 1em;底色铺满整页。 默认把 page1、page2、page3 依次合成一份三页 PDF。 用法: uv run python scripts/export_pdf.py # 三页 → dist/三页demo.pdf uv run python scripts/export_pdf.py --page page2.html -o dist/page2.pdf uv run python scripts/export_pdf.py --pages page1.html page3.html -o dist/选页.pdf uv run python scripts/export_pdf.py --em 16 -o dist/三页demo-16.pdf """ from __future__ import annotations import argparse import io import pathlib from playwright.sync_api import sync_playwright from pypdf import PdfWriter ROOT = pathlib.Path(__file__).resolve().parents[1] COLS = 28 ROWS = 44 MARGIN_EM = 1 DEFAULT_PAGES = ("page1.html", "page2.html", "page3.html") 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 render_pdf(browser, html, css, em, page_w, page_h): """渲染单页并返回 PDF 字节;导出前校验画面四边恰好留 1em。""" page = browser.new_page( viewport={"width": round(page_w), "height": round(page_h)}, device_scale_factor=1, ) try: page.goto(html.as_uri(), wait_until="load") page.evaluate("() => document.fonts.ready") if not page.evaluate("document.fonts.check('16px Zpix')"): raise SystemExit(f"{html.name}: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"{html.name}:导出布局校验失败:{metrics}") return page.pdf( 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, ) finally: page.close() def export(htmls, out, em): css, page_w, page_h = export_css(em) with sync_playwright() as pw: browser, browser_name = launch_browser(pw) blobs = [render_pdf(browser, html, css, em, page_w, page_h) for html in htmls] browser.close() writer = PdfWriter() for blob in blobs: writer.append(io.BytesIO(blob)) out.parent.mkdir(parents=True, exist_ok=True) with out.open("wb") as fh: writer.write(fh) writer.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)") for index, html in enumerate(htmls, start=1): print(f" 第 {index} 页:{html.name}") print(f"已导出 {len(htmls)} 页:{out.relative_to(ROOT)}") def main() -> None: parser = argparse.ArgumentParser( description="将文字画页面导出为 PDF(四周各留 1em,可多页合一)" ) parser.add_argument( "--pages", "--page", dest="pages", nargs="+", default=list(DEFAULT_PAGES), help="要导出的 HTML,按顺序合成多页;默认 page1 page2 page3", ) 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") htmls = [] for name in args.pages: html = (ROOT / name).resolve() if not html.is_file(): parser.error(f"页面不存在:{name}") htmls.append(html) export(htmls, args.out.resolve(), args.em) if __name__ == "__main__": main()