SanzarRehman/HTML2PDF
GitHub: SanzarRehman/HTML2PDF
基于 Rust 的高并发、低内存 HTML 转 PDF 引擎,通过自建渲染流水线在单进程内并行渲染文档,无需启动浏览器子进程即可达到接近 Chromium 的渲染保真度。
Stars: 138 | Forks: 7
# htmltopdf
[](https://www.rust-lang.org/)
[](#许可证)
[](#项目状态)
`htmltopdf` 是一个 Rust 的 HTML 转 PDF 引擎,旨在实现高并发、低内存、低 CPU 开销,并长期保持浏览器级的渲染保真度。其核心理念很简单:在一个进程内并行渲染多个文档,而无需为每个任务启动 Chromium、Puppeteer 或浏览器子进程。
该项目围绕真实的渲染流水线构建:HTML 解析、紧凑的 DOM、CSS 解析、层叠、盒模型生成、布局、显示列表绘制,以及流式压缩的 PDF 写入器。
```
HTML -> html5ever -> arena DOM -> cssparser -> cascade
-> box tree -> layout -> display list -> compressed PDF
```
## htmltopdf 与 Chromium
相同的输入(`reg-2-9-1.html`,一个真实的 1.8 MB 电子表格导出文件,包含约 2.2 万个表格单元格),渲染为 PDF 的第 1 页 —— 左侧是 `htmltopdf`,右侧是无头 Chromium(`--print-to-pdf`)。两个引擎都从文档自身的 `font-family: Calibri/Arial` CSS 中选择字体 —— htmltopdf 会自行解析、嵌入并子集化真实的 Arial + Arial Bold 字体(不需要 `--font` 标志)。
| htmltopdf | Chromium |
| --- | --- |
|  |  |
粗体标题、字体大小、网格线粗细、列宽、标题换行和每页行数都非常接近(33 页对比 Chromium 的 32 页)。
### 20 路并发转换基准测试
同时启动二十个相同的转换任务,使用 htmltopdf 的 release CLI 和 20 个全新的无头 Chrome 配置文件。报告的结束点是指最后一个 PDF 写入完成的时刻。这两次运行都在 Apple Silicon macOS(2026 年 7 月)上为每个引擎生成了所有 20 个 PDF。
| 测试用例 | htmltopdf: 挂钟时间 / 吞吐量 / 峰值 RSS | Chrome: 挂钟时间 / 吞吐量 / 峰值 RSS | 结果 |
| --- | --- | --- | --- |
| [简单的一页 HTML](examples/concurrency-simple.html) (770 B) | **0.077 s** / **261.37 PDF/s** / **57.2 MiB** | 6.582 s / 3.04 PDF/s / 11.49 GiB | **86 倍**吞吐量,**206 倍**更低的峰值 RSS |
| `reg-2-9-1.html` (1.8 MB, ~2.2 万个表格单元格) | **1.568 s** / **12.75 PDF/s** / **1.25 GiB** | 17.002 s / 1.18 PDF/s / 9.35 GiB | **10.8 倍**吞吐量,**7.5 倍**更低的峰值 RSS |
htmltopdf 的 RSS 是指其单一多 worker 进程的内存占用,通过 `/usr/bin/time -l` 测量。Chrome 的 RSS 是属于这 20 个独立配置文件的每个浏览器、渲染器和辅助进程的峰值总和,每 100 毫秒采样一次。作为参考,CPU 使用率分别为 0.14 / 15.96 htmltopdf CPU 秒,以及大约 9.91 / 28.99 Chrome 进程树 CPU 秒(简单/复杂);Chrome 采样的 CPU 峰值分别为 765% 和 603%。Chrome 在写入 PDF 后可能会保留空闲的辅助进程,因此基准测试在 PDF 准备就绪时结束,并且仅终止带有其唯一运行标签的进程。
使用以下命令复现
[`scripts/benchmark-concurrency.sh`](scripts/benchmark-concurrency.sh):
```
bash scripts/benchmark-concurrency.sh examples/concurrency-simple.html 20
bash scripts/benchmark-concurrency.sh reg-2-9-1.html 20
```
## htmltopdf 与 UniDoc (UniHTML)
[UniDoc](https://unidoc.io) 的 HTML→PDF 路径,**UniHTML**,是一个商业的 Go SDK,其转换器是一个打包为 1.72 GB Docker 服务器的**无头 Chromium**。以下三个测试用例通过两个引擎以匹配的 Letter / 48 pt 几何参数进行渲染 —— 左侧是 `htmltopdf`,右侧是 UniHTML(即 Chromium)—— 然后进行栅格化和像素差异对比。

结构高度吻合。在真实的**发票**上,两者几乎完全相同(标题、表格、蓝色表头、列宽、`Total due`、页脚);剩余的差异主要是默认字体 —— 在没有设置 `font-family` 的情况下,htmltopdf 回退到 sans-serif,而 Chromium 的 UA 默认值是 serif。两端的文本都保持真实且可选(提取的字数匹配)。两处明显的差距是从零开始构建的引擎客观存在的已知限制:htmltopdf 尚未将 flex/grid 项背景**拉伸**以填满其单元格(`align-items: stretch` 表现得类似于 `flex-start`,见网格侧边栏/主内容行),并且在换行时它将 `flex-basis` 视为项的外部尺寸(因此它每行挤下两个标签,而 Chromium 在 `box-sizing: content-box` 下添加了 padding,只能放下一个)。
| | **htmltopdf** | UniDoc UniHTML | 优势 |
| --- | --- | --- | --- |
| **对比 Chromium 的栅格化保真度** | 发票 4.0% · 网格 8.1% · flex 10.8% 差异像素 | *即* Chromium(参考对象) | 相比 UniDoc 的完整浏览器级保真度 |
| **PDF 大小** | **1.6–2.1 KB** | 20–32 KB | **缩小约 10–16 倍** |
| **单文档转换时间** | **约 18 ms** 端到端 CLI(布局本身为微秒级) | 约 0.8 s 连接到已预热的服务器(首次请求 1.3 s) | **快约 40 倍** |
| **内存(峰值 RSS)** | 这些测试用例**约 3.5 MiB** · 2.2 万单元格文档**约 112 MiB** | 17.6 MiB 空闲服务器,但每次转换都运行**无头 Chromium** —— 浏览器级别的 RSS(2.2 万单元格文档上约 846 MB;根据上述 Chromium 数据,20 个并发任务下最高达 ~9 GiB)—— 运行在 **1.72 GB** 镜像上 | 2.2 万单元格文档上**少约 30 倍** |
| **运行环境** | 单个 Rust 进程,**无子进程** | 1.72 GB Docker 容器中的无头 Chromium + gRPC/HTTP 服务器 | — |
| **许可证** | **MIT,免费** | 商业 —— 按文档计量的点数,或按开发者的永久授权 | — |
内存(RAM)这一行是架构的关键。htmltopdf 的峰值 RSS 是通过 `/usr/bin/time -l` 测量的(许多文档在一个小进程中渲染)。UniHTML 的转换器*本身*就是无头 Chromium,因此其单次转换的工作集就是上面 [对比 Chromium](#htmltopdf-vs-chromium) 章节中量化的浏览器级别的内存占用量;其空闲服务器(17.6 MiB,Chromium 尚未生成)和 1.72 GB 的镜像大小是直接测量的,但由于免费的计量点数在测量中途耗尽,无法在此处重新捕获其实时渲染峰值。
速度数据同样低估了 htmltopdf:其 18 ms 包含了每次调用的进程 + 字体启动时间,而 UniHTML 的 0.8 秒**不包含**容器和 Chromium 启动时间以及许可证验证的网络往返。这是一种常见的权衡 —— UniHTML 是一个真正的浏览器,因此它具有完全的 CSS 保真度(它可以拉伸项,处理每一个 box-sizing 的细微差别,并支持整个 Web 平台),而 htmltopdf 是一个紧凑的 CSS 子集,在发票、报表和报告上已经能与之媲美。开发测量在一台机器(Apple Silicon,macOS)上进行,不作为绝对保证。
## 为什么选择 htmltopdf?
- **设计上即为极速**:独立的渲染任务可跨 CPU 核心扩展。
- **极低内存占用**:紧凑的基于 arena 的 DOM,基于索引的数据,且每次转换无需浏览器渲染器进程。
- **并行优先**:CLI 基准测试和 HTTP 服务器均围绕 worker 级别的并行性构建。
- **真正的 HTML 解析器**:使用 `html5ever`,而非临时的标签扫描。
- **真正的 CSS 解析器**:使用 `cssparser` 进行样式表标记化和层叠支持。
- **可选且压缩的 PDF**:生成的文本保持可搜索/可选择状态。
- **Unicode 字体支持**:可选的 TrueType/OpenType 嵌入,支持 Type0 / Identity-H PDF、ToUnicode 映射,并在可能的情况下进行 TrueType 字形子集化。
- **光栅图像**:支持来自文件路径和 `data:` URI 的 `
` JPEG 和 PNG(包含 alpha 通道),作为 PDF 图像 XObject 嵌入 —— JPEG 通过 `DCTDecode` 直接透传,PNG 由内部解码,因此不依赖任何图像编解码器。
- **极小的依赖面**:无异步 runtime,无浏览器,无 Web 框架。
## 项目状态
这是一个早期的引擎,而不是一个完整的浏览器。长期目标是实现完整的 CSS 和可控的 JavaScript 支持,且内存成本远低于基于 Chromium 的渲染器。
目前已支持:
- 通过 `html5ever` 进行 HTML 解析。
- 对支持的选择器/声明子集进行 CSS 解析和层叠。
- 类型、通用(`*`)、id、类和属性选择器(`[a]`, `[a=b]`, `~= |= ^= $= *=`);后代/子代/兄弟选择器(` `, `>`, `+`, `~`);结构化伪类(`:first-child`, `:nth-child()`, `:*-of-type`, `:empty`, `:root`, `:not()`);`@media print` 查询;特异性、源代码顺序、继承和 `!important`。
- 基础流式文档:标题、段落、列表、内联序列、块引用 —— 以及与周围流内容内联渲染的表格。
- 表格:行、单元格、跨列、**跨行**(跨越的单元格在其所跨行中仅绘制一次,后续行移入释放出的列中,并且跨越分页符的跨度按页拆分)、页眉/页脚、边框、背景、对齐、换行、裁剪以及重复的表头 —— 支持**丰富的单元格内容**:混合粗体/颜色/大小片段、可点击链接以及单元格内的 RTL 文本(普通单元格保留快速的单一样式路径)。
- CSS 颜色、字体大小、粗体文本(渲染为合成的加粗填充+描边)、文本对齐(包括 `text-align: justify`)、文本修饰(下划线/删除线)、外边距、内边距(包含垂直外边距折叠)、`line-height` 和背景:纯色、`linear-gradient()`(块和表格单元格)以及流式块上的 `background-image: url()` 光栅图像(支持 `background-size`/`-position`/`-repeat`,平铺并裁剪至盒模型内)。
- **`display: inline-block`**:一个块级盒模型元素(内边距、边框、背景、`border-radius`、CSS 宽度/高度),但作为对齐到文本基线的原子项在周围行中流动 —— 徽章、按钮、标签、芯片、颜色样本 —— 周围的文本环绕它并换行。(首期:单行内部内容。)
- **百分比长度和 min/max 尺寸**:`%` 宽度、内边距、外边距和定位盒模型的偏移量基于包含块进行解析;`min-width`/`max-width`(磅或 `%`)以及 `min-height`/`max-height`(磅)对盒模型进行限制;带有固定高度的 `overflow: hidden` 将内容裁剪至边框盒;`box-sizing: border-box`。
- **CSS 自定义属性**:`--name: value` 声明可级联和继承,`var(--name, fallback)` 会进行解析 —— 包括作为其他变量别名的变量、针对缺失变量的回退,以及通过在祖先元素上重新定义变量来重新着色子树从而实现组件作用域内的覆盖。
- **`calc()` 表达式**:`+ - * /`、括号、嵌套的 `calc()` 和单位混合。混合的 `calc(100% - 20px)` 在布局时(宽度、内边距、外边距、移量)基于包含块进行解析;`calc()` 可与 `var()` 组合。
- **排版控制**:`text-transform`(大写/小写/首字母大写 —— 测量时会看到转换后的文本,且 `th` 单元格正确大写),`letter-spacing`(正或负,通过 PDF `Tc` 状态重现,因此字距得以保留),`word-spacing` 和 `text-indent`(磅或 `%`,仅限首行)。
- **`::before`/`::after` 生成内容**:带引号的字符串(包含 CSS 十六进制转义)、`attr()` 和字符串拼接 —— 必填项星号、徽章前缀、在锚点后打印链接 href —— 并将伪规则的专属样式(颜色、粗细、大小、间距)应用于生成的文本。
- **真实的边框**:各边独立的 `border-top/right/bottom/left`,具有独立的宽度、样式和颜色 —— `solid`、`dashed` 和 `dotted`(double/groove/ridge 渲染为 solid),`thin/medium/thick`,默认为 `currentColor`。边框占用布局空间,背景在其下方延伸,与浏览器一致。**`border-radius`** 可使卡片背景和均匀边框变圆(贝塞尔路径);在块、浮动和定位盒模型上支持 **`box-sizing: border-box`**。表格单元格也遵循各边独立规则 —— 经典的 `th { border-bottom: 2px solid }` 准确绘制该边缘,而均匀的电子表格网格线则保持快速路径。
- 现代布局,各自的首次迭代:**flexbox**(`display: flex` —— grow/basis/`flex-shrink`、`order`、`flex-wrap`/`wrap-reverse`、`justify-content`、`align-items`/`align-self`/`align-content`、gap、行和列),**grid**(`display: grid` —— 固定/`fr`/`auto`/`repeat()`/`minmax()` 列*和* `grid-template-rows` 轨道,占用网格上的二维 `grid-column`/`grid-row` 线条放置和行跨度,`grid-template-areas`/`grid-area` 命名放置,`align-items`/`align-self`,gap),带有真实文本环绕的 **floats**(`float: left/right`,`clear`,堆叠浮动)以及 **positioning**(带有盒模型偏移的 `position: relative/absolute/fixed`;带有负 z-index 的 `z-index` 排序,绘制在流*下方* —— `z-index: -1` 背景层模式;块上的 CSS `width` 和 `height`)。
- **文本整形**(通过 `rustybuzz` 实现 HarfBuzz)用于嵌入字体:在 PDF 中重现字距、带有可提取文本的连字、阿拉伯语连接形式。
- **双向文本 + RTL 段落** (UAX #9):混合的 LTR/RTL 文本行 —— 英语句子中的阿拉伯语或希伯来语短语 —— 重新排序为正确的视觉顺序,且 PDF 文本在逻辑顺序上保持可提取状态。`dir="rtl"` / `direction: rtl` 设置基础段落方向(可继承),翻转 bidi 基础级别并默认右对齐。
- **字体回退链**:所选字体中缺失的字符(CJK、韩文、西里尔文等)会自动回退到涵盖该字符的系统字体,每种字体都作为其独立的子集字体嵌入 —— 中/日/韩发票无需任何标志即可正确渲染。
- **每个元素独有的 `font-family` 带有真实的粗体/斜体字体**:命名的家族和 CSS 泛型会解析为真实的系统字体(包括真正的粗体和斜体变体 —— 当某个字体家族已知时,不再有合成的加粗),每个文档可包含多个子集字体;`pre`/`code` 默认为 monospace。
- **`@font-face` Web 字体**:作者声明的字体家族会覆盖系统查找。`src:` 链的工作方式与浏览器类似 —— 跳过不支持的候选字体(WOFF2),`url()` 从 `data:` URI 和本地文件中加载 TrueType/OpenType/**WOFF**(远程 `http(s)` 遵循与远程图像相同的主动选择策略),`local()` 通过家族、全名或 PostScript 名称解析系统字体。每个家族的多个规则通过 `font-weight`/`font-style` 选择真正的粗体/斜体变体。
- **可点击的链接和文档大纲**:`` 成为真正的 PDF 链接注释 —— 外部 URI、`mailto:` 以及文档内指向 `id` 锚点的 `#fragment` 跳转 —— 采用浏览器 UA 默认样式(蓝色,下划线;支持 `text-decoration: none` 和作者自定义颜色)。标题构建 PDF 书签侧边栏(`h2` 嵌套在 `h1` 下,依此类推)。
- **分页媒体运行中的页眉、页脚和页码**:CSS `@page` 页边距框(`@top-left/center/right`、`@bottom-left/center/right`)在文档分页后绘制静态文本以及最终的 `counter(page)` / `counter(pages)` 值。
- `
` 图像:支持 JPEG(`DCTDecode` 直接透传)和 PNG(内部解码,alpha 作为软掩码),来自文件路径和 `data:` URI,带有 `width`/`height` 尺寸控制和长宽比保留。与文本共享一行的图像在基线上**内联**流动(图标、徽章 —— 可在链接内点击);独立图像呈块级渲染,浮动的图像会使文本环绕其周围。
- 分页、页边距、横向页面、压缩的 PDF 流。
- 内置 Helvetica 度量标准和可选的嵌入式 TrueType/OpenType 字体。
- 针对 `glyf` 的 TrueType 字体进行字体子集化,对于尚无法子集化的格式提供完整字体回退。
- CLI、Rust 库 API 和轻量级 HTTP API。
主动选择功能(受构建特性控制):
- 有界的布局前 **JavaScript** 阶段,针对实时的 DOM 运行内联 `