Skip to content

PDF OCR — Python / Node.js / Go / C# / Rust(Tesseract 不要)

内蔵 OCR でスキャン PDF からテキストを抽出します。v0.3.27 から、Python、Node.js、Go、C#、Rust のすべてのバインディングに対して、統一された FFI レイヤー(pdf_ocr_engine_createpdf_ocr_page_needs_ocrpdf_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()ScannedPageHybridPage のどちらに対しても 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())?;
}

仕組み

  1. PDF Oxide が内部でページを画像にレンダリング(300 DPI)
  2. 検出戦略に沿って画像をリサイズ(v3/v4 は MaxSide、v5 は MinSide
  3. DBNet++ テキスト検出器が四辺形バウンディングボックスでテキスト領域を特定
  4. SVTR テキスト認識器が検出された各領域の文字を読み取り
  5. 読み順ソートで結果をテキストに組み立て
  6. ハイブリッドページでは 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())?;

関連ページ