我做了一个HTML→PDF工具。不用浏览器。
上个月我想把一张架构图转成 PDF。
打开 Chrome,Ctrl+P,另存为 PDF——15MB。一张纯文字的流程图,比原图大了 200 倍。
我需要的东西很明确:一个命令行工具,吃 HTML,吐 PDF。不要浏览器,不要几百 MB 的依赖,几十 MB 的二进制就够。
搜了一圈:wkhtmltopdf 上次更新是 2020 年,基于 QtWebKit——一个早已停止维护的浏览器内核。PaperMuncher 是 C++ 项目,绑在 Odoo 生态上。
没有现成的答案。
那就自己做一个。
● ● ●
4400 行 Rust,没有一行是我写的
TypePress 的全部代码,是我和 Hermes 聊出来的。
我在 Hermes 里描述需求,它调用 DeepSeek 模型生成代码,我跑测试,看哪里不对,再描述,再生成。几十轮对话下来,79 个 commit,从 cargo init 到 v0.4.0——全程没有亲手写过一行 Rust。
这不是"AI 辅助编程"。这是 AI 编程,我验收。
● ● ●
我做了什么
一个纯 Rust 的 HTML → PDF 引擎。没有 Chrome,没有 Electron,没有 Qt。
HTML → Blitz(解析)→ Taffy(CSS 布局)→ Krilla(PDF 渲染)→ PDF
三层全是 Rust crate:Blitz 解析 HTML 和 CSS,Taffy 算 Flexbox/Grid 布局,Krilla 写 PDF 内容流。整个链路不经过浏览器内核。编译完一个 23MB 二进制,macOS、Linux、Windows 都能跑。
● ● ●
我为什么不用 Chromium
Chromium headless 当然能渲染 HTML。但我没法接受:
文件体积。 同一张架构图,Chromium PDF 15MB,我这里出来 30KB。差了 500 倍。因为我直接写 PDF 内容流,Chromium 把整个浏览器渲染状态序列化进去了。
依赖。 Chromium 本体 200MB+,npx playwright install 再下 400MB。我一个 23MB 二进制完事。
不可复现。 headless 模式的窗口大小、滚动条、DPI——每次截图都可能不一样。CI 里尤其噩梦:--no-sandbox、字体缺失、共享内存不够——一个接一个坑。
对于报表、发票、合同、知识图谱这种模板化 PDF——你不需要浏览器。
● ● ●
我让 CLI 完全兼容 wkhtmltopdf
你以前的 wkhtmltopdf 命令,换二进制名就生效:
typepress -s A4 -O Portrait -T 15mm -B 15mm -L 20mm -R 20mm \ --title "Report" --zoom 1.0 input.html -o output.pdf
甚至可以 alias wkhtmltopdf=typepress。--page-size、--margin-top、--header-html、--user-style-sheet——参数照搬,迁移零成本。
● ● ●
我加了 autofit:让内容自己找页面
最头疼的问题是:HTML 渲染出来到底多大?一页还是三页?你不可能每次打开看。
--autofit 自动试不同的页面尺寸和方向,二分搜索最佳缩放:
typepress diagram.html -o out.pdf --autofit --check # Autofit: A3 landscape → 1 page at 65.1% zoom
从当前尺寸开始,依次试更大的纸和横竖方向,每个组合跑 12 轮二分,最后选视觉效果最好的那个。
不用猜,不用调。一个 flag。
● ● ●
我加了 check:渲染后给我一份体检报告
内容有没有被截断?缩放合不合理?
typepress input.html -o out.pdf --check
输出:
╔══════════════════════════════════════╗ ║ TypePress Diagnostic Report ║ ╠══════════════════════════════════════╣ ║ Page size: 420×297 mm (landscape) ║ Pages: 1 ║ Zoom: 76.9% ║ Text items: 363 ║ Images: 0 ╠══════════════════════════════════════╣ ║ ✅ No issues detected ║ ╚══════════════════════════════════════╝
页数、缩放、文字/图片数一目了然。多页没开 --fit 会提醒,缩放太低会警告。批量跑 500 份不用手动抽查。
● ● ●
为什么选 Rust
一开始考虑过 Python(weasyprint)和 JS(Puppeteer)。
Python 那套要 Cairo、Pango、GTK——三个 C 库,跨平台部署是灾难。JS 方案本质还是套 Chromium,体积问题绕不开。
Rust 的 fulgur 栈给了我第三条路。blitz-html 解析、taffy 布局、krilla 渲染——三个 crate 都是纯 Rust,零 C 依赖。编译出来一个静态链接的二进制,copy 到任何机器直接跑。
代价是 CSS 不如浏览器全。linear-gradient 文字、animation 还不支持。但对于 90% 的 PDF 场景——报表、文档、知识图谱——已经够了。
● ● ●
现在能做什么,还差什么
| 能做的 | 还差的 |
|---|---|
| ✅ HTML/CSS → PDF | 🟡 网络图片(引擎限制) |
| ✅ Flexbox / Grid 原生布局 | 🚧 渐变文字 |
| ✅ Markdown(数学公式、代码高亮) | 🚧 CSS Grid 高级特性 |
| ✅ Mermaid 图表 | |
| ✅ Emoji 原生矢量(COLRv1) | |
| ✅ wkhtmltopdf 完全兼容 | |
| ✅ @font-face 自动下载 | |
| ✅ `--autofit` / `--check` |
Sparksuite 发票、Bootstrap 报表、Odoo QWeb——实测都能正确渲染。
目前 v0.4.0,发了 crates.io、PyPI、npm 三个注册表。Python 和 Node 的包是 thin wrapper,装完自动从 GitHub Releases 下拉对应平台的二进制。
github.com/alitrack/typepress
本文示意图由 TypePress 自身渲染:HTML → TypePress PDF → 自动去白边 PNG。零浏览器参与。