Files
rogerwang2008 f1888fbaf8 feat: PDF 导出脚本支持三页合一
逐页渲染 page1/page2/page3 并用 pypdf 合并为单个 PDF,默认输出 dist/三页demo.pdf;
--page 改为可多值的 --pages(保留旧名),每页各自校验四边 1em 与 Zpix 字体。
README 同步导出用法与各页说明。
2026-11-11 04:18:11 +08:00

193 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""将文字画页面导出为 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()