docparser-pdf 0.2.1

docparser PDF 后端:PDFium 常驻 worker 逐文本对象抽取(CJK/CID 可用)+ pdf-extract 纯 Rust 兜底
Documentation
# docparser-pdf

PDF 后端:**PDFium 常驻 worker**(一条线程独占 native 状态,通道收发)逐行抽取
`text + bbox`(text page rect 分段,含 Form XObject 内文本),支持 CJK/CID 字体;
`pdf-extract`(纯 Rust)作为兜底后端。

产出结构:页(`width`/`height`/`rotation`)→ 行归并(基线对齐)→ 表格检测(≥3 列 + ≥60% 行一致性)
→ 多栏阅读序 → `Paragraph` / `Table` 块 + `Image`(bbox / px / OCR 文本)。

## 能力

| 能力 | 说明 |
| --- | --- |
| 文本抽取 | 逐行取 `text + bbox`(pt、左上原点):`FPDFText_CountRects` + `FPDFText_GetBoundedText`,**含 Form XObject 内文本**(代码清单/图表标注/水印);rect 高 > 25% 页高的跨高条块跳过(否则会吞入叠压正文);CJK/CID 走 PDFium 内建 CMap,不依赖 ToUnicode |
| 版式 | `LayoutMode::{Auto, Rebuild, Raw}`:`Auto`/`Rebuild` 做行归并 + 表格 + 分栏;`Raw` 按内容流顺序 |
| 分栏阅读序 | 先算「相邻行同 y 且 x0 跨半页宽」的交错指纹,<0.25 不改序(内容流本已是栏序,盲改会把好顺序改坏);需重排时走**沟槽法**(窄项 x 占位直方图,≥20pt 零占位带即栏沟,与 Python `_column_order` 同口径),沟槽被跨栏元素填平/切分失败则回退**视觉行行首 x0 聚类**(先按基线聚行、再按 `max(12, 2×字高)` 横向间隙切开;阈值用行首起点自己的跨度,不能用整页内容跨度——表格页会把 126pt 内的起点全链成一栏) |
| 表格 | ≥3 列 + ≥60% 行一致,行按**基线对齐**聚行(与 Python `_group_lines` 同口径:底边差 ≤ 0.3×(行高+字高),用纵向重叠会在密排行/标签基线参差时并错行);另加自洽性判据:bbox 内散落文本多于表内、且 >40 字符长句单元格占比 ≥0.2 → 不认表(避免「正文行 + 图内标签」凑成假表格把正文掰断) |
| 页数上限 | `PdfOptions::max_pages`,超出时 `meta.extra.pdf.truncated = true` |
| 元数据 | `pdf_version` / `title` / `author` / `producer` / `creator`,页级 `complexity`(chars / garbled_ratio / needs_ocr)、`page_errors`、`layout_modes`,全部在 `meta.extra.pdf`(JSON 字符串) |
| 页眉页脚 | 顶/底 7% 边带 + 数字归一化,多页重复行剔除(`meta.extra.pdf.headers_footers`) |
| OCR | 稀疏/乱码页整页渲染 200dpi → `docparser-ocr`;嵌入图 OCR opt-in(`DOCPARSER_OCR_IMAGES=1`) |
| 兜底 | `pdfium-text-v1`(pdf-extract,纯 Rust,无 dylib;无 bbox → 自动退化为 raw 语义) |

## 安装

```bash
cargo add docparser-pdf   # 或 cargo add docparser-sdk(含 core + office)
```

## 运行时依赖(重要)

PDFium 是**运行时**动态库,不由本 crate 打包。查找顺序:

1. `DOCPARSER_PDFIUM_LIB` 显式指定(推荐,发布环境务必显式设置);
2. `<exe>/../lib/{libpdfium.so|libpdfium.dylib|pdfium.dll}`(自包含发布布局);
3. home 下常见 conda 布局(`~/miniconda3/lib/python3.x/site-packages/{pypdfium2_raw,liteparse}`);
4. 扫描常见 Python site-packages(`python*/Lib/site-packages`、`AppData\Local\Programs\Python\Python3xx`、
   `/usr/lib/python3*` 等)。`HOME` 在 Windows 上常常为空,这里同时看 `USERPROFILE` 与
   `HOMEDRIVE`+`HOMEPATH`;`docparser-cli doctor` 可打印实际绑定到的库路径。

