ARTICLE · 1107284
6 个 PDF 转 OFD 库实测:速度和体积双第一的那个,输出是空页面
这是全系列最值得读的一篇。
因为它完整演示了基准测试怎么骗人,以及怎么识破。
先看结果总表 —— 速度和体积两项,rust-easyofd 各拿 7 胜,7 个样本全胜,零反对。
听起来:史上最强 PDF→OFD 库实际上:hello 和 ano 输出的是空页同一个库,7 胜和"最差"可以同时成立。往下看。
一、为什么 PDF→OFD 的选择少这么多
先看生态全景。ofd-libraries.md 收录了 66 个 OFD 库,覆盖 Go/Rust/Python/Java/C++/.NET/JavaScript/Pascal 八种语言。
支持 → PDF 输出的: 20 个 ✅ + 5 个 ⚠️支持 PDF → OFD 导入的: 16 个 ✅ + 1 个 ⚠️66 个库里只有 16 个能读 PDF(66 ÷ 16 ≈ 4.1 倍)。若只看"能写出 PDF"的这 20 个,则是 20 ÷ 16 = 1.25 倍。
原因也不神秘:写"生成 OFD"是按 GB/T 33190 的对象模型正向建模,有国标条文可依;写"导入 PDF"必须去逆向别人的私有对象结构,没有规范可查,全靠踩坑。
这就是为什么 OFD→PDF 能测出 8 个转换器,PDF→OFD 只有 6 个。
二、参测的 6 个转换器
⚠️ 先读这段:本篇的核心结论(谁最快、体积最小、谁输出空页)只对下表版本成立。这些库多在
0.x/1.0阶段,PDF→OFD 恰恰是最容易出新版的环节 —— 下面第二节的证据显示,go-ofdgo一天之内就被修掉了三个 bug。
go-zc310 | zc310/ofd | ||||
go-ofdgo | xiaoqidun/ofdgo | 2026-09-28 版 | |||
rust-easyofd | easyofd-rust | ||||
java-ofdrw | ofdrw | ||||
python-pdf2ofd | pdf2ofd | ||||
node-jsofd | Hufe921/jsOFD |
⭐ 一个"数据会过期"的活证据
本文最刺眼的结论是"rust-easyofd 速度 7 胜、体积 7 胜,但输出是空页"。请注意这条结论的脆弱性:
rust-easyofd = easyofd-rust 0.1.4 缺陷:hello/ano 输出空页、999/1000-pages 文本 CID 乱码、版面固定 (10,20) 性质:实现缺失(没转矢量路径 / 没做字体 CMap 映射 / 没读页面布局) ↓ 这是典型的「作者下一个版本就会修」的 bug,不是设计取舍对比同一项目里刚发生的事 —— go-ofdgo 在 PDF→OFD 方向的工程记录:
ofdgo 的 PDF→OFD(ConvertPDF + pdfgo)7 个测试文件 7/7 全部成功(2026-09-25 15:21 版修复非 Identity-H 复合字体编码、Stamp 注解、nil panic)
三个 bug(复合字体编码 / Stamp 注解 / nil panic)在一次提交里同时修掉。 如果测试跑在那次提交之前,go-ofdgo 这一行会是一片 FAILED;跑在那次之后,就是 7/7 全通过。
同一天,16 位色值的问题也已经被"新版 zc310 兼容读取"绕过 —— 本文第三章提到的那个体积问题,其实已经是个正在愈合的旧伤。
所以本文的正确用法是:
✗ 「rust-easyofd 是垃圾,PDF→OFD 别用」 → 版本 0.1.4 的判断✗ 「go-ofdgo 最可靠」 → 2026-09-25 修复后的判断✓ 「PDF→OFD 的质量与版本强相关,选型前必须锁定并自测当前版本」各自的能力边界
文档里写得很清楚:
- go-zc310
基于 pdfcpu 的 PDF→OFD 导入器,覆盖 路径/文本/图片/注解/渐变/图案/网格/大纲/元数据,文本保留为可提取 TextObject - go-ofdgo
基于 pdfgo,覆盖 路径/文本/图片/注解 - rust-easyofd
基于 lopdf 提取文本/图片,不转换矢量路径 - java-ofdrw
文本转为矢量轮廓,保留原始外观 - python-pdf2ofd
基于 PyMuPDF 渲染,文本栅格化 - node-jsofd
基于 pdfjs 重放绘制指令流(路径/文本/图片),不转换注解/渐变/图案/Type3/剪裁
测试文件:直接用数科版式的参考 PDF
这一步设计得很好 —— 用的就是 02 篇里那组"数科版式阅读器导出的参考 PDF",保证双向测试落在同一组文档上。
hello.pdf | |||
999.pdf | |||
zsbk.pdf | 签章文件 | ||
ano.pdf | 发票 | ||
intro.pdf | |||
1000-pages.pdf | 1000 | ||
GBT_33190-2016.pdf | 国标正文,字体全内嵌、图表多 |
三、速度:7 胜,但那是"空转"的 7 胜
| rust-easyofd | 4.9ms | 4.1ms | 7.2ms | 105ms | 24ms | 274ms | 76ms | 7 |
注意 rust-easyofd 的 hello = 4.9ms、ano = 4.1ms。 转一个 3 页的发票,4 毫秒。
文档的注解写得毫不留情:
rust-easyofd 的「最快」源于几乎未提取内容(hello/ano 输出空页),速度优势无实际意义。
再看多页:1000-pages(1000 页)只要 274ms,而 java-ofdrw 要 25.90s —— 95 倍。但 274ms 处理 1000 页,平均每页 0.27ms。读一个 PDF 页面、解析资源字典、绘制内容流、写出 OFD 的 XML 和资源引用,0.27ms 做不到。
除非它什么也没做。
四、体积:那个 0.00 是全场最大的坑
| rust-easyofd | 1.2K | 1.8K | 3.6K | 5.4M | 919K | 432K | 2.0M | 7 |
| 34.4M | 19.4M | 57.4M | 30.5M | |||||
| 44.8M | 37.7M | |||||||
| 35.6M | 13.2M | 63.4M | 35.1M |
看 ano 这一列
源 PDF 904Krust-easyofd 1.8K ← 比值 0.00go-zc310 389.7K ← 0.43go-ofdgo 586.1K ← 0.651.8K。 一个 904K 的三页发票,转出来 1.8K。
比值列写得清清楚楚:
| 0.33 | 0.00 | 0.04 | 0.78 | 0.57 | 0.20 | 0.41 | |
文档第二次点名:
rust-easyofd「体积最小」同样是内容丢失的结果(hello 1.2K/ano 1.8K 实为空页),不具可比性。
真正的压缩能力看 go-zc310
0.43 0.43 0.59 1.08 0.93 1.43 1.347 个样本全部落在 0.43~1.43(中位数 0.93),没有一个超过 1.5 倍;其中 hello/ano/999/zsbk 四个转换后比源 PDF 还小(OFD 的 XML + 资源经 ZIP 压缩,且没有 PDF 的增量更新与对象间接表开销)。而且这是在内容全转的前提下做到的。这才是压缩。
而"保外观"路线的代价
java-ofdrw 999 = 10.24 倍 zsbk = 12.07 倍 1000-pages = 26.48 倍python-pdf2ofd 999 = 9.18 倍 1000-pages = 20.63 倍node-jsofd 1000-pages = 29.25 倍 GBT = 7.09 倍文档总结:
两种文本转图形的方案输出普遍膨胀:ofdrw 在 999、zsbk、1000-pages 上是源 PDF 的 10~26 倍,pdf2ofd 在 999、1000-pages 上是 9~21 倍(zc310 全部 ≤1.5x);jsOFD 全面膨胀(2.1~29 倍,1000-pages 达 29 倍)。
为什么会这样? 因为文字信息被替换成了几何信息:
一行字 = 1 个 TextObject + 若干 TextCode + 字体引用 ≈ 几百字节同样一行字 = N 个 PathObject,每个字形一条闭合轮廓 ≈ 几 KB 到几十 KB几何数据比字符数据大一个量级,这是必然的。"体积膨胀"不是实现质量问题,是路线选择的必然代价。
五、还原度:回环测试怎么做的
方法:每个转换器输出的 OFD,都用 go-zc310 转回 PDF(回环),再与源 PDF 比。
📌 回环口径必须交代清楚,文档写得很严谨:
统一回环度量的是"输出 OFD 的互操作正确性",不是端到端体验。
好处:能暴露互操作问题。如果各库都自己回环,写错的坐标/格式会被自家读取器按同样的错读回来,问题被掩盖。比如 ofdgo的 16 位色值,只有换读取器才暴露。代价:引入了 zc310 渲染质量偏差,数据仅供参考。
5.1 像素相似度
| 99.98% | 99.62% | |||||||
| 99.75% | 99.98% | |||||||
| 99.80% | 99.37% | |||||||
| 98.94% | ||||||||
| 90.23% | ||||||||
| 43.84% | 73.67% |
三点观察:
① node-jsofd 的 ano 只有 90.23%。 它其余 6 个文件都在 98.13%~99.94%,偏偏这一项掉到 90% —— 是全表中唯一一处"单项掉队"(区别于 rust-easyofd 在 intro 上的整表崩塌)。原因很具体:
逐条重放绘制指令流,不转换注解/渐变/图案/Type3/剪裁。
ano因 Stamp 丢失像素仅 90.23%。
发票的骑缝章/电子章在 PDF 里常以 Stamp 注解存在,位于页面 Annots 数组而非内容流。不读 Annots,画面就少一块。go-zc310 走了 pdfcpu 且路径明确含注解,所以 ano 拿到 99.75%。
② rust-easyofd 的 intro 只有 43.84%,比 02 篇的 OFD→PDF 还低(44.99%)。 同一个库,两个方向都在复杂文档上崩。
③ rust-easyofd 的 GBT 拿到 96.28%,看起来不错 —— 但它这份样本的文本提取是 0 字符。
这就是 01 篇讲过的陷阱:
像素相似度基于整页平均色差,对内容稀疏的页面(白底小字)区分度有限。
GBT_33190-2016.pdf 是 132 页国标正文,页面大量留白。一张空页和一张"正确但稀疏"的页面,色差本来就很接近。
像素相似度只能证明"没画错",不能证明"该画的都画了"。
5.2 文本提取能力
从转换后的 OFD 里提取字符数(括号内是源 PDF 的参考字符数):
| 10927 | 3375 | 534998 | 234779 | ||||
| 11882 | |||||||
| 0 | |||||||
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 |
java-ofdrw 和 python-pdf2ofd 整行是 0。 不是提取失败 —— 文档说得很清楚:
ofdrw / pdf2ofd 的输出 OFD 不含可提取文本(Content.xml 无 TextObject)——ofdrw 转成矢量轮廓、pdf2ofd 栅格成位图,故相似度为 0。
路线决定的,不是 bug。
rust-easyofd 的 * 标记是"有字符但乱码":
999、1000-pages 提取的文本为 CID 字体乱码(标 * 的 242/10996/10 字符)且版面固定在 (10,20)。
这一行最有意思。字符数不为 0,但没有一个字是人话。 提取工具"提取到了东西",内容全是 CID 编码值。
这比"提取失败"危险得多 —— 归档系统不会报错,它会静静地把一堆乱码索引进全文检索库。
5.3 文本相似度
| 34.55% | 52.11% | 45.20% | 42.01% | ||||
go-zc310 是三个"每一列都不是 0"的转换器中最高的一个。 它在 6 列取得最高相似度(ano 34.55%、999 52.11%、intro 45.20%、zsbk 9.11%、1000-pages 8.25%、GBT 42.01%),hello 列与 go-ofdgo 并列 17.02%。
注:源文档未对这 7 项的相似度排名给"胜出次数",此处"6 列领先"是按表中数值直接比较得出的。另一个"每列非 0"的是
node-jsofd(10.00~19.08%),但它每一列都低于 zc310。
但它的 zsbk 只有 9.11% —— 同样是签章文件,为什么这么低?
文档的解释:
go-zc310 保留文本对象(全部 6 个文件均可提取),文本顺序正确(如「欢迎使用」顺序无误),但字形间插入空格(如
H e l l o)使 4-gram 相似度偏低,内容本身完整。
zsbk 只有 1150 个参考字符,是 7 个样本中最短的一个。(⚠️ 空格插在字形间是源文档对 go-zc310 文本相似度偏低的通用解释;源文档并未单独说明 zsbk 为何只有 9.11%,"短文档放大效应"是本文推测,供参考。)
go-ofdgo 和 node-jsofd 的典型病症:
go-ofdgo 提取 234779 字符(GBT) → 相似度 1.08%node-jsofd 提取 225836 字符(GBT) → 相似度 1.08%提取了 2 倍于参考的字符,相似度却只有 1.08%。 字符多 ≠ 对。文档定性为"顺序/字距差异"。
5.4 方法论:像素相似度的三个坑
文档列了三条,读的时候务必对照:
像素相似度基于整页平均色差,对内容稀疏的页面(白底小字)区分度有限,需结合文本提取与人工核对判断 intro的像素对比跳过 14~19 页(数科参考 PDF 这几页背景图丢失,不计入对比) 回环统一使用 go-zc310 的 OFD→PDF,度量的是「输出 OFD 的互操作正确性」而非端到端体验
六、结论:项目文档怎么说
- go-zc310
:唯一保留可复制文本(顺序正确)且可用的转换器,像素还原 2 胜(ano、GBT 99.98%),在可用转换器中速度/体积最优(45ms~2.87s、1.6K~6.6M) - go-ofdgo
:像素 2 胜(intro 99.80%、zsbk 99.37%)、hello 速度反超 zc310(21ms);文本提取量最大(1000-pages 534998、GBT 234779 字符)但相似度偏低(≤29.63%) - rust-easyofd
:速度与体积数值均第一(各 7 胜)但为虚假优势 —— hello/ano 输出空页、999/1000-pages 文本为 CID 乱码、intro 像素仅 43.84%,GBT 像素 96.28% 亦仅因图表保留(文本 0 字符),实际质量最差 - java-ofdrw
:像素还原 2 胜(hello、1000-pages),文本转为轮廓不可提取;输出体积大(1000-pages 57MB、GBT 30.5M)、多页转换慢(GBT 12.78s) - python-pdf2ofd
:简单易用,单页/小文件还原不错(999 像素最高),但纯位图渲染不可提取文本、大文件最慢(1000-pages 34.2s、GBT 18.5s) - node-jsofd
:像素普遍接近源 PDF(6 文件 ≥98.1%),但体积最大(1000-pages 63.4M、GBT 35.1M)且大文件偏慢;文本相似度低(≤19.08%), ano因 Stamp 未转换像素仅 90.23%
七、怎么把"假第一名"认出来
这是本篇最想留下的方法论。
7.1 通用识别法:三个交叉验证
看到「速度/体积双第一」时,问三个问题: ① 内容在吗? → 文本提取字符数是不是 0 或接近 0? ② 画全了吗? → 像素相似度高,但版面是不是"固定在 (10,20)"? ③ 长得像吗? → 打开文件,肉眼看是不是空页?rust-easyofd 三个全中:文本 0、乱码、版面固定。
7.2 五个危险信号
ano = 0.00 | ||
7.3 最有价值的一个指标组合
文本提取字符数 │ ┌───────────────┼───────────────┐ │ │ │ = 0 少但相似度高 多但相似度低 │ │ │ 「保外观路线」 「可用的保结构」 「切分逻辑要修」 ofdrw/pdf2ofd go-zc310 ofdgo/jsOFD ——能用但搜不了 ——首选 ——需二次开发字符数 0 不可怕,可怕的是"字符数非 0 但内容是乱码"——它会骗过所有自动化检查。
八、怎么用这 6 个库
| PDF→OFD 还要能搜索/复制 | go-zc310 | |
| java-ofdrw | ||
intro 类) | go-ofdgo | |
| go-zc310 | ano 99.75% | |
| python-pdf2ofd | ||
| node-jsofd | ||
| 商用项目 | rust-easyofd PDF→OFD |
通用建议:组合拳。
方案 A(保文本优先) go-zc310 主用 + 人工复核方案 B(保真优先) java-ofdrw / go-ofdgo + 接受 0 文本方案 C(分级路由) 简单文档走 go-zc310 复杂/签章走 go-ofdgo 或 java-ofdrw 两套结果都入库,用文本特征区分标记九、一句话收束
PDF→OFD 选型的第一道题不是「谁最快」,而是: · 你的 OFD 转换后要能被检索吗? → 看文本提取,别看速度 · 能接受 26 倍体积膨胀吗? → 不能,就排除轮廓/栅格路线 · 你的 PDF 有 Stamp 注解吗? → 有,路线必须含注解转换 · 榜单第一的输出你打开看过吗? → 看过再信"7 胜 0 负"和"输出空页"可以同时为真 —— 所以任何榜单,在你没打开输出文件看过之前,都不构成结论。
参考资料
ofd-benchmark/docs/pdf-to-ofd.md(本文全部数据来源) ofd-benchmark/docs/ofd-libraries.md(66 个库能力表,PDF→OFD 仅 16 个支持)