alitrack

我做了一个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 都能跑。

Image

● ● ●

我为什么不用 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。零浏览器参与。