```rust
use docparser_core::{DocumentEngine, Registry};
use docparser_pdf::{LayoutMode, PdfOptions};

let mut reg = Registry::new();
docparser_core::plugins::text::register_defaults(&mut reg);

// 默认选项(LayoutMode::Auto、不限页数)
docparser_pdf::register_defaults(&mut reg);

// 或指定选项:不重排版式 + 只解析前 20 页
let options = PdfOptions::new()
    .with_layout_mode(LayoutMode::Raw)
    .with_max_pages(20);
docparser_pdf::register_with(&mut reg, &options);

let doc = DocumentEngine::new(reg).parse("report.pdf")?;
println!("{} 页 / {} 字符", doc.pages.len(), doc.meta.text_length);
if let Some(pdf) = doc.meta.extra.get("pdf") {
    println!("pdf 元数据: {pdf}"); // pdf_version / complexity / truncated / layout_modes …
}
```

`find_library()` / `find_ocr_binary()` / `find_ocr_models()` 可直接用于自检:

```rust
println!("{:?}", docparser_pdf::find_library());
```

## 示例

```bash
cargo run -p docparser-pdf --example layout_modes -- doc.pdf   # auto vs raw 对照
```

CLI 侧等价开关:`docparser-cli parse doc.pdf --layout-mode raw --max-pages 20`。

## 扫描件与嵌入图 OCR

- 纯文本层可用即不触发 OCR;稀疏/乱码页才路由到 OCR(`BackendPolicy.enable_ocr`)。
- OCR 走外部可执行文件 `docparser-ocr`(`DOCPARSER_OCR_BIN`)+ 模型目录(`DOCPARSER_OCR_MODELS`),
  缺失时降级为纯文本,不报错。
- 嵌入图文字默认关闭,`DOCPARSER_OCR_IMAGES=1` 开启。

## 后端与错误

| 后端 id | 形态 | 说明 |
| --- | --- | --- |
| `pdfium-worker` | bundled | 逐文本对象 + CJK CMap |
| `pdfium-text-v1` | bundled | `pdf-extract` 兜底 |

`encrypted` / `unsupported` 为终止码,不回退;其余错误按策略链继续尝试。

## 已知差异(相对 Python 参考实现)

| 项 | 现状 |
| --- | --- |
| pypdf 轻量路由(T1/T2/T3/T5 双维度 + 页数交叉点) | 不做:固定走 PDFium(小文件多 ~17ms 启动开销,结论不变) |
| `speed` 极速模式(整页 `get_text_range`) | 不做:`LayoutMode::Raw` 是最近似替代(仍逐对象取文本) |
| 嵌入图 md5 去重键 | 已做:有字节 → md5 十六进制(`image_dedup_key`),无字节 → `"{w}x{h}@{ext}"` 兜底(与 Python 同形);**跨载体的键值不保证相等**——原始流路径一致时才逐字节相同,位图回退两侧编码器不同(Rust `image` vs Pillow),真实 JPX 图实测两侧各得各的 md5 |
| 单页失败 | 渲染失败记入 `meta.extra.pdf.page_errors`,不影响其它页 |
| 逐页文本 `sim` 残余 | 11 件 / 162 页真实语料上 `cov` 全部 1.0(最低 0.9994)、 3 篇整篇逐字一致;`sim` 均值最低 0.9991、单页最低 0.990,只剩"一个 token 落点"级差异(tracemonkey p7 项目符号 `•`/`\x02` 等)——见 `python/bench/REAL_CHECKLIST.md` |

测试注意:仓库不含真实 PDF 语料(`python/bench/corpus_pdf/real/` 与 `corpus_real/` 均已 gitignore,
本地用 `cd python && python bench/fetch_arxiv.py --max 8` 现取),
改动行归并/表格/分栏逻辑后请自备语料复测(`cargo test -p docparser-pdf` 只覆盖纯函数)。

## 许可

MIT OR Apache-2.0