PDF OCR — Python / Node.js / Go / C# / Rust 无需 Tesseract
使用内置 OCR 从扫描 PDF 中提取文本。自 v0.3.27 起,OCR 已在所有语言绑定(Python、Node.js、Go、C#、Rust)中通过统一的 FFI 层(pdf_ocr_engine_create、pdf_ocr_page_needs_ocr、pdf_ocr_extract_text)对外开放。
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("scanned.pdf")
text = doc.extract_text_ocr(0)
print(text)
Node.js
const { PdfDocument, OcrEngine } = require("pdf-oxide");
const doc = new PdfDocument("scanned.pdf");
const ocr = new OcrEngine();
if (ocr.pageNeedsOcr(doc, 0)) {
console.log(ocr.extractText(doc, 0));
}
ocr.close();
doc.close();
Go
import pdfoxide "github.com/yfedoseev/pdf_oxide/go"
doc, _ := pdfoxide.Open("scanned.pdf")
defer doc.Close()
ocr, _ := pdfoxide.NewOcrEngine()
defer ocr.Close()
if ocr.NeedsOcr(doc, 0) {
text, _ := ocr.ExtractTextWithOcr(doc, 0)
fmt.Println(text)
}
C#
using PdfOxide.Core;
using PdfOxide.Ocr;
using var doc = PdfDocument.Open("scanned.pdf");
using var ocr = new OcrEngine();
if (ocr.PageNeedsOcr(doc, 0))
{
Console.WriteLine(ocr.ExtractText(doc, 0));
}
Rust
use pdf_oxide::PdfDocument;
use pdf_oxide::ocr::{OcrEngine, OcrConfig, OcrExtractOptions, extract_text_with_ocr};
let mut doc = PdfDocument::open("scanned.pdf")?;
let config = OcrConfig::default();
let engine = OcrEngine::new("models/det.onnx", "models/rec.onnx", "models/dict.txt", config)?;
let options = OcrExtractOptions::default();
let text = extract_text_with_ocr(&mut doc, 0, Some(&engine), options)?;
println!("{text}");
PDF Oxide 通过 ONNX Runtime 自带 PaddleOCR —— 不用装 Tesseract,没有系统依赖,也不调用子进程。OCR 引擎直接在进程内运行。支持 PP-OCRv3、PP-OCRv4 和 PP-OCRv5 三代模型家族。
说明: WebAssembly 环境下不提供 OCR(它需要本地的 ONNX Runtime)。在 Go、Node.js、C# 和 Rust 中请打开
ocr特性来编译。Python wheel 默认就带上了 OCR。
Python PDF OCR 无需 Tesseract
大多数 Python PDF OCR 方案都要求把 Tesseract 作为系统依赖安装——不同操作系统、不同 CI 环境下配置方式都不一样,非常繁琐。PDF Oxide 把 PaddleOCR 模型直接打包进了 Python wheel:
- 无系统依赖 —
pip install pdf_oxide就够了 - 不调用子进程 — OCR 通过 ONNX Runtime 原生运行
- 三个模型家族 — PP-OCRv3、PP-OCRv4、PP-OCRv5
- 自动识别页面 — 自动判断页面是扫描页还是文本页
对比:PDF Oxide OCR vs PyMuPDF + Tesseract
| PDF Oxide | PyMuPDF + Tesseract | |
|---|---|---|
| 安装 | pip install pdf_oxide |
pip install pymupdf + 系统 Tesseract |
| OCR 引擎 | PaddleOCR (ONNX) | Tesseract(子进程) |
| 配置复杂度 | 一行命令 | 各操作系统分别安装 Tesseract |
| CI/Docker | 无需额外配置 | 需要 apt-get install tesseract-ocr |
| 模型是否自带 | 是(随 wheel 一起) | 否(需另行下载) |
安装
Python
pip install pdf_oxide
OCR 模型已经在 wheel 里,不需要再下载任何东西。
Rust
[dependencies]
pdf_oxide = { version = "0.3", features = ["ocr"] }
Go
go build -tags ocr ./...
Node.js
npm install pdf-oxide --build-from-source -- --features ocr
C#
NuGet 包在 Linux / macOS / Windows 的默认二进制里就启用了 OCR,不需要额外配置。
什么时候需要 OCR
多数 PDF 都带有嵌入文本,extract_text() 每页 0.8ms 就能完成。只有以下几种场景才用得上 OCR:
- 扫描文档 — 从纸质文档扫描成的 PDF
- 纯图像 PDF — 由照片或截图生成的 PDF
- 文字被当作图片的 PDF — 一些生成器会直接把文字光栅化
- 混合页 — 同一页里既有原生文本也有扫描图像区域
PP-OCR 模型版本
PDF Oxide 支持三代 PaddleOCR 模型。默认配置对应 PP-OCRv3 和 PP-OCRv4;PP-OCRv5 服务器模型需要换用另一种缩放策略。
PP-OCRv3 / PP-OCRv4(默认)
移动端优化的模型,会把图像缩小到某个最大边长以内。大多数文档都适用。
- 检测模型:DBNet++(轻量)
- 识别模型:SVTR
- 缩放策略:
MaxSide— 把长边压到 960px - 适合场景:普通文档、移动端和边缘部署
Python
from pdf_oxide import OcrConfig, OcrEngine
# 默认配置适配 v3/v4 模型
config = OcrConfig()
engine = OcrEngine("det_v4.onnx", "rec_v4.onnx", "dict.txt", config)
Rust
use pdf_oxide::ocr::{OcrConfig, OcrEngine};
// 默认配置:MaxSide { max_side: 960 }
let config = OcrConfig::default();
let engine = OcrEngine::new("det_v4.onnx", "rec_v4.onnx", "dict.txt", config)?;
PP-OCRv5(服务器)
服务器级模型,必要时会把图像放大以保留高分辨率,在密集或小字文档上精度显著更高。
- 检测模型:DBNet++(服务器版,更大)
- 识别模型:SVTR-v5
- 缩放策略:
MinSide— 保证短边至少 64px,上限 4000px - 适合场景:高精度提取、服务器环境、密集文本
Python
from pdf_oxide import OcrConfig, OcrEngine
# v5 配置:用于服务器模型的高分辨率输入
config = OcrConfig(use_v5=True)
engine = OcrEngine("det_v5.onnx", "rec_v5.onnx", "dict_v5.txt", config)
Rust
use pdf_oxide::ocr::{OcrConfig, OcrEngine};
// v5 配置:MinSide { min_side: 64, max_side_limit: 4000 }
let config = OcrConfig::v5();
let engine = OcrEngine::new("det_v5.onnx", "rec_v5.onnx", "dict_v5.txt", config)?;
模型对比
| 特性 | PP-OCRv3/v4 | PP-OCRv5 |
|---|---|---|
| 缩放策略 | MaxSide(缩到 960px) |
MinSide(放大,上限 4000px) |
| 输入分辨率 | 较低(更快) | 较高(更准) |
| 检测模型大小 | 约 3 MB | 约 12 MB |
| 识别模型大小 | 约 12 MB | 约 25 MB |
| 适合场景 | 移动端、边缘、普通文档 | 服务器、密集文本、小字 |
OcrConfig |
OcrConfig() / OcrConfig::default() |
OcrConfig(use_v5=True) / OcrConfig::v5() |
页面类型识别
PDF Oxide 会自动对页面分类,判断是否需要 OCR。extract_text_ocr() 在内部处理这一步,你也可以手动检测页面类型。
自动识别扫描页
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("mixed.pdf")
for i in range(doc.page_count()):
text = doc.extract_text(i)
if len(text.strip()) < 50:
# 很可能是扫描页 —— 走 OCR
text = doc.extract_text_ocr(i)
print(f"Page {i + 1} (OCR): {text[:100]}...")
else:
print(f"Page {i + 1} (text): {text[:100]}...")
Rust
use pdf_oxide::PdfDocument;
use pdf_oxide::ocr::{detect_page_type, PageType, OcrEngine, OcrConfig, OcrExtractOptions, extract_text_with_ocr};
let mut doc = PdfDocument::open("mixed.pdf")?;
let engine = OcrEngine::new("det.onnx", "rec.onnx", "dict.txt", OcrConfig::default())?;
for i in 0..doc.page_count() {
let page_type = detect_page_type(&mut doc, i)?;
match page_type {
PageType::NativeText => {
let text = doc.extract_text(i)?;
println!("Page {} (native): {}...", i + 1, &text[..100.min(text.len())]);
}
PageType::ScannedPage => {
let text = extract_text_with_ocr(&mut doc, i, Some(&engine), OcrExtractOptions::default())?;
println!("Page {} (OCR): {}...", i + 1, &text[..100.min(text.len())]);
}
PageType::HybridPage => {
// 同时有原生文本和扫描图像 —— 两个来源会合并
let text = extract_text_with_ocr(&mut doc, i, Some(&engine), OcrExtractOptions::default())?;
println!("Page {} (hybrid): {}...", i + 1, &text[..100.min(text.len())]);
}
}
}
PageType 变体(Rust)
| 变体 | 说明 |
|---|---|
NativeText |
页面带嵌入文本 —— 无需 OCR |
ScannedPage |
整页扫描(大图,几乎没有文本)—— 走完整 OCR |
HybridPage |
页面里同时有原生文本和大片扫描图像 —— 会把原生文本与 OCR 结果合并 |
辅助函数 needs_ocr() 对 ScannedPage 和 HybridPage 都返回 true:
use pdf_oxide::ocr::needs_ocr;
if needs_ocr(&mut doc, 0)? {
let text = extract_text_with_ocr(&mut doc, 0, Some(&engine), OcrExtractOptions::default())?;
}
工作原理
- PDF Oxide 在内部把页面渲染为图像(300 DPI)
- 按检测策略缩放图像(v3/v4 用
MaxSide,v5 用MinSide) - DBNet++ 文本检测器把文字区域定位为四边形边界框
- SVTR 文本识别器在每个检测到的区域读取字符
- 结果按阅读顺序排序,拼装成文本
- 对于混合页,OCR 文本会与原生文本合并
整条流水线都在 ONNX Runtime 上进程内执行。没有外部可执行文件,没有子进程调用,也没有临时文件。
OCR 配置
Python
from pdf_oxide import OcrConfig, OcrEngine
# 默认(v3/v4)
config = OcrConfig()
# PP-OCRv5 服务器模型
config = OcrConfig(use_v5=True)
# 自定义阈值
config = OcrConfig(
det_threshold=0.5, # 检测置信度 (0.0-1.0)
box_threshold=0.7, # 边框置信度 (0.0-1.0)
rec_threshold=0.6, # 识别置信度 (0.0-1.0)
num_threads=8, # ONNX Runtime 线程数
max_candidates=500, # 最大文本区域数
)
# 自定义阈值搭配 v5
config = OcrConfig(use_v5=True, det_threshold=0.4, num_threads=8)
engine = OcrEngine("det.onnx", "rec.onnx", "dict.txt", config)
Rust
use pdf_oxide::ocr::{OcrConfig, OcrConfigBuilder, DetResizeStrategy};
// 默认(v3/v4):MaxSide { max_side: 960 }
let config = OcrConfig::default();
// PP-OCRv5:MinSide { min_side: 64, max_side_limit: 4000 }
let config = OcrConfig::v5();
// 自定义 builder
let config = OcrConfig::builder()
.det_threshold(0.5)
.box_threshold(0.7)
.rec_threshold(0.6)
.num_threads(8)
.max_candidates(500)
.detect_styles(true) // 启用基于 OCR 几何的样式识别
.build();
// 自定义缩放策略
let config = OcrConfig::builder()
.det_resize_strategy(DetResizeStrategy::MinSide {
min_side: 128,
max_side_limit: 6000,
})
.build();
DetResizeStrategy(Rust)
控制在运行检测模型之前如何对输入图像进行缩放。
| 变体 | 字段 | 说明 |
|---|---|---|
MaxSide |
max_side: u32(默认 960) |
向下缩放,使长边不超过 max_side。PP-OCRv3/v4 的默认值。 |
MinSide |
min_side: u32(默认 64)、max_side_limit: u32(默认 4000) |
向上放大,使短边至少为 min_side,同时以 max_side_limit 封顶。PP-OCRv5 的默认值。 |
OcrConfig 字段
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
det_threshold |
f32 |
0.3 |
检测概率阈值 |
box_threshold |
f32 |
0.6 |
边框置信度阈值 |
rec_threshold |
f32 |
0.5 |
识别置信度阈值 |
det_max_side |
u32 |
960 |
图像最大边长(兼容 v3/v4) |
det_resize_strategy |
DetResizeStrategy |
MaxSide { 960 } |
图像缩放策略 |
rec_target_height |
u32 |
48 |
识别裁剪的目标高度 |
num_threads |
usize |
4 |
ONNX Runtime 推理线程数 |
unclip_ratio |
f32 |
1.5 |
边框扩张比例 |
max_candidates |
usize |
1000 |
允许检测的最大文本区域数 |
detect_styles |
bool |
true |
从 OCR 几何中识别字体样式 |
det_model_path |
Option<PathBuf> |
None |
自定义检测模型路径 |
rec_model_path |
Option<PathBuf> |
None |
自定义识别模型路径 |
dict_path |
Option<PathBuf> |
None |
自定义字符字典路径 |
自定义模型
使用自己的 ONNX 模型替代自带的模型:
Rust
use pdf_oxide::ocr::OcrConfig;
let config = OcrConfig::builder()
.det_model_path("models/custom_det.onnx")
.rec_model_path("models/custom_rec.onnx")
.dict_path("models/custom_dict.txt")
.build();
样式识别
开启 detect_styles(默认开启)后,PDF Oxide 会根据 OCR 几何信息(字号、字距、位置)推断字体样式(粗体、标题层级)。这能提升从扫描页生成 Markdown 的质量。
let config = OcrConfig::builder()
.detect_styles(true) // 从文本几何推断样式
.build();
OCR vs Tesseract
| 特性 | PDF Oxide OCR | Tesseract(经 PyMuPDF) |
|---|---|---|
| 安装 | pip install pdf_oxide |
系统包 + pytesseract |
| 系统依赖 | 无 | 需要 Tesseract 二进制 |
| 运行环境 | ONNX(进程内) | 调用子进程 |
| 模型版本 | PP-OCRv3、v4、v5 | Tesseract LSTM |
| 多语种 | 原生多语种 | 需要语言包 |
| 配置复杂度 | 为零 | 中等 |
| 检测模型 | DBNet++ | Tesseract 内置 |
| 识别模型 | SVTR / SVTR-v5 | Tesseract LSTM |
| 高分辨率支持 | MinSide 策略(v5) |
DPI 设置 |
| 页面类型识别 | 自动(原生/扫描/混合) | 手动 |
自定义 DPI
在把 PDF 页面转成图像供 OCR 使用时,可以控制渲染分辨率:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("scanned.pdf")
# 默认 300 DPI —— 在精度和速度之间比较平衡
text = doc.extract_text_ocr(0)
# 小字场景可以调高 DPI 以提升精度
text = doc.extract_text_ocr(0) # Rust 中通过 OcrExtractOptions 设置 DPI
Rust
use pdf_oxide::ocr::OcrExtractOptions;
// DPI 越高越准,但更慢
let options = OcrExtractOptions::default().with_dpi(300.0);
// DPI 越低越快,但精度下降
let options = OcrExtractOptions::default().with_dpi(150.0);
OCR 输出结构(Rust)
OcrEngine::ocr_image() 方法返回按 span 带置信度的详细结果:
use pdf_oxide::ocr::OcrEngine;
let engine = OcrEngine::new("det.onnx", "rec.onnx", "dict.txt", Default::default())?;
let output = engine.ocr_image(&image)?;
// 按阅读顺序输出的全文
println!("{}", output.text_in_reading_order());
// 按 span 输出详情
for span in &output.spans {
println!("Text: '{}' (confidence: {:.2})", span.text, span.confidence);
println!(" 边界框: {:?}", span.bounding_rect());
println!(" 单字符置信度: {:?}", span.char_confidences);
}
// 总体置信度
println!("Total confidence: {:.2}", output.total_confidence);
OcrOutput 字段
| 字段 / 方法 | 类型 | 说明 |
|---|---|---|
spans |
Vec<OcrSpan> |
所有识别出的文本区域 |
total_confidence |
f32 |
所有 span 的平均置信度 |
text() |
String |
以空格拼接的全部文本 |
text_in_reading_order() |
String |
按位置排序的文本(从上到下、从左到右) |
OcrSpan 字段
| 字段 | 类型 | 说明 |
|---|---|---|
text |
String |
识别出的文本 |
polygon |
[[f32; 2]; 4] |
四边形边界框(4 个顶点) |
confidence |
f32 |
总体置信度(0.0–1.0) |
char_confidences |
Vec<f32> |
每个字符的置信度 |
批量 OCR
处理一个装满扫描 PDF 的目录:
Python
from pdf_oxide import PdfDocument, PdfError
from pathlib import Path
pdf_dir = Path("scans/")
output_dir = Path("text-output/")
output_dir.mkdir(exist_ok=True)
for pdf_path in pdf_dir.glob("*.pdf"):
try:
doc = PdfDocument(str(pdf_path))
pages = []
for i in range(doc.page_count()):
text = doc.extract_text(i)
if len(text.strip()) < 50:
text = doc.extract_text_ocr(i)
pages.append(text)
out_path = output_dir / pdf_path.with_suffix(".txt").name
out_path.write_text("\n\n".join(pages), encoding="utf-8")
except PdfError as e:
print(f"Error: {pdf_path.name}: {e}")
Rust
use pdf_oxide::PdfDocument;
use pdf_oxide::ocr::{OcrEngine, OcrConfig, OcrExtractOptions, extract_text_with_ocr, needs_ocr};
use std::fs;
use std::path::Path;
let engine = OcrEngine::new("det.onnx", "rec.onnx", "dict.txt", OcrConfig::default())?;
let options = OcrExtractOptions::default();
for entry in fs::read_dir("scans/")? {
let path = entry?.path();
if path.extension().map_or(false, |e| e == "pdf") {
let mut doc = PdfDocument::open(path.to_str().unwrap())?;
let mut all_text = String::new();
for i in 0..doc.page_count() {
let text = if needs_ocr(&mut doc, i)? {
extract_text_with_ocr(&mut doc, i, Some(&engine), options.clone())?
} else {
doc.extract_text(i)?
};
all_text.push_str(&text);
all_text.push_str("\n\n");
}
let out_path = Path::new("text-output/")
.join(path.file_stem().unwrap())
.with_extension("txt");
fs::write(out_path, &all_text)?;
}
}
并行 OCR(Python)
from pdf_oxide import PdfDocument
from multiprocessing import Pool
from pathlib import Path
def ocr_pdf(pdf_path: str) -> dict:
doc = PdfDocument(pdf_path)
text = ""
for i in range(doc.page_count()):
text += doc.extract_text_ocr(i) + "\n"
return {"file": pdf_path, "text": text}
pdf_files = [str(p) for p in Path("scans/").glob("*.pdf")]
with Pool(4) as pool:
results = pool.map(ocr_pdf, pdf_files)
OCR 转 Markdown
把扫描页转换成 Markdown:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("scanned-report.pdf")
for i in range(doc.page_count()):
md = doc.to_markdown(i, detect_headings=True)
if len(md.strip()) < 50:
# 扫描页 —— 先 OCR 再格式化
text = doc.extract_text_ocr(i)
md = text # OCR 输出是纯文本
print(f"--- Page {i + 1} ---")
print(md)
Rust
use pdf_oxide::PdfDocument;
use pdf_oxide::ocr::{OcrEngine, OcrConfig, OcrExtractOptions, needs_ocr, extract_text_with_ocr};
let mut doc = PdfDocument::open("scanned-report.pdf")?;
let engine = OcrEngine::new("det.onnx", "rec.onnx", "dict.txt", OcrConfig::default())?;
for i in 0..doc.page_count() {
let text = if needs_ocr(&mut doc, i)? {
extract_text_with_ocr(&mut doc, i, Some(&engine), OcrExtractOptions::default())?
} else {
doc.to_markdown(i, &Default::default())?
};
println!("--- Page {} ---\n{}", i + 1, text);
}
性能注意事项
OCR 比文本提取慢得多:
| 操作 | 典型速度 |
|---|---|
| 文本提取 | 每页 0.8ms |
| OCR (v3/v4) | 每页 200–1000ms |
| OCR (v5 服务器) | 每页 500–2000ms |
OCR 速度取决于页面复杂度、图像分辨率、文本密度和模型版本。PP-OCRv5 更慢但更准。面对大批量数据时,可以考虑并行处理(见上面的「批量 OCR」一节)。
从字节加载模型(Rust)
use pdf_oxide::ocr::{OcrEngine, OcrConfig};
let det_bytes = std::fs::read("models/det.onnx")?;
let rec_bytes = std::fs::read("models/rec.onnx")?;
let dict = std::fs::read_to_string("models/dict.txt")?;
let engine = OcrEngine::from_bytes(&det_bytes, &rec_bytes, &dict, OcrConfig::default())?;
相关页面
- 文本提取 — 标准文本提取
- Markdown 转换 — 带标题识别的 Markdown
- 页面渲染 — 把页面渲染成图像(OCR 内部用到)
- 批量处理 — 并行处理模式
- 从 PDF 提取文本 — 文本提取指南