夜雨聆风学习资料网

ARTICLE · 1107284

6 个 PDF 转 OFD 库实测:速度和体积双第一的那个,输出是空页面

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
Go
zc310/ofd
v0.1.3
pdfcpu 导入器
Apache-2.0
go-ofdgo
Go
xiaoqidun/ofdgo2026-09-28 版
pdfgo 导入器
Apache-2.0
rust-easyofd
Rust
easyofd-rust
0.1.4
lopdf 抽取
Apache-2.0
java-ofdrw
Java
ofdrw
2.4.0
文本转轮廓
Apache-2.0
python-pdf2ofd
Python
pdf2ofd
0.1.0
PyMuPDF 栅格化
MIT
node-jsofd
JS
Hufe921/jsOFD
1.0.1
pdfjs 指令流重放
MIT

⭐ 一个"数据会过期"的活证据

本文最刺眼的结论是"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
3.5K
1
简单文本
999.pdf
80K
5
简单文档
zsbk.pdf
1.6M
2
签章文件
ano.pdf
904K
3
发票
intro.pdf
6.7M
42
介绍文档
1000-pages.pdf
2.1M
1000
多页文档
GBT_33190-2016.pdf
4.8M
132
国标正文,字体全内嵌、图表多

三、速度:7 胜,但那是"空转"的 7 胜

转换器
hello
ano
999
intro
zsbk
1000-pages
GBT
胜出
rust-easyofd4.9ms4.1ms7.2ms105ms24ms274ms76ms7
go-zc310
45ms
94ms
64ms
523ms
301ms
917ms
2.87s
0
go-ofdgo
21ms
363ms
414ms
1.17s
6.12s
3.97s
12.91s
0
python-pdf2ofd
99ms
800ms
513ms
2.39s
928ms
34.24s
18.47s
0
java-ofdrw
472ms
1.27s
1.02s
4.37s
2.90s
25.90s
12.78s
0
node-jsofd
416ms
712ms
3.50s
10.01s
5.25s
4.77s
9.02s
0

注意 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 是全场最大的坑

转换器
hello
ano
999
intro
zsbk
1000-pages
GBT
胜出
rust-easyofd1.2K1.8K3.6K5.4M919K432K2.0M7
go-zc310
1.6K
389.7K
47.2K
7.4M
1.5M
3.1M
6.6M
0
go-ofdgo
1.9K
586.1K
73.6K
8.1M
3.1M
5.6M
11.5M
0
java-ofdrw
5.2K
905K
818K
34.4M19.4M57.4M30.5M
0
python-pdf2ofd
4.3K
1.3M
734K
9.1M
1.7M
44.8M37.7M
0
node-jsofd
7.4K
2.2M
591.6K
35.6M13.2M63.4M35.1M
0

看 ano 这一列

源 PDF      904Krust-easyofd   1.8K     ← 比值 0.00go-zc310     389.7K     ← 0.43go-ofdgo     586.1K     ← 0.65

1.8K。 一个 904K 的三页发票,转出来 1.8K。

比值列写得清清楚楚:

转换器
hello
ano
999
intro
zsbk
1000-pages
GBT
rust-easyofd
0.330.000.040.780.570.200.41
go-zc310
0.46
0.43
0.59
1.08
0.93
1.43
1.34
go-ofdgo
0.52
0.65
0.92
1.19
1.90
2.59
2.33
java-ofdrw
1.48
1.00
10.24
5.01
12.07
26.48
6.17
python-pdf2ofd
1.22
1.43
9.18
1.33
1.04
20.63
7.62
node-jsofd
2.08
2.47
7.40
5.18
8.18
29.25
7.09

文档第二次点名:

rust-easyofd「体积最小」同样是内容丢失的结果(hello 1.2K/ano 1.8K 实为空页),不具可比性。

真正的压缩能力看 go-zc310

0.43  0.43  0.59  1.08  0.93  1.43  1.34

7 个样本全部落在 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 像素相似度

转换器
hello
ano
999
intro
zsbk
1000-pages
GBT
胜出
java-ofdrw
99.98%
98.37%
98.73%
99.42%
99.11%
99.62%
99.64%
2
go-zc310
99.96%
99.75%
98.33%
96.95%
97.61%
99.49%
99.98%
2
go-ofdgo
99.96%
99.41%
98.39%
99.80%99.37%
99.49%
99.95%
2
python-pdf2ofd
99.94%
98.34%
98.94%
99.36%
97.62%
99.31%
99.12%
1
node-jsofd
99.89%
90.23%
98.13%
99.23%
98.82%
99.59%
99.94%
0
rust-easyofd
99.84%
98.56%
95.77%
43.84%
95.62%
73.67%
96.28%
0

三点观察:

① 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 的参考字符数):

转换器
hello (25)
ano (7139)
999 (3934)
intro (5546)
zsbk (1150)
1000-pages (323700)
GBT (107779)
go-ofdgo
41
9882
10163
109273375534998234779
node-jsofd
41
11882
10039
10478
2899
510996
225836
go-zc310
37
9443
9746
7612
2698
420998
170740
rust-easyofd
0
0
242*
0
10*
10996*
0
java-ofdrw
0000000
python-pdf2ofd
0000000

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 文本相似度

转换器
hello
ano
999
intro
zsbk
1000-pages
GBT
go-zc310
17.02%
34.55%52.11%45.20%
9.11%
8.25%
42.01%
go-ofdgo
17.02%
27.13%
29.63%
13.71%
4.63%
7.34%
1.08%
node-jsofd
10.00%
19.08%
11.27%
13.58%
4.50%
5.49%
1.08%
rust-easyofd
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
java-ofdrw
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
python-pdf2ofd
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%

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 五个危险信号

信号
含义
本例
体积比 0.00~0.05
内容丢失,不是压缩
rust-easyofd ano = 0.00
文本提取 0 但像素 >95%
轮廓化或栅格化
ofdrw / pdf2ofd
有字符但全是 CID 码
编码映射丢失
rust-easyofd 242/10996
版面坐标固定不变
没用真实布局
rust-easyofd (10,20)
字符数 ≫ 参考但相似度 <2%
顺序/切分错乱
ofdgo / jsOFD GBT 1.08%

7.3 最有价值的一个指标组合

                    文本提取字符数                         │         ┌───────────────┼───────────────┐         │               │               │         = 0          少但相似度高     多但相似度低         │               │               │    「保外观路线」    「可用的保结构」  「切分逻辑要修」    ofdrw/pdf2ofd     go-zc310       ofdgo/jsOFD    ——能用但搜不了    ——首选          ——需二次开发

字符数 0 不可怕,可怕的是"字符数非 0 但内容是乱码"——它会骗过所有自动化检查。


八、怎么用这 6 个库

你的需求
首选
理由
PDF→OFD 还要能搜索/复制go-zc310
唯一"可用 + 有序 + 可提取"三者兼得
要最高像素还原(且不搜字)
java-ofdrw
hello 99.98%、1000-pages 99.62%
复杂图形文档(intro 类)
go-ofdgo
intro 99.80% 全场最高
发票/签章
go-zc310
读 Stamp 注解,ano 99.75%
纯展示、无需文本、体积无所谓
python-pdf2ofd
最简单,但 0 文本 + 34s/1000 页
Node 环境 + 要文本
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 个支持)

相关学习资料