feat: 完善文字画视觉并新增 PDF 导出
- 调整飞鸟、楼群与标题配色和布局 - 新增 uv + Playwright PDF 导出脚本,四边各留 1em - 页面标题改为「封面」
This commit is contained in:
1 parent
17a224be27
commit
6085fc24d3
8 files changed
+343
-19
No files matched your search
@@ -0,0 +1,151 @@
|
||||
"""将 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/luori-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" / "luori.pdf",
|
||||
help="输出文件路径,默认 dist/luori.pdf",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if args.em <= 0:
|
||||
parser.error("--em 必须大于 0")
|
||||
export(args.em, args.out.resolve())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user