每天一个开源项目#62 pdf-inspector:0.47秒解析200份PDF
GitHub Trending 第 13/13 名|快照日期:2026-08-07|历史 Stars / Forks:快照未保留|Rust|MIT
项目地址:github.com/firecrawl/p...
历史源码快照:
f731e1191ca8c3adeda4a12be44a6c5cae0ab807(2026-08-06 19:52 UTC,8 月 7 日 16:00 定时快照之前的 main 最新提交)
📋 项目概览
| 字段 | 内容 |
|---|---|
| 项目名 | firecrawl/pdf-inspector |
| 一句话定位 | 用纯 Rust 对 PDF 做类型判断、位置感知文本抽取和结构化 Markdown 转换,并把真正需要 OCR 的页面单独标出来 |
| 历史 Trending 排名 | 2026-08-07 第 13 名,共捕获 13 个项目 |
| Stars / Forks | 8 月 7 日快照未保留;2026-08-10 GitHub API 补充复核为 14,102 / 960 |
| Open Issues | 125(2026-08-10 API 补充复核;GitHub 的该字段同时包含未关闭 Issue 与 PR) |
| 主语言 | Rust;2026-08-10 API 语言字节占比约 Rust 96.09%、Python 1.82%、HTML 1.77%、JavaScript 0.31% |
| License | MIT |
| 历史源码版本面 | Rust crate 0.1.7;PyPI 0.2.6;Node 包 1.12.0;WASM crate 0.1.3,四个发布面并非同一版本号 |
| 核心依赖 | lopdf、rayon、regex、unicode-normalization、ttf-parser;Python 使用 PyO3,Node 使用 napi-rs,浏览器使用 wasm-bindgen |
| 本文验证 | 历史提交上 cargo test --all-targets:976 通过、0 失败;CLI 对仓库 PDF fixture 的分类和 Markdown 抽取冒烟均成功 |
🔥 为什么值得关注
PDF 流水线最常见的浪费,不是 OCR 本身不够强,而是无论文件是否已经有可提取文本,都先走昂贵的 OCR 或视觉模型。原生文本 PDF、扫描件、带隐藏文本层的扫描件、图像占主导的页面、字体编码损坏的页面,经常被当成同一种输入处理,结果是延迟、成本和输出质量一起失控。
pdf-inspector 的价值在于把这件事拆成可解释的路由问题:它先读 PDF 的页面树、内容流、字体和图像操作符,判断每页到底有没有可信的文本证据,再决定本地抽取还是转给 OCR。输出不只是一个 text_based/scanned 标签,还包含置信度、需要 OCR 的页码和原因码,例如 scanned、no_text、vector_text、suspected_garbled_text。这比"整份文档二选一"更适合批量文档处理。
8 月 7 日榜单前列的 TencentDB-Agent-Memory、agent-skills、cloudflare/computer、code-review-graph 和 superpowers 已在历史归档中作为主角分析过。pdf-inspector 虽然只排第 13 名,却提供了更少重复的技术主题:PDF 内容流解释、字体解码、版面阅读顺序、表格重建和跨语言绑定。它不是给 LLM 再套一层提示词,而是把文档进入模型前最脏、最确定的一层工程问题做成可执行基础设施。
🏗️ 核心特性
1. 先分类,再按页路由 OCR
默认 DetectionConfig 不是只看第一页,也不是遇到一页图片就把整份文档判成扫描件。源码默认使用 ScanStrategy::Sample(8),从文档中均匀抽取最多 8 页,结合以下信号判断:
Tj/TJ文本操作符数量;Do图像/XObject 调用;- 每页唯一字符与字母数字字符数量;
- 图像数量与文本操作符比例;
- 大面积模板图像;
- Type3 字体、Identity-H/V 字体及 ToUnicode 可用性;
- 大量路径绘制但缺少文本操作符的"轮廓字";
- 高密度文本与字体切换比例,用于识别复杂报纸版式。
默认阈值在历史提交的 src/detector.rs 中可直接核验:
rust
DetectionConfig {
strategy: ScanStrategy::Sample(8),
min_text_ops_per_page: 3,
text_page_ratio_threshold: 0.6,
}
分类结果是四种,而不是简单布尔值:
| 类型 | 含义 | 推荐后续动作 |
|---|---|---|
TextBased |
大部分采样页存在可信可解码文本 | 本地抽取并生成 Markdown |
Scanned |
没有文本操作符,主要是页面图像 | 交给 OCR |
ImageBased |
有少量文本信号,但主体是图片或轮廓字 | 优先 OCR 或视觉解析 |
Mixed |
文本页与图像页混合,或模板图像承载关键信息 | 只 OCR pages_needing_ocr |
2. PDF 内容流状态机,而不是字符串抓取
真正的 PDF 文本并不按阅读顺序存在。字符可能被拆成多个绘制指令,字体编码可能依赖 CMap,坐标还会经过当前变换矩阵。项目的 src/extractor/content_stream.rs 维护图形状态、文本矩阵、字体、字宽、旋转和 XObject 递归,遍历内容流后生成带坐标与样式的 TextItem:
text
PDF Content Stream
├─ BT / ET 文本对象边界
├─ Tf 当前字体与字号
├─ Tm / Td / TD 文本矩阵与位置
├─ Tj / TJ 字符串与字距数组
├─ Do 图像或 Form XObject
├─ re / m / l / S / f 矩形、线条、路径
└─ q / Q / cm 图形状态栈与坐标变换
↓
TextItem{text,x,y,width,height,font,style,page,mcid}
PdfRect / PdfLine / Image bbox
这一步是后续标题、列表、表格、链接和多栏阅读顺序能够成立的基础。若只从 PDF 中抽取一个纯文本字符串,几乎所有布局证据都会在进入 Markdown 转换前丢失。
3. 字体解码:ToUnicode、CID 和嵌入字体多级回退
PDF 中"看得见但复制出来是乱码"往往不是 OCR 问题,而是字体编码问题。项目会:
- 优先解析 ToUnicode CMap;
- 对 Type0 / Identity-H 字体检查 CID 到 Unicode 的映射;
- 检查
W数组是否呈现可直接解释的 CID; - 尝试从嵌入 TrueType/OpenType 字体的
cmap表恢复字符; - 对 Adobe CJK 预定义 CMap 使用内置映射;
- 如果仍不可解码,将页面标为
suspected_garbled_text,而不是输出看似成功的乱码。
仓库里 src/adobe_korea1.rs 与 src/glyph_names.rs 两个生成映射文件合计 21,663 行,它们解释了为什么 GitHub 代码体量看起来很大,也提醒我们不能把全部物理行数都当成手写算法代码。
4. 多栏阅读顺序使用"证据门控"的区域图
普通"按 Y 从上到下、按 X 从左到右"会把双栏论文读成左右交叉。项目的 src/extractor/reading_order.rs 把页面切成区域节点,并按拓扑顺序输出:
text
┌──────────────────────────────┐
│ 全宽标题 / 图片 │ above
├──────────────┬───────────────┤
│ 左栏内容 │ 右栏内容 │ left → right
├──────────────┴───────────────┤
│ 全宽尾注 / 标题 │ below
└──────────────────────────────┘
它不会看到两个 X 坐标簇就贸然分栏,而是要求多行对齐、足够的栏间距、两侧正文证据以及图像锚点。RTL 文本会把栏顺序改成右栏优先。源码还专门拒绝页眉 Logo、横幅图、装饰图等容易造成假双栏的场景。
5. 表格识别是多路证据竞争,而不是一个正则
表格是 PDF 转 Markdown 最容易"看起来能跑、实际一塌糊涂"的部分。pdf-inspector 同时使用:
- 矩形网格 :从
re等绘图操作提取单元格边框,用 union-find 聚类; - 线段网格:从横竖线重建行列边界;
- 文本对齐启发式:按字号、Y 区域、X 聚类和跨行列对齐找无边框表格;
- 结构树:对 tagged PDF 读取语义表格结构;
- 区域提示:边框不足以形成完整网格时,仍用其包围盒限制启发式搜索;
- 反例门禁:排除图表柱形、段落条纹、目录、键值布局、修订红线和页面背景矩形。
矩形聚类虽然保留了 O(n²) 的两两重叠判断,但组件一旦超过 2,000 个矩形就停止继续扩张,防止复杂矢量图把检测拖到分钟级。这个实现细节比"支持表格"四个字更能体现项目的工程成熟度。
6. 一套 Rust 核心,覆盖四种调用环境
| 接入面 | 技术 | 历史提交中的版本 | 关键边界 |
|---|---|---|---|
| Rust crate / CLI | 原生 Rust | 0.1.7 |
原生构建启用 lopdf/rayon 并行解析 |
| Python | PyO3 + maturin | 0.2.6 |
CPython ≥3.8,类型桩随包发布 |
| Node.js | napi-rs | 1.12.0 |
发布多个平台原生二进制包 |
| Browser | wasm-bindgen | 0.1.3 |
单线程、嵌入 CMap、不要求 cross-origin isolation |
这些版本号是不同发布面的独立演进,不应压缩成一个"项目最新版"。浏览器版为了兼容性关闭 rayon,解析发生在当前线程;大文件应放到 Web Worker 中,否则会阻塞页面。
🔬 技术架构深度解析
1. 总体数据流
text
┌─────────────────────────┐
PDF path / bytes ──►│ validate + lopdf load │
│ 一次加载,共享 Document │
└────────────┬────────────┘
│
┌──────────────────┴──────────────────┐
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ detector │ │ extractor │
│ 页面采样、文本/图像/字体信号 │ │ 内容流状态机、字体、XObject │
│ PdfType + OCR 页级原因 │ │ TextItem / Rect / Line │
└─────────────┬────────────┘ └─────────────┬────────────┘
│ │
│ ┌──────────────┼──────────────┐
│ ▼ ▼ ▼
│ reading order tables links/forms
│ 区域图/RTL 多路检测 注解与字段
│ └──────────────┼──────────────┘
│ ▼
│ Markdown analysis
│ 字号统计、标题层级、列表、代码块
│ │
└──────────────────┬──────────────────┘
▼
PdfProcessResult / per-page Markdown / CLI JSON
│
TextBased ─────┴─────► 本地完成
Mixed/Scanned ───────► 按页路由 OCR
核心 API 的重要优化是"一次加载"。process_pdf* 先得到共享的 lopdf::Document,分类和抽取复用它,避免为了判断类型和生成文本分别解析同一文件。
2. 分类不是只数 Tj/TJ
一个"扫描件带隐藏 OCR 层"的页面既有大图,也可能有文本操作符;一个包含很多统计图的原生 PDF 也有图片。项目因此组合多个门限:
text
文本操作符足够
AND 唯一字符数 >= 5
AND 不是图像数量远高于文本
AND 不是大量路径组成的轮廓字
AND 不是只有不可解码 Type3 字体
↓
可信文本页
对含图页面,最小文本操作符阈值会从 3 提高到至少 10。若图像数量超过 10 且大于文本操作符的 3 倍,页面被视为图像主导。轮廓字要求路径操作数至少 1,000、远高于文本操作数且唯一字母数字很少。这些都是启发式,不是数学证明,所以输出置信度和原因码比单一标签更重要。
3. 表格检测为什么需要"假设竞争"
页面中的矩形可能是:
- 真正的单元格边框;
- 柱状图条形;
- 页面背景或裁剪路径;
- 表单输入框;
- 每行后面的装饰底纹;
- 日历或信息卡片布局。
因此源码会先做几何聚类,再让"表格假设"和"图表假设"竞争。如果某个矩形簇看起来像柱状图,代码还会移除重复页面填充后重新检测;只有能形成至少 8 行的稳定表格结构,才允许表格假设翻盘。这个设计比先验地把所有矩形都当表格更保守。
4. 标题与段落来自文档内统计
标题层级不是固定写死"18pt 就是 H1"。src/markdown/analysis.rs 先统计正文常见字号,再对大于正文 1.2 倍的字号分层;0.5pt 内聚为一档,最多四档。对于正文 10pt、标题仅 11pt 的书籍,会退回"粗体且略大于正文"的规则。
段落间距同样使用文档内中位数:采集同页相邻行的 Y 差,取中位数并乘 1.3,同时至少为正文字号的 1.5 倍。这能适配双倍行距文档,避免固定阈值把每行都切成新段落。
5. 源码规模与测试证据
在历史提交 f731e119 上用 git ls-files -z 直接统计,避免终端输出截断:
| 指标 | 数值 | 口径 |
|---|---|---|
| Git 跟踪文件 | 288 | 全仓库,不含 .git |
| Rust 文件 | 43 | 含核心、绑定和测试 |
| Rust 物理行 | 85,948 | 含生成映射表 |
核心 src/ Rust |
39 文件 / 80,757 行 | 含 Adobe/CID 映射 |
| 扣除两个生成映射文件后的核心源码 | 37 文件 / 59,094 行 | 更接近手写实现规模 |
| Node + WASM 绑定 | 2 文件 / 1,134 行 | 两个 Rust 入口 |
| 集成测试文件 | 1 文件 / 4,054 行 | 不含散落在模块内的单元测试 |
| Rust 测试标注 | 981 | #[test]、WASM 测试等静态计数 |
| PDF fixture | 27 | 覆盖加密、乱码、表格、扫描/文本混合等情况 |
实际执行 cargo test --all-targets 得到:
| 测试组 | 通过 | 失败 |
|---|---|---|
| 主库单元测试 | 822 | 0 |
pdf2md 二进制测试 |
2 | 0 |
| 集成测试 | 152 | 0 |
| 合计 | 976 | 0 |
静态标注数大于本次实际执行数,是因为其中还包含 WASM 专用测试等未纳入本机原生 --all-targets 的条件编译测试。CI 另外声明了 cargo fmt、cargo clippy -D warnings、Linux/macOS Release Build、WASM Check/Clippy 和 wasm-pack test --node;本文没有把"CI 声明了"写成"本机全部复跑了"。
6. 基准数据如何正确解读
README 在历史提交中公布了 200 份 opendataloader-bench PDF 的结果。条件是:Apple M4 Pro、OCR 关闭、每个解析器单进程顺序处理、预热轮次排除、5 次完整轮转取速度中位数。
| Engine | Overall | Reading Order NID | Tables TEDS | Headings MHS | 200 份总耗时 |
|---|---|---|---|---|---|
| pdf-inspector 0.2.6 | 0.875 | 0.915 | 0.814 | 0.788 | 0.470s |
| LiteParse 2.10.1 | 0.873 | 0.913 | 0.693 | 0.811 | 0.750s |
| OpenDataLoader 2.2.1 | 0.831 | 0.902 | 0.489 | 0.739 | 2.569s |
| PyMuPDF4LLM 0.2.0 | 0.735 | 0.886 | 0.401 | 0.424 | 17.117s |
| MarkItDown 0.1.5 | 0.589 | 0.844 | 0.273 | 0.000 | 16.165s |
按表中总耗时换算,0.470 秒对应约 2.35ms/份;相对 LiteParse、OpenDataLoader、PyMuPDF4LLM、MarkItDown 的总耗时比分别约为 1.60×、5.47×、36.42×、34.39×。但要注意四个边界:
- 这是维护者提供的固定语料结果,不是本文独立复跑的完整 benchmark;
- 只比较本地、非模型 PDF 解析器,且关闭 OCR;
- 200 份文档的总耗时不能外推到任意文件大小、并发和硬件;
- pdf-inspector 对 LiteParse 的 Overall 只高 0.002,真正显著的差异主要在表格 TEDS 高 0.121,而 LiteParse 的标题分数更高。
项目提供 paired harness,可以让 baseline 与 candidate 在同一语料和 evaluator 修订上运行,并对单文档回退设置阈值。这比只维护一张静态宣传表更可靠。
📖 README 核心内容摘要
README 把产品边界说得比较清楚:它是无 OCR 的本地 PDF 解析器,适合原生文本 PDF;扫描件仍需外部 OCR。核心输出包括四类 PDF、置信度、页级 OCR 路由、带坐标的文本项、表格/多栏页面列表和 Markdown。
推荐工作流不是"用它替代所有 PDF 工具",而是:
text
PDF 到达
→ 先用 pdf-inspector 分类
→ 原生文本页:本地抽取与 Markdown 转换
→ 疑难页:按 pages_needing_ocr 交给 OCR
→ 合并两条路径的结果
README 还强调跨平台接入:Rust、Python、Node.js 和浏览器 WASM 共用同一核心。WASM 在浏览器本地解析,PDF 字节无需上传;但同步执行可能阻塞 UI,建议放进 Web Worker。对加密 PDF,CLI 和 API 支持密码;对无法恢复的字体编码,调用方应尊重 has_encoding_issues 与页级原因,而不是继续消费乱码。
版本方面,历史提交同时存在 Rust 0.1.7、PyPI 0.2.6、Node 1.12.0、WASM 0.1.3。仓库有 Git tags,但 GitHub API 未返回 Releases。使用者应按具体语言包读取版本,不要仅凭仓库 tag 推断所有生态包已经同步。
🚀 快速上手
方式一:CLI 先判断,再转换
以下命令已对历史提交的真实参数解析器核验;项目没有传统 clap 风格的独立 --help 入口,直接执行会打印 Usage,因此不要凭习惯发布不存在的 pdf2md --help 工作流。
bash
# 从 crates.io 安装两个 CLI
cargo install pdf-inspector
# 只做类型判断,输出机器可读 JSON
detect-pdf document.pdf --json
# 同时分析表格和多栏布局
detect-pdf document.pdf --analyze --json
# 转成 Markdown
pdf2md document.pdf --raw
# 只处理指定页,并保留页标记
pdf2md document.pdf --raw --pages --select-pages 1,3,5-10
本文在历史提交上对 tests/fixtures/thermo-freon12.pdf 做了真实冒烟:
json
{
"pdf_type": "text_based",
"page_count": 3,
"pages_sampled": 3,
"pages_with_text": 3,
"confidence": 1.0,
"ocr_recommended": false,
"pages_needing_ocr": [],
"detection_time_ms": 8
}
随后执行第 1 页 pdf2md --raw --select-pages 1,退出码为 0,并输出 176 字符 Markdown。这个结果证明本机历史源码的构建、分类和抽取链路能运行;它不是对 200 份 benchmark 的复测。
方式二:Python 文档流水线
bash
pip install pdf-inspector
python
from pathlib import Path
import pdf_inspector
pdf = Path("document.pdf")
result = pdf_inspector.process_pdf(str(pdf))
if result.pdf_type == "text_based" and not result.has_encoding_issues:
Path("document.md").write_text(result.markdown or "", encoding="utf-8")
else:
print("需要 OCR 的页:", result.pages_needing_ocr)
for reason in result.ocr_reasons_by_page:
print(reason.page, reason.reasons)
如果只想做低成本路由,不需要生成 Markdown:
python
result = pdf_inspector.detect_pdf("document.pdf")
if result.pages_needing_ocr:
route_pages_to_ocr(result.pages_needing_ocr)
else:
route_to_local_extraction()
方式三:Rust 内存 API
rust
use pdf_inspector::{process_pdf_mem, PdfType};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let bytes = std::fs::read("document.pdf")?;
let result = process_pdf_mem(&bytes)?;
match result.pdf_type {
PdfType::TextBased if result.pages_needing_ocr.is_empty() => {
println!("{}", result.markdown.unwrap_or_default());
}
_ => {
eprintln!("OCR pages: {:?}", result.pages_needing_ocr);
}
}
Ok(())
}
方式四:浏览器 WASM
bash
npm install @firecrawl/pdf-inspector-wasm
javascript
import init, { processPdf } from "@firecrawl/pdf-inspector-wasm";
await init();
const response = await fetch("/annual-report.pdf");
const bytes = new Uint8Array(await response.arrayBuffer());
const result = processPdf(bytes, {
pages: [1, 3, 5],
profile: "compact",
includePageMarkers: true,
});
console.log(result.pdfType, result.pagesNeedingOcr, result.markdown);
大文件请在 Web Worker 中执行。WASM 版是单线程,不能把原生 rayon 性能直接套用到浏览器环境。
📊 增长速度与社区热度
1. 历史快照与后补数据边界
8 月 7 日 cron 产物完整保留了 13 个仓库的顺序,但只为脚本预选的 addyosmani/agent-skills 保存 Stars、Forks、语言和 README。pdf-inspector 当日只有第 13 名这一历史事实,没有保留当日 Stars、Forks 或"今日新增 Stars",本文不做反推或伪造。
2026-08-10 GitHub API 补充观察值为:
| 指标 | 补充观察值 |
|---|---|
| Stars | 14,102 |
| Forks | 960 |
| Open Issues + PRs | 125 |
| Watchers/Subscribers | 37 |
| 仓库创建时间 | 2026-02-06 |
| 历史提交前 30 天提交数 | 83 |
| 历史提交前 7 天提交数 | 23 |
| 截至历史提交的 Git commit 数 | 428 |
从创建到 8 月 10 日约 184.53 天,14,102 Stars 对应约 76.4 Stars/天;这只是生命周期平均基线,不能称为 8 月 7 日单日增长。Git 历史显示提交高度集中:主要作者的两个邮箱身份合计 415 次提交,其余贡献者多数为 1~2 次。这说明迭代速度高,但 bus factor 和维护集中度也需要关注。
仓库没有 GitHub Releases,主要通过 Rust/PyPI/npm/WASM 各自的包发布流程交付。多版本面让更新快,但也增加了文档、tag 和包版本错位的可能。
2. 2026-08-07 完整 Trending 榜单
| Rank | Repository |
|---|---|
| 1 | TencentCloud/TencentDB-Agent-Memory |
| 2 | addyosmani/agent-skills |
| 3 | cloudflare/computer |
| 4 | mattpocock/skills |
| 5 | goauthentik/authentik |
| 6 | huangruiteng/loopx |
| 7 | google/guava |
| 8 | TapXWorld/ChinaTextbook |
| 9 | Significant-Gravitas/AutoGPT |
| 10 | tirth8205/code-review-graph |
| 11 | esengine/DeepSeek-Reasonix |
| 12 | obra/superpowers |
| 13 | firecrawl/pdf-inspector |
榜单顺序直接来自 8 月 7 日 16:00 cron JSON,排名连续且 13 个仓库各出现一次。选择第 13 名不是因为它热度最高,而是因为榜单前列多个项目已经写过,且 pdf-inspector 在文档解析、OCR 路由与 Rust 工程上提供了更强的新主题和源码可验证性。
🎯 适用场景
| 场景 | 为什么适合 | 注意事项 |
|---|---|---|
| RAG / 知识库导入 | 先产出结构化 Markdown,再送分块与索引 | 表格和标题仍需按业务语料抽检 |
| OCR 成本路由 | 只把 pages_needing_ocr 送入 OCR |
分类是启发式,关键文档需保留回退策略 |
| 发票、报告、论文批处理 | 支持原生文本、表格、多栏和页级结果 | 对复杂公式、手写内容和图表语义能力有限 |
| 浏览器本地处理 | WASM 不上传 PDF,适合隐私敏感预处理 | 单线程同步执行,大文件放 Web Worker |
| Python 数据工程 | PyO3 暴露路径和 bytes API,接入成本低 | 非预构建平台需要 Rust toolchain |
| Node 服务 | napi-rs 提供原生性能和多平台二进制 | Node 包版本独立,不要与 Rust tag 混用 |
| PDF 质量检测 | 原因码能发现乱码字体、轮廓字、扫描页 | 它不是完整 PDF 合规/安全扫描器 |
不适合直接承诺的场景:
- 纯扫描件 OCR:项目本身不识别图像文字,只负责发现并路由;
- 复杂图表语义理解:能避免把图表误判成表格,但不会解释图表含义;
- 视觉版式复刻:输出目标是结构化 Markdown,不是像素级还原;
- 所有 PDF 的固定 200ms SLA:README 延迟来自特定条件,文件大小、加密、字体、表格和平台都会影响性能;
- 零误判分类:阈值来自经验与 fixture,生产系统必须保留监控、抽检和 OCR 回退。
💡 总结
pdf-inspector 最值得借鉴的,不是"Rust 比 Python 快"这个表层结论,而是它把 PDF 处理拆成了三个可治理阶段:先判断文本是否可信,再按布局恢复结构,最后只把必要页面交给 OCR。这种分层让成本、延迟和质量都有明确观测点。
源码显示它对最难的边界投入很多:字体资源作用域、ToUnicode/CID 回退、轮廓字、扫描件隐藏文本层、图表与表格竞争、RTL 多栏顺序、加密文件、损坏 xref 和跨语言绑定。976 个本机通过测试和 27 份 PDF fixture 为这些能力提供了比 README 更强的证据。
同时也要保持克制:0.470 秒和 0.875 Overall 是维护者在固定 benchmark 上的结果,不是通用 SLA;版本面分裂、贡献高度集中、启发式阈值和浏览器单线程都是上线前要评估的边界。最合理的定位不是"万能 PDF 解析器",而是文档流水线中一个速度快、可解释、能把 OCR 用在刀刃上的本地前置层。