ARTICLE · 1116237
怎么自己跑一遍 OFD⇄PDF 基准测试,以及这份数据的 9 条局限
前四篇的所有结论都来自 ofd-benchmark 项目的实测。
这一篇讲两件事:
- 怎么自己跑一遍
(完整复现命令) - 这份数据有哪些不能当真
(9 条局限,请务必读完再引用)
先说第二件,因为更重要。
一、9 条数据局限(引用本文前必读)
项目文档自己列出了这些,我原样列出并逐条解释它意味着什么。
局限 1:只跑了一次
本测试仅运行 1 次,无方差/置信区间,无法判断差异是否显著。
这意味着:rust-easyofd 45ms vs go-ofdgo 102ms,这个 2 倍差距可能全部来自那次运行的系统抖动,而不是库本身的速度差。
参考量级:1000-pages 上 rust 951ms vs ofdgo 7.88s,是 8 倍 —— 这种量级的差异基本可信。 但 45ms vs 102ms 这种,不可信。
实操建议:只引用差距 5 倍以上的结论,2~3 倍的一律标注"单次数据"。
局限 2:没记录机器规格
未记录机器规格(CPU 型号/频率/核数/内存/是否 SSD),绝对耗时不可移植。
这意味着:本文所有毫秒数不能拿去做 SLA。同一份代码在 M2 上和在做旧的双核服务器上,差距可能是 5 倍以上。
能用的只有相对排名,而且排名本身也受机器负载影响(尤其对 IO 密集和多线程并行的库)。
局限 3:Java / Node 未预热
Java/Node 未预热(JIT 未达稳态),首次运行吃亏,"Java 最慢"的结论需谨慎。
这意味着:02 篇里 java-ofdrw 速度 0 胜,很大一部分可能是 JVM 启动 + 类加载的成本(hello 507ms 里有相当比例是这些固定开销,源文档未拆分)。
同理 node-ofd2pdf 的 287ms 可能含 Node 冷启动。
实操建议:JVM 栈上生产环境(长期运行的容器)会预热,基准测试的劣势在实际业务里会缩小很多。不要因为这张表就把 ofdrw 排除。
局限 4:没有冷/热启动分离
无冷启动/热启动分离,单次测量包含类加载、字体扫描等开销。
这意味着:短文档(hello 1.5K)的耗时里,开销占比可能远大于实际转换耗时。hello 一栏的 45ms vs 507ms,测的更多是"启动成本"而非"转换速度"。
所以看 intro(7.2M)和 1000-pages(1000 页)两栏的差距,比 hello 有意义得多。
局限 5:intro 的像素对比跳过了 6 页
intro的像素对比跳过 14~19 页(数科导出的参考 PDF 这几页背景图丢失,不计入对比)。
这意味着:基准本身有缺陷,做了排除处理。这个处理是正确且必要的,但引用 intro 的像素数字时要知道它不是全页统计。
局限 6:像素指标的固有弱点
像素相似度基于整页平均色差,对内容稀疏的页面(白底小字)区分度有限,需结合文本提取与人工核对判断。
这意味着:03 篇里 rust-easyofd 对 132 页国标拿到 96.28% 像素相似度,而它的文本提取是 0 字符。因为国标页面留白多,空页和白底页的色差天然接近 0。
像素相似度只能证明"没画错",不能证明"该画的都画了"。
局限 7:回环测试的口径代价
回环统一使用 go-zc310 的 OFD→PDF,度量的是「输出 OFD 的互操作正确性」而非端到端体验:
若改用各库自回环,写错坐标/格式可被自家读取器按同错方式读回而掩盖(如 ofdgo 的 16 位色值只有换读取器才暴露) 代价是引入 zc310 渲染质量偏差,数据仅供参考
这意味着:03 篇的回环分数不是"这个库的最终输出有多好",而是"这个库写的 OFD 能不能被第三方正确读取和渲染"。
这个口径是有意为之且正确的(统一回环能暴露互操作问题),但读者必须知道它引入的偏差。
局限 8:结论由 AI 自动汇总
以上结论由 AI 根据测试数据自动汇总生成。
这意味着:文字结论可能有过度解读。请以表格原始数据为准,不要只看结语文本。
局限 9:结论绑定版本,且版本会走(最重要的一条)
这是全系列最容易被忽略、也最影响决策的局限。
本系列所有性能与质量数字,都只对被测的那个版本成立。而 OFD 生态多在 0.x/1.0 早期阶段,迭代非常快。
9.1 本次测试的版本快照
OFD → PDF(8 个)
PDF → OFD(6 个)
9.2 版本跨度带来的解读偏差
2026-04-27 easyofd(Python) ← 距测试约 5 个月,半年没更新2026-05-07 ofd2img2026-07-08 ofd2pdf(Python)2026-08-03 @miconvert/ofd-to-pdf2026-08-04 ofdrw2026-09-25 ofdgo PDF→OFD 修复版2026-09-28 zc310 / ofdgo / easyofd-rust同一次测试里,有的库是三天前刚发布的,有的已经五个月没动。
对失败的解读要格外小心:
「easyofd 转换失败 4 次」 ✗ 不能直接读成「这个方案不行」 ✓ 可能是「半年没人修」—— 维护状态和实现能力是两件事9.3 一个已经发生过的"数据过期"实例
go-ofdgo 的 PDF→OFD 方向,工程记录写着:
7 个测试文件 7/7 全部成功(2026-09-25 15:21 版修复非 Identity-H 复合字体编码、Stamp 注解、nil panic)
三个 bug 在一次提交里修完。 测试跑在那次提交之前,这一行会全是 FAILED;跑在之后,就是 7/7 通过。
同一时期,"16 位色值"问题也已被"新版 zc310 ofd-converter 兼容读取"绕过。
结论:本文关于 go-ofdgo 的数据,有效期可能只有几天。
9.4 哪些结论不会过期
✅ 不会过期 许可证状态(无许可证就是无许可证,不会因升级改变)⚠️ 大概率不会过期(本文归纳,源文档未如此表述) · 路线层面的结构性事实 —— 轮廓化必然丢文本、栅格化必然丢文本和矢量 (源文档记录了 ofdrw「转成矢量轮廓」、pdf2ofd「栅格成位图」导致 0 文本, "必然"二字是本文从这些观测推出的推广) · 指标解读方法 —— 像素 vs 文本该怎么一起看⚠️ 会过期 任何具体库的成败、排名、速度、体积第四、五节的"识别假第一名的方法"比任何具体排名都更值得记住。
9.5 复现时如何锁版本
想复现出同样的数据,必须锁版本:
# Python —— 锁死版本(否则 pip 会装最新版)pip install "easyofd==20260427""ofd2img==0.1.2"pip install "pdf2ofd==0.1.0"# Go —— 记录而非锁定(go.mod 里 pin 住)grep -E 'zc310/ofd|xiaoqidun/ofdgo' go.modgo list -m all | grep -E 'ofd'# Rust —— Cargo.lock 已锁,需检查是否被更新grep -A1 'easyofd' rust/pdf2ofd/Cargo.lock# Node —— 保留 package-lock.json,别跑 npm updatecd node && npm ci && cd ..# ⚠️ Node <22 需补 process.getBuiltinModule(见 node/pdf2ofd_jsofd.mjs)# Java —— JAR 手工下载,lib/ 里就是 2.4.0ls java/lib/⚠️ 复现时如果不锁版本,你得到的将是"当前版本"的数据,与本文数字不同 —— 这不说明本文错了,说明版本变了。
9.6 选型时怎么用这份数据
✗ 「rust-easyofd 0.1.4 是垃圾」 → 版本判断✗ 「go-ofdgo 2026-09-25 版最可靠」 → 版本判断✓ 「PDF→OFD 质量与版本强相关,锁版本 + 自测」✓ 「文本丢失若源于轮廓化/栅格化路线,那是路线决定,换版本也救不了」最后一条尤其重要:区分"版本会修的 bug"和"路线决定的固有限制",否则你会为"等上游修复"或"换库"做错决策。判断方法:
文本丢失,Content.xml 里没有 TextObject → 轮廓化/栅格化路线 → 固有限制文本有,但 CID 乱码 → 编码映射 bug → 等修复或换库输出空页 / 版面固定在 (10,20) → 布局解析未实现 → 等修复或换库📌 上表左两列是实测观测(源文档明确记录);右列的归因是本文推断 —— 源文档没有点名 (10,20) 的成因是"布局解析未实现",也没有把 CID 乱码归因为"编码映射 bug"。把它们当成排查线索可以,当成确定结论则超出了源文档。
原文的最后提醒
建议:手工核对输出 PDF 效果,选择合适的库。不同 OFD 文件结构差异较大,实际效果可能与基准测试结果不同。
测试输出 PDF 文件可在 Releases 页面下载。
⭐ 最省事的验证方式:直接下载 Releases 里的输出 PDF,打开看一眼。 比读十遍表格都有用。
二、复现:环境准备
2.1 基础环境
系统 Ubuntu 26.04.1 LTS (64-bit)字体 apt-get install fonts-wqy-zenhei fonts-noto-cjk 字体目录 ~/.local/share/fonts ⚠️ 中文字体缺失会导致中文渲染异常,指标全部失真2.2 测试文件
来源:https://github.com/zc310/ofd/tree/main/test/testdata
# 需从本地复制(网络下载可能超时)cp /home/go/workspace/ofd/test/testdata/* /home/go/workspace/ofd-benchmark/test/testdata/⚠️
test/testdata/*.pdf是数科版式阅读器的参考输出,是像素对比基准,勿删除。它们同时是 PDF→OFD 的输入。
三、复现:OFD→PDF
3.1 编译
cd /home/go/workspace/ofd-benchmark# ⚠️ 上级存在 go.work 时必须关闭 workspaceexport GOWORK=offgo build -o bin/ofd-benchmark .go build -o bin/ofdgo-convert ./cmd/ofdgo-convertgo build -o bin/zc310-convert ./cmd/zc310-convertcp/easyofd bin/easyofd 常见报错
not one of the workspace modules→ 就是GOWORK=off没加。
3.2 各语言依赖
# Pythonpip install easyofd ofd2imgpip install pdf2ofd # PEP 668 环境需 --user --break-system-packages# Java:需排除损坏的占位 jar 才能通过 javaccd javaCP=$(ls lib/*.jar | rg -v 'jaxen|pdfbox-sandbox|sqlite' | tr'\n'':')javac -cp"$CP" Ofd2Pdf.java# ofdrw PDF→OFD 打包 OFD 还需 zip4j(必须在 java/ 目录下下载)curl -fsSL -o lib/zip4j.jar \ https://repo1.maven.org/maven2/net/lingala/zip4j/zip4j/2.11.5/zip4j-2.11.5.jar# ⚠️ 运行时 bin/java/ofd2pdf_java.sh 从 $SCRIPT_DIR/lib/*.jar 组 classpath,# 所以 bin/java/lib/ 也必须放一份,否则 java 阶段会因缺 zip4j 打包失败mkdir -p ../bin/java/lib && cp lib/zip4j.jar ../bin/java/lib/cd ..# Nodecd node && npm install && cd ..📌
java/lib/里有 3 个损坏占位 jar(jaxen / pdfbox-sandbox / sqlite,各 554 字节):javac需排除,运行时java会自动忽略。这不是你的环境问题。
3.3 运行
# 全部转换器./bin/ofd-benchmark test/testdata/hello.ofd# 指定转换器./bin/ofd-benchmark test/testdata/hello.ofd ofdgo./bin/ofd-benchmark test/testdata/hello.ofd zc310./bin/ofd-benchmark test/testdata/hello.ofd rust./bin/ofd-benchmark test/testdata/hello.ofd python./bin/ofd-benchmark test/testdata/hello.ofd java./bin/ofd-benchmark test/testdata/hello.ofd node# 批量for f in hello intro ano 1000-pages 999 zsbk; do ./bin/ofd-benchmark "test/testdata/${f}.ofd"done输出在 bin/output/,命名 {原文件名}_{转换器}.pdf。
3.4 对比分析
# 像素对比(96 DPI),可传多个文件名python3 tools/compare_pdf.py 96 hello ano 999 1000-pages intro zsbk# 文本相似度(4-gram Jaccard)python3 tools/compare_text.py⚠️
1000-pages用 96 DPI 做像素对比约需 3.5 分钟,别以为卡死了。
四、复现:PDF→OFD
# 编译 zc310 通用转换器cd /home/go/workspace/ofdGOWORK=off go build -o ../ofd-benchmark/bin/ofd-converter ./cmd/ofd-convertercd ../ofd-benchmarkGOWORK=off go build -o bin/pdf2ofd-benchmark ./cmd/pdf2ofd-benchmarkGOWORK=off go build -o bin/ofdgo-pdf2ofd ./cmd/ofdgo-pdf2ofd# Javacd java && CP=$(ls lib/*.jar | rg -v 'jaxen|pdfbox-sandbox|sqlite' | tr'\n'':')javac -cp"$CP" Pdf2Ofd.java && cp Pdf2Ofd.class ../bin/java/ && cd ..# Rustcargo build --release --manifest-path rust/pdf2ofd/Cargo.tomlcp rust/pdf2ofd/target/release/pdf2ofd bin/easyofd-pdf2ofd# 运行./bin/pdf2ofd-benchmark test/testdata/hello.pdf./bin/pdf2ofd-benchmark test/testdata/hello.pdf zc310 ofdgo jsofdfor f in hello ano 999 intro zsbk 1000-pages GBT_33190-2016; do ./bin/pdf2ofd-benchmark "test/testdata/${f}.pdf"done输出在 bin/output_ofd/:.ofd + 回环 .pdf + 提取文本 .txt。
# 回环还原度 / 文本对比python3 tools/compare_pdf2ofd.py 96五、项目结构(便于定位代码)
ofd-benchmark/├── main.go # OFD→PDF 基准主程序(单文件输入)├── cmd/│ ├── ofdgo-convert/ # ofdgo OFD→PDF 封装│ ├── ofdgo-pdf2ofd/ # ofdgo PDF→OFD 封装│ ├── zc310-convert/ # zc310 OFD→PDF 封装│ └── pdf2ofd-benchmark/ # PDF→OFD 基准驱动├── python/│ ├── ofd2pdf.py # easyofd│ ├── ofd2pdf_ofd2pdf.py # ofd2pdf│ ├── ofd2pdf_ofdreader.py # ofdreader│ └── pdf2ofd_convert.py # pdf2ofd├── java/│ ├── Ofd2Pdf.java # ofdrw OFD→PDF│ ├── Pdf2Ofd.java # ofdrw PDF→OFD│ └── lib/ # JAR 依赖(含 zip4j)├── node/│ ├── ofd2pdf.mjs # @miconvert/ofd-to-pdf│ └── pdf2ofd_jsofd.mjs # @hufe921/jsofd + pdfjs-dist├── rust/│ ├── ofd2pdf/ # easyofd OFD→PDF│ └── pdf2ofd/ # easyofd PDF→OFD├── bin/│ ├── output/ # OFD→PDF 结果│ └── output_ofd/ # PDF→OFD 结果(含回环 PDF / 文本)├── test/testdata/ # OFD/PDF 样本├── tools/│ ├── compare_pdf.py # 像素对比│ ├── compare_text.py # 文本相似度│ └── compare_pdf2ofd.py # 回环还原度/文本└── docs/ ├── ofd-libraries.md # 66 个库能力表 ├── ofd-to-pdf.md # OFD→PDF 结果 └── pdf-to-ofd.md # PDF→OFD 结果六、指标口径(复现时对齐用)
0.00 | ||
1 - 平均归一化色差 | ||
提取文本的参考写法:
import fitz # pip install pymupdfdoc = fitz.open("bin/output/hello_rust-easyofd.pdf")text = ""for page in doc: text += page.get_text()print(len(text.strip()))七、已知故障清单(复现前先知道)
这些是项目文档记录的既有问题,不是你的环境问题。
⚠️ 本表是版本快照,不是永久状态。 逐行标注了"这问题还会不会存在"的判断 —— 修 bug 的那些很可能已经修好,而路线决定的固有限制不会变。
python-ofdreader | 始终 FAILED | ||
python-easyofd | hello1000-pages crash(list index out of range) | ||
python-ofd2pdf | ano、intro 失败 | ||
rust-easyofd | |||
go-ofdgo | |||
node-jsofd | process.getBuiltinModule | ||
ofdrwpdf2ofd | |||
java/lib/ | |||
intro |
读这张表的正确方式:
🚫 路线固有限制(换版本也救不了) ofdrw / pdf2ofd 的 PDF→OFD 无文本 —— 因为它们把文字转成轮廓/位图 → 决策:接受,或换库🔧 版本会修的 bug(别急着永久排除某个库) easyofd 的 crash、rust-easyofd 的空页与乱码 → 决策:测当前版本,锁版本后再下结论八、给你的实测方案(比跑整套基准更实用)
完整基准要编 5 种语言(Go / Rust / Python / Java / JavaScript)、装 4 套依赖,很多人跑不完。但选型其实只需要验一个库。
最小可行验证(约 30 分钟)
# 1. 准备样本:把手上真实的 OFD/PDF 放进 test/mkdir -p mytest && cp ~/业务文档/*.ofd mytest/# 2. 只编你候选的那一个(假设 Go 栈)export GOWORK=offgo build -o bin/zc310-convert ./cmd/zc310-convert# 3. 逐个转for f in mytest/*.ofd; do ./bin/zc310-convert "$f""out_$(basename ${f%.ofd}).pdf"done# 4. 三项检查python3 - <<'PY'import fitz, glob, osfor p in sorted(glob.glob("out_*.pdf")): d = fitz.open(p) txt = "".join(pg.get_text() for pg in d) n = len(txt.strip())print(f"{os.path.basename(p):40s} {os.path.getsize(p)/1024:8.1f}KB " f"pages={len(d):4d} chars={n:7d} " f"{'⚠️ 无文本' if n==0 else '✓'}")PY# 5. 人工打开看几个(这步不能省)加两项门禁
# 文本非乱码检查(关键:拦 CID 乱码)deflooks_like_garbage(t):ifnot t: returnTrue cjk = sum(1for c in t if'\u4e00' <= c <= '\u9fff') latin = sum(1for c in t if c.isalnum() andord(c) < 128)return (cjk + latin) / len(t) < 0.5# 体积门禁(输出 ÷ 源):<0.1 疑似内容丢失,>5 疑似异常膨胀python3 -c "import os,sysr = os.path.getsize(sys.argv[2]) / os.path.getsize(sys.argv[1])print(f'体积比 {r:.2f}' + (' ⚠️ 疑似内容丢失' if r < 0.1 else (' ⚠️ 膨胀过大' if r > 5 else ' ✓')))" in.ofd out.pdf三条硬性阈值建议
文本字符数 ≥ 参考的 50%(或至少 > 0 且非乱码)像素相似度 ≥ 98%(按业务松紧调整)体积比 0.3 ~ 3.0(超出先查是否有内容丢失/异常膨胀)九、引用这份数据时的注意事项
□ ★ 引用任何具体库的成败/排名/速度/体积 → 必须附该库版本号□ 引用毫秒数 → 必须注明"单次运行、机器规格未记录"□ 引用 2~3 倍差距 → 标注"未验证显著性"□ 说"Java 最慢" → 补"未预热,JIT 冷启动"□ 说某库"还原度高" → 补"像素指标对稀疏页面区分度低"□ 引用 PDF→OFD 回环分数 → 补"统一用 zc310 回环,测的是互操作性"□ 引用 rust-easyofd 的速度/体积第一 → 必须同时说明输出空页 + 标注版本 0.1.4□ 无许可证的库 → 不得推荐进商用项目(★ 这条不会过期,可长期引用)□ 说某库"有 bug" → 先分清是"版本会修"还是"路线固有限制"(见局限 9.6)十、整个系列的收束
六篇看下来,可以浓缩成七条:
1. OFD→PDF 是翻译,PDF→OFD 是考古 —— 难度不对等2. 「转换成功」≠「转换出的文件能用」—— 文本层是分水岭3. 像素相似度高 ≠ 画全了 —— 稀疏页面上空页得分很高4. 体积比 0.00 不是压缩好 —— 是内容丢了5. 榜单第一必须打开输出文件验证过6. 一切结论都只对样本成立 —— 用你自己的文档重测7. ★ 一切结论都只对版本成立 —— 引用必须带版本号第 7 条是这一轮补充的,也是最容易被忽略的。它的存在有一个已经发生的实证:
go-ofdgo PDF→OFD 2026-09-25 15:21 一次提交修掉 3 个 bug → 7 个测试文件从「不确定」变成 7/7 全通过 → 本文关于 go-ofdgo 的数据,有效期可能只有几天区分"会被修掉的 bug"和"改不了的路线",是本系列最实用的一条判断:
🔧 空页 / 乱码 / crash / 不转某些特性 → 等版本,或换库🚫 轮廓化必然丢文本、栅格化必然丢文本和矢量 → 接受,或换路线最后一句留给实践:这份数据能做的是"把明显不行的排除掉",做不了的是"告诉你哪个一定行"。
最后那一步,永远得你自己走。
全部数据出处
ofd-benchmark/docs/ofd-to-pdf.md(8 转换器) ofd-benchmark/docs/pdf-to-ofd.md(6 转换器) ofd-benchmark/docs/ofd-libraries.md(66 个库) ofd-benchmark/README.md测试输出 PDF/OFD:https://github.com/zc310/ofd-benchmark/releases