PDF OCR — Python / Node.js / Go / C# / Rust(Tesseract 不要)
内蔵 OCR でスキャン PDF からテキストを抽出します。v0.3.27 から、Python、Node.js、Go、C#、Rust のすべてのバインディングに対して、統一された FFI レイヤー(pdf_ocr_engine_create、pdf_ocr_page_needs_ocr、pdf_ocr_extract_text)で OCR を公開しています。
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 は PaddleOCR を ONNX Runtime 経由で同梱しています。Tesseract のインストールもシステム依存もサブプロセス呼び出しも不要で、OCR エンジンはプロセス内で直接動作します。PP-OCRv3、PP-OCRv4、PP-OCRv5 のモデルファミリーに対応しています。
注意: WebAssembly では OCR は利用できません(ネイティブの ONNX Runtime が必要なため)。Go/Node.js/C#/Rust では
ocrフィーチャを有効にしてビルドしてください。Python の wheel は既定で OCR が有効な状態で配布されます。
Tesseract なしの Python PDF OCR
Python の PDF OCR ソリューションの多くは、Tesseract をシステム依存としてインストールする必要があり、OS や CI 環境ごとに手順が異なって設定が複雑になりがちです。PDF Oxide は PaddleOCR モデルを Python wheel に直接同梱しています:
- システム依存なし —
pip install pdf_oxideだけで完結します - サブプロセス呼び出しなし — OCR は ONNX Runtime 上でネイティブに動作します
- 3 つのモデルファミリー — 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(サブプロセス) |
| セットアップの手間 | 1 行 | OS ごとの 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 の 3 世代のモデルに対応しています。既定の設定は 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();
// カスタムビルダー
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
OCR 用に PDF ページを画像化するときのレンダリング解像度を制御します:
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() メソッドは、スパンごとの信頼度スコア付きの詳細な結果を返します:
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());
// スパンごとの詳細
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 |
全スパンの平均信頼度 |
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 はテキスト抽出に比べてかなり遅い処理です:
| 処理 | 典型的な速度 |
|---|---|
| テキスト抽出 | 1 ページあたり 0.8ms |
| OCR(v3/v4) | 1 ページあたり 200–1,000ms |
| OCR(v5 サーバー) | 1 ページあたり 500–2,000ms |
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 からテキストを抽出 — テキスト抽出ガイド