夜雨聆风学习资料网

ARTICLE · 1116237

怎么自己跑一遍 OFD⇄PDF 基准测试,以及这份数据的 9 条局限

怎么自己跑一遍 OFD⇄PDF 基准测试,以及这份数据的 9 条局限

前四篇的所有结论都来自 ofd-benchmark 项目的实测。

这一篇讲两件事:

  1. 怎么自己跑一遍
    (完整复现命令)
  2. 这份数据有哪些不能当真
    (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 个)

转换器
版本
库更新
go-zc310
v0.1.3
2026-09-28
go-ofdgo
v0.0.0-20260928111512
2026-09-28
rust-easyofd
v0.1.4
2026-09-28
python-easyofd
20260427(+ ofd2img 0.1.2)
2026-04-27 / 2026-05-07
python-ofd2pdf
0.0.2
2026-07-08
java-ofdrw
2.4.0
2026-08-04
node-ofd2pdf
0.2.3
2026-08-03
python-ofdreader
—(未安装)
—

PDF → OFD(6 个)

转换器
版本
go-zc310
v0.1.3 起(pdfimport)
go-ofdgo
2026-09-28 版
rust-easyofd
0.1.4
java-ofdrw
2.4.0
python-pdf2ofd
0.1.0
node-jsofd
1.0.1

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 结果

六、指标口径(复现时对齐用)

指标
定义
陷阱
转换耗时
单次运行时间
含进程启动/类加载/字体扫描;未预热
PDF/OFD 文件大小
输出字节数
0 字符时无意义
大小比
输出 ÷ 参考(或 ÷ 源)
0.00
 = 内容丢失,非压缩
像素相似度
96 DPI 渲染,1 - 平均归一化色差
稀疏页面区分度低;系统奖励"空白"
文本提取
PyMuPDF 提取的字符数
0 = 无文本层;非 0 可能是 CID 乱码
文本相似度
4-gram Jaccard
对顺序/换行/字距敏感;空格会大幅拉低
胜出次数
每列第一的计数
并列未标注;只反映相对位置

提取文本的参考写法:

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
⚠️ 环境问题:源文档记为「模块未安装,该转换器始终 FAILED」,属本机环境而非库能力;本文未测过它
python-easyofd
20260427
hello
、1000-pages crash(list index out of range)
🔧 会修(半年未更新的库,随时可能补)
python-ofd2pdf
0.0.2
基于图片渲染,无法提取文本;ano、intro 失败
🔧 失败会修 / 🚫 "无文本"由路线决定,不因升级而变
rust-easyofd
(PDF→OFD)
0.1.4
hello/ano 输出空页;999、1000-pages 文本 CID 乱码;版面固定 (10,20)
🔧 观测到的实现缺失,作者可能修(本文推断)
go-ofdgo
(PDF→OFD)
2026-09-28
写 16 位色值
✅ 读取端已兼容(源文档:「新版 zc310 ofd-converter 已兼容读取(回环可测)」)
node-jsofd
1.0.1
不转换注解/渐变/图案/Type3/剪裁;Node <22 需补 process.getBuiltinModule
🔧 缺失项会补 / ⚠️ Node 版本是你自己的环境问题
ofdrw
 / pdf2ofd
2.4.0 / 0.1.0
PDF→OFD 输出不含可提取文本(轮廓 / 位图)
🚫 由路线决定:除非换路线,升级不会带来文本
java/lib/
—
3 个损坏占位 jar(554 字节),javac 需排除
✅ 环境问题,重下依赖即可
intro
 对比
—
自动跳过 14~19 页(数科参考 PDF 背景图丢失)
✅ 基准数据固有问题,长期存在

读这张表的正确方式:

🚫 路线固有限制(换版本也救不了)   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

相关学习资料