feat: PDF 导出脚本支持三页合一
逐页渲染 page1/page2/page3 并用 pypdf 合并为单个 PDF,默认输出 dist/三页demo.pdf; --page 改为可多值的 --pages(保留旧名),每页各自校验四边 1em 与 Zpix 字体。 README 同步导出用法与各页说明。
This commit is contained in:
1 parent
7a39a8fe2c
commit
f1888fbaf8
4 files changed
+96
-34
No files matched your search
@@ -53,8 +53,8 @@ python -m http.server 8000
|
|||||||
现有页面:
|
现有页面:
|
||||||
|
|
||||||
- `page1.html`:完整日落城市文字画,空白处铺淡色「天」。
|
- `page1.html`:完整日落城市文字画,空白处铺淡色「天」。
|
||||||
- `page2.html`:无「天」背景;底排「地」,其上以「桌」「椅」「枪」摆出桌子、椅子与枪。
|
- `page2.html`:空白处铺淡色「墙」;底排「地」,其上以「桌」「椅」摆出桌子与椅子,桌面立「酒杯」「烛」与火,右下角「桶」包「酒」作酒桶,右上角「链」「笼」「灯」「穗」拼出灯笼,墙上以「框」包「图」作画框,前景为「酒水单」与两列曲目。
|
||||||
- `page3.html`:无「天」背景的演职员表;含 35 个演职员 nametag、电线、麻雀、草、消防栓与下半屏火山。
|
- `page3.html`:空白处铺淡色「黑」的演职员表;含 36 个演职员 nametag、电线、麻雀、草、消防栓与下半屏火山,nametag 与「演职员表」四周的空白格不铺背景。
|
||||||
|
|
||||||
共用与专属:
|
共用与专属:
|
||||||
|
|
||||||
@@ -65,22 +65,25 @@ python -m http.server 8000
|
|||||||
|
|
||||||
## 导出 PDF
|
## 导出 PDF
|
||||||
|
|
||||||
使用 Playwright(由 uv 管理)导出,页面尺寸为 30em × 46em:28 × 44 格的画面,四周各留恰好 1em。
|
使用 Playwright(由 uv 管理)导出,页面尺寸为 30em × 46em:28 × 44 格的画面,四周各留恰好 1em。默认把三张页面合成一份三页 PDF。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv sync
|
uv sync
|
||||||
uv run python scripts/export_pdf.py # 默认导出 page1.html 到 dist/封面demo.pdf
|
uv run python scripts/export_pdf.py # 三页 → dist/三页demo.pdf
|
||||||
uv run python scripts/export_pdf.py --page page3.html -o dist/page3.pdf
|
uv run python scripts/export_pdf.py --page page2.html -o dist/page2.pdf
|
||||||
uv run python scripts/export_pdf.py --em 16 -o dist/封面demo-16.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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`--page` / `--pages` 按给定顺序逐页渲染后用 pypdf 合并,只传一个页面即得到单页 PDF。
|
||||||
|
|
||||||
脚本优先调用本机 Edge / Chrome;若都未安装,可先下载 Playwright Chromium:
|
脚本优先调用本机 Edge / Chrome;若都未安装,可先下载 Playwright Chromium:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run playwright install chromium
|
uv run playwright install chromium
|
||||||
```
|
```
|
||||||
|
|
||||||
导出页为黑底满幅,画面距上、下、左、右边界各 1em;脚本会在导出前校验该边距。默认 `--em 24` 时输出 720 × 1104 px(约 190 × 292 mm)。
|
导出页为黑底满幅,画面距上、下、左、右边界各 1em;脚本会在导出前逐页校验该边距。默认 `--em 24` 时每页输出 720 × 1104 px(约 190 × 292 mm)。
|
||||||
|
|
||||||
## 调整指南
|
## 调整指南
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ description = "落日文字画页面与 PDF 导出工具"
|
|||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"playwright>=1.48",
|
"playwright>=1.48",
|
||||||
|
"pypdf>=7.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
|
|||||||
+59
-26
@@ -3,24 +3,30 @@
|
|||||||
页面为 28 × 44 格文字画,1em 即一个汉字格。导出页尺寸为 30em × 46em,
|
页面为 28 × 44 格文字画,1em 即一个汉字格。导出页尺寸为 30em × 46em,
|
||||||
场景四周各留 1em;底色铺满整页。
|
场景四周各留 1em;底色铺满整页。
|
||||||
|
|
||||||
|
默认把 page1、page2、page3 依次合成一份三页 PDF。
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
uv run python scripts/export_pdf.py
|
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 --page page2.html -o dist/page2.pdf
|
||||||
uv run python scripts/export_pdf.py --em 16 -o dist/封面demo-16.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
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import io
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
from playwright.sync_api import sync_playwright
|
from playwright.sync_api import sync_playwright
|
||||||
|
from pypdf import PdfWriter
|
||||||
|
|
||||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
COLS = 28
|
COLS = 28
|
||||||
ROWS = 44
|
ROWS = 44
|
||||||
MARGIN_EM = 1
|
MARGIN_EM = 1
|
||||||
|
DEFAULT_PAGES = ("page1.html", "page2.html", "page3.html")
|
||||||
|
|
||||||
|
|
||||||
def export_css(em: float) -> tuple[str, float, float]:
|
def export_css(em: float) -> tuple[str, float, float]:
|
||||||
@@ -71,19 +77,17 @@ def launch_browser(pw):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def export(em: float, out: pathlib.Path, html: pathlib.Path) -> None:
|
def render_pdf(browser, html, css, em, page_w, page_h):
|
||||||
css, page_w, page_h = export_css(em)
|
"""渲染单页并返回 PDF 字节;导出前校验画面四边恰好留 1em。"""
|
||||||
|
page = browser.new_page(
|
||||||
with sync_playwright() as pw:
|
viewport={"width": round(page_w), "height": round(page_h)},
|
||||||
browser, browser_name = launch_browser(pw)
|
device_scale_factor=1,
|
||||||
page = browser.new_page(
|
)
|
||||||
viewport={"width": round(page_w), "height": round(page_h)},
|
try:
|
||||||
device_scale_factor=1,
|
|
||||||
)
|
|
||||||
page.goto(html.as_uri(), wait_until="load")
|
page.goto(html.as_uri(), wait_until="load")
|
||||||
page.evaluate("() => document.fonts.ready")
|
page.evaluate("() => document.fonts.ready")
|
||||||
if not page.evaluate("document.fonts.check('16px Zpix')"):
|
if not page.evaluate("document.fonts.check('16px Zpix')"):
|
||||||
raise SystemExit("Zpix 字体未加载,无法导出。")
|
raise SystemExit(f"{html.name}:Zpix 字体未加载,无法导出。")
|
||||||
|
|
||||||
page.add_style_tag(content=css)
|
page.add_style_tag(content=css)
|
||||||
page.wait_for_timeout(200)
|
page.wait_for_timeout(200)
|
||||||
@@ -106,32 +110,56 @@ def export(em: float, out: pathlib.Path, html: pathlib.Path) -> None:
|
|||||||
or abs(metrics["h"] - ROWS * em) > 0.5
|
or abs(metrics["h"] - ROWS * em) > 0.5
|
||||||
or any(v != expected_pad for v in metrics["pad"])
|
or any(v != expected_pad for v in metrics["pad"])
|
||||||
):
|
):
|
||||||
raise SystemExit(f"导出布局校验失败:{metrics}")
|
raise SystemExit(f"{html.name}:导出布局校验失败:{metrics}")
|
||||||
|
|
||||||
out.parent.mkdir(parents=True, exist_ok=True)
|
return page.pdf(
|
||||||
page.pdf(
|
|
||||||
path=str(out),
|
|
||||||
width=f"{page_w:g}px",
|
width=f"{page_w:g}px",
|
||||||
height=f"{page_h:g}px",
|
height=f"{page_h:g}px",
|
||||||
margin={"top": "0", "right": "0", "bottom": "0", "left": "0"},
|
margin={"top": "0", "right": "0", "bottom": "0", "left": "0"},
|
||||||
print_background=True,
|
print_background=True,
|
||||||
prefer_css_page_size=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()
|
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_w = page_w / 96 * 25.4
|
||||||
mm_h = page_h / 96 * 25.4
|
mm_h = page_h / 96 * 25.4
|
||||||
print(f"浏览器:{browser_name}")
|
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"页面:{page_w:g} × {page_h:g}px(约 {mm_w:.0f} × {mm_h:.0f} mm),四边各 {em:g}px(1em)")
|
||||||
print(f"已导出:{out.relative_to(ROOT)}")
|
for index, html in enumerate(htmls, start=1):
|
||||||
|
print(f" 第 {index} 页:{html.name}")
|
||||||
|
print(f"已导出 {len(htmls)} 页:{out.relative_to(ROOT)}")
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser(description="将文字画页面导出为 PDF(四周各留 1em)")
|
parser = argparse.ArgumentParser(
|
||||||
|
description="将文字画页面导出为 PDF(四周各留 1em,可多页合一)"
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
"--pages",
|
||||||
"--page",
|
"--page",
|
||||||
default="page1.html",
|
dest="pages",
|
||||||
help="要导出的 HTML 文件,默认 page1.html",
|
nargs="+",
|
||||||
|
default=list(DEFAULT_PAGES),
|
||||||
|
help="要导出的 HTML,按顺序合成多页;默认 page1 page2 page3",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--em",
|
"--em",
|
||||||
@@ -143,16 +171,21 @@ def main() -> None:
|
|||||||
"-o",
|
"-o",
|
||||||
"--out",
|
"--out",
|
||||||
type=pathlib.Path,
|
type=pathlib.Path,
|
||||||
default=ROOT / "dist" / "封面demo.pdf",
|
default=ROOT / "dist" / "三页demo.pdf",
|
||||||
help="输出文件路径,默认 dist/封面demo.pdf",
|
help="输出文件路径,默认 dist/三页demo.pdf",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.em <= 0:
|
if args.em <= 0:
|
||||||
parser.error("--em 必须大于 0")
|
parser.error("--em 必须大于 0")
|
||||||
html = (ROOT / args.page).resolve()
|
|
||||||
if not html.is_file():
|
htmls = []
|
||||||
parser.error(f"页面不存在:{args.page}")
|
for name in args.pages:
|
||||||
export(args.em, args.out.resolve(), html)
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -94,10 +94,14 @@ version = "0.1.0"
|
|||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "playwright" },
|
{ name = "playwright" },
|
||||||
|
{ name = "pypdf" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [{ name = "playwright", specifier = ">=1.48" }]
|
requires-dist = [
|
||||||
|
{ name = "playwright", specifier = ">=1.48" },
|
||||||
|
{ name = "pypdf", specifier = ">=7.0.1" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "playwright"
|
name = "playwright"
|
||||||
@@ -117,3 +121,24 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/8c/0d/32e96514b48fabd16ece31bae11afba5e18cf425123fb7c36c6b8bd8db8f/playwright-1.65.0-py3-none-win_amd64.whl", hash = "sha256:ab22611318e8747477c949c81b055ee6e224ecedf66c2a794209c7fd0210a743", size = 42664080, upload-time = "2026-11-09T21:09:54.425Z" },
|
{ url = "https://files.pythonhosted.org/packages/8c/0d/32e96514b48fabd16ece31bae11afba5e18cf425123fb7c36c6b8bd8db8f/playwright-1.65.0-py3-none-win_amd64.whl", hash = "sha256:ab22611318e8747477c949c81b055ee6e224ecedf66c2a794209c7fd0210a743", size = 42664080, upload-time = "2026-11-09T21:09:54.425Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/d6/2a75d6eeda7b23e1b241ef898ed53bc3ea134ff52d2c959c770dce843e3c/playwright-1.65.0-py3-none-win_arm64.whl", hash = "sha256:67f0b1a80d0d2d4349f67c1ccfeceaa067f1d85aaac32d4d81dd3e971a96ddbb", size = 38367903, upload-time = "2026-11-09T21:09:58.28Z" },
|
{ url = "https://files.pythonhosted.org/packages/ac/d6/2a75d6eeda7b23e1b241ef898ed53bc3ea134ff52d2c959c770dce843e3c/playwright-1.65.0-py3-none-win_arm64.whl", hash = "sha256:67f0b1a80d0d2d4349f67c1ccfeceaa067f1d85aaac32d4d81dd3e971a96ddbb", size = 38367903, upload-time = "2026-11-09T21:09:58.28Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pypdf"
|
||||||
|
version = "7.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/00/c2/4c34b3229abae3e2fcc7809e6cf80fdea159771a37142f475fbf1009c809/pypdf-7.0.1.tar.gz", hash = "sha256:0a78bb99efb41e9088260e9a8e5da134a5cbb9a322014744177bf29b5267ea6d", size = 7206748, upload-time = "2026-11-10T11:47:22.582Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/a9/ae11deef21eaac5113f8c3848da1c42423b7abeca0abc349bfec7abf4c4e/pypdf-7.0.1-py3-none-any.whl", hash = "sha256:6131b764b046282357037309e7ef2a1b7679d4a5c53eed3db2bfbf7c673ddc2b", size = 416136, upload-time = "2026-11-10T11:47:20.716Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||||
|
]
|
||||||
Reference in new issue
Block a user