Skip to content

PDF-OCR in Python, Node.js, Go, C# und Rust — ohne Tesseract

Text aus gescannten PDFs mit integriertem OCR extrahieren. Seit v0.3.27 ist OCR für alle Sprachanbindungen verfügbar — Python, Node.js, Go, C# und Rust — über eine einheitliche FFI-Schicht (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 liefert PaddleOCR über ONNX Runtime mit — keine Tesseract-Installation, keine Systemabhängigkeiten, keine Subprozess-Aufrufe. Die OCR-Engine läuft direkt im Prozess. Unterstützt die Modellfamilien PP-OCRv3, PP-OCRv4 und PP-OCRv5.

Hinweis: Für WebAssembly ist OCR nicht verfügbar (benötigt native ONNX Runtime). Für Go, Node.js, C# und Rust mit dem Feature ocr bauen. Python-Wheels werden standardmäßig mit aktiviertem OCR ausgeliefert.

Python-PDF-OCR ohne Tesseract

Die meisten Python-PDF-OCR-Lösungen setzen Tesseract als Systemabhängigkeit voraus — ein komplexes Setup, das sich je nach Betriebssystem und CI-Umgebung unterscheidet. PDF Oxide enthält die PaddleOCR-Modelle direkt im Python-Wheel:

  • Keine Systemabhängigkeitenpip install pdf_oxide genügt
  • Keine Subprozess-Aufrufe — OCR läuft nativ über ONNX Runtime
  • Drei Modellfamilien — PP-OCRv3, PP-OCRv4 und PP-OCRv5
  • Automatische Seitenerkennung — erkennt, welche Seiten gescannt und welche textbasiert sind

Vergleich: PDF Oxide OCR vs PyMuPDF + Tesseract

PDF Oxide PyMuPDF + Tesseract
Installation pip install pdf_oxide pip install pymupdf + System-Tesseract
OCR-Engine PaddleOCR (ONNX) Tesseract (Subprozess)
Setup-Aufwand Eine Zeile Betriebssystemspezifische Tesseract-Installation
CI/Docker Keine Zusatzkonfiguration Benötigt apt-get install tesseract-ocr
Modelle enthalten Ja (im Wheel) Nein (separater Download)

Installation

Python

pip install pdf_oxide

Die OCR-Modelle sind im Wheel enthalten. Es sind keine weiteren Downloads nötig.

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#

Das NuGet-Paket bringt OCR in den Standardbinärdateien für Linux, macOS und Windows bereits mit — eine zusätzliche Konfiguration ist nicht erforderlich.

Wann OCR sinnvoll ist

Die meisten PDFs enthalten eingebetteten Text, den extract_text() mit 0,8 ms pro Seite verarbeitet. OCR wird nur in folgenden Fällen gebraucht:

  • Gescannte Dokumente — Papierdokumente, die als PDF eingescannt wurden
  • Reine Bild-PDFs — aus Fotos oder Screenshots erstellte PDFs
  • PDFs mit Text als Bild — manche Generatoren rastern Text
  • Hybride Seiten — Seiten mit nativem Text und gleichzeitig gescannten Bildbereichen

PP-OCR-Modellversionen

PDF Oxide unterstützt drei Generationen von PaddleOCR-Modellen. Die Standardkonfiguration passt zu PP-OCRv3 und PP-OCRv4. Die PP-OCRv5-Servermodelle benötigen eine andere Resize-Strategie.

PP-OCRv3 / PP-OCRv4 (Standard)

Für mobile Geräte optimierte Modelle, die Bilder herunterskalieren, damit sie in eine maximale Seitenlänge passen. Gut für die meisten Dokumente.

  • Erkennungsmodell: DBNet++ (leichtgewichtig)
  • Textlesemodell: SVTR
  • Resize-Strategie: MaxSide — skaliert die längere Seite auf 960 px herunter
  • Am besten für: Standarddokumente, Mobile- und Edge-Deployments

Python

from pdf_oxide import OcrConfig, OcrEngine

# Standardkonfiguration passt zu v3/v4-Modellen
config = OcrConfig()
engine = OcrEngine("det_v4.onnx", "rec_v4.onnx", "dict.txt", config)

Rust

use pdf_oxide::ocr::{OcrConfig, OcrEngine};

// Standardkonfiguration: MaxSide { max_side: 960 }
let config = OcrConfig::default();
let engine = OcrEngine::new("det_v4.onnx", "rec_v4.onnx", "dict.txt", config)?;

PP-OCRv5 (Server)

Server-Modelle, die eine hohe Auflösung bewahren, indem sie Bilder bei Bedarf hochskalieren. Deutlich präziser bei dichten oder fein gedruckten Dokumenten.

  • Erkennungsmodell: DBNet++ (Server, größer)
  • Textlesemodell: SVTR-v5
  • Resize-Strategie: MinSide — stellt sicher, dass die kürzere Seite mindestens 64 px hat, gedeckelt bei 4000 px
  • Am besten für: hochgenaue Extraktion, Serverumgebungen, dichten Text

Python

from pdf_oxide import OcrConfig, OcrEngine

# v5-Konfiguration: hochaufgelöste Eingabe für Servermodelle
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-Konfiguration: 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)?;

Modellvergleich

Merkmal PP-OCRv3/v4 PP-OCRv5
Resize-Strategie MaxSide (herunter auf 960 px) MinSide (hoch, gedeckelt bei 4000 px)
Eingangsauflösung Niedriger (schneller) Höher (genauer)
Größe Erkennungsmodell ~3 MB ~12 MB
Größe Textlesemodell ~12 MB ~25 MB
Am besten für Mobile, Edge, Standarddokumente Server, dichter Text, Kleindruck
OcrConfig OcrConfig() / OcrConfig::default() OcrConfig(use_v5=True) / OcrConfig::v5()

Seitentyp-Erkennung

PDF Oxide klassifiziert Seiten automatisch, um zu entscheiden, ob OCR nötig ist. extract_text_ocr() erledigt das intern; Sie können Seitentypen aber auch manuell bestimmen.

Gescannte Seiten automatisch erkennen

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:
        # Wahrscheinlich gescannt — OCR verwenden
        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 => {
            // Hat nativen Text und gescannte Bilder — beide Quellen werden zusammengeführt
            let text = extract_text_with_ocr(&mut doc, i, Some(&engine), OcrExtractOptions::default())?;
            println!("Page {} (hybrid): {}...", i + 1, &text[..100.min(text.len())]);
        }
    }
}

PageType-Varianten (Rust)

Variante Beschreibung
NativeText Seite enthält eingebetteten Text — kein OCR erforderlich
ScannedPage Seite ist komplett gescannt (großes Bild, kein oder minimaler Text) — vollständiges OCR
HybridPage Seite enthält sowohl nativen Text als auch große gescannte Bilder — nativer Text wird mit OCR-Ergebnissen zusammengeführt

Die Hilfsfunktion needs_ocr() liefert sowohl bei ScannedPage als auch bei 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())?;
}

Funktionsweise

  1. PDF Oxide rendert die Seite intern als Bild (mit 300 DPI)
  2. Das Bild wird gemäß der Erkennungsstrategie skaliert (MaxSide für v3/v4, MinSide für v5)
  3. Der DBNet++-Textdetektor lokalisiert Textregionen als viereckige Begrenzungsrahmen
  4. Der SVTR-Textleser liest die Zeichen aus jeder erkannten Region
  5. Die Ergebnisse werden mit Lesereihenfolge-Sortierung zu Text zusammengesetzt
  6. Bei hybriden Seiten wird OCR-Text mit dem nativen Text verschmolzen

Die gesamte Pipeline läuft prozessintern über ONNX Runtime. Keine externen Binärdateien, keine Subprozess-Aufrufe, keine temporären Dateien.


OCR-Konfiguration

Python

from pdf_oxide import OcrConfig, OcrEngine

# Standard (v3/v4)
config = OcrConfig()

# PP-OCRv5-Servermodelle
config = OcrConfig(use_v5=True)

# Eigene Schwellenwerte
config = OcrConfig(
    det_threshold=0.5,    # Erkennungs-Konfidenz (0.0-1.0)
    box_threshold=0.7,    # Box-Konfidenz (0.0-1.0)
    rec_threshold=0.6,    # Lese-Konfidenz (0.0-1.0)
    num_threads=8,        # ONNX-Runtime-Threads
    max_candidates=500,   # Maximale Textregionen
)

# v5 mit eigenen Schwellenwerten
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};

// Standard (v3/v4): MaxSide { max_side: 960 }
let config = OcrConfig::default();

// PP-OCRv5: MinSide { min_side: 64, max_side_limit: 4000 }
let config = OcrConfig::v5();

// Eigener 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)        // Stilerkennung aus OCR-Geometrie aktivieren
    .build();

// Eigene Resize-Strategie
let config = OcrConfig::builder()
    .det_resize_strategy(DetResizeStrategy::MinSide {
        min_side: 128,
        max_side_limit: 6000,
    })
    .build();

DetResizeStrategy (Rust)

Steuert, wie Eingabebilder skaliert werden, bevor das Erkennungsmodell läuft.

Variante Felder Beschreibung
MaxSide max_side: u32 (Standard: 960) HERUNTERSKALIEREN, damit die längere Seite in max_side passt. Standard für PP-OCRv3/v4.
MinSide min_side: u32 (Standard: 64), max_side_limit: u32 (Standard: 4000) HOCHSKALIEREN, damit die kürzere Seite mindestens min_side hat, gedeckelt bei max_side_limit. Standard für PP-OCRv5.

OcrConfig-Felder

Feld Typ Standard Beschreibung
det_threshold f32 0.3 Wahrscheinlichkeitsschwelle der Erkennung
box_threshold f32 0.6 Konfidenzschwelle für Boxen
rec_threshold f32 0.5 Konfidenzschwelle fürs Textlesen
det_max_side u32 960 Maximale Bilddimension (v3/v4-Kompatibilität)
det_resize_strategy DetResizeStrategy MaxSide { 960 } Bild-Resize-Strategie
rec_target_height u32 48 Zielhöhe für Lese-Crops
num_threads usize 4 ONNX-Runtime-Inferenz-Threads
unclip_ratio f32 1.5 Box-Aufweitungsverhältnis
max_candidates usize 1000 Maximale Anzahl erkannter Textregionen
detect_styles bool true Schriftstile aus OCR-Geometrie ableiten
det_model_path Option<PathBuf> None Pfad zum eigenen Erkennungsmodell
rec_model_path Option<PathBuf> None Pfad zum eigenen Textlesemodell
dict_path Option<PathBuf> None Pfad zum eigenen Zeichenwörterbuch

Eigene Modelle

Eigene ONNX-Modelle anstelle der mitgelieferten verwenden:

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();

Stilerkennung

Ist detect_styles aktiviert (Standard), leitet PDF Oxide Schriftstile (fett, Überschriftenebene) aus der OCR-Geometrie ab — Textgröße, Abstand und Position. Das verbessert die Markdown-Ausgabe gescannter Seiten.

let config = OcrConfig::builder()
    .detect_styles(true)    // Stile aus Textgeometrie ableiten
    .build();

OCR vs Tesseract

Merkmal PDF Oxide OCR Tesseract (über PyMuPDF)
Installation pip install pdf_oxide Systempaket + pytesseract
Systemabhängigkeiten Keine Tesseract-Binary erforderlich
Laufzeit ONNX (prozessintern) Subprozess-Aufruf
Modellversionen PP-OCRv3, v4, v5 Tesseract LSTM
Sprachen Mehrsprachig Benötigt Sprachpakete
Setup-Aufwand Keiner Mittel
Erkennungsmodell DBNet++ Tesseract-intern
Textlesemodell SVTR / SVTR-v5 Tesseract LSTM
Hochauflösung MinSide-Strategie (v5) DPI-Einstellung
Seitentyp-Erkennung Automatisch (nativ/gescannt/hybrid) Manuell

Eigene DPI

Steuern Sie die Rendering-Auflösung, wenn PDF-Seiten für OCR in Bilder konvertiert werden:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("scanned.pdf")

# Standard sind 300 DPI — gute Balance zwischen Genauigkeit und Geschwindigkeit
text = doc.extract_text_ocr(0)

# Höhere DPI für bessere Genauigkeit bei Kleindruck
text = doc.extract_text_ocr(0)  # DPI wird in Rust über OcrExtractOptions konfiguriert

Rust

use pdf_oxide::ocr::OcrExtractOptions;

// Höhere DPI = genauer, aber langsamer
let options = OcrExtractOptions::default().with_dpi(300.0);

// Niedrigere DPI = schneller, aber weniger genau
let options = OcrExtractOptions::default().with_dpi(150.0);

OCR-Ausgabestruktur (Rust)

Die Methode OcrEngine::ocr_image() liefert detaillierte Ergebnisse mit Konfidenzwerten pro Span:

use pdf_oxide::ocr::OcrEngine;

let engine = OcrEngine::new("det.onnx", "rec.onnx", "dict.txt", Default::default())?;
let output = engine.ocr_image(&image)?;

// Gesamter Text in Lesereihenfolge
println!("{}", output.text_in_reading_order());

// Details pro Span
for span in &output.spans {
    println!("Text: '{}' (confidence: {:.2})", span.text, span.confidence);
    println!("  Begrenzungsrahmen: {:?}", span.bounding_rect());
    println!("  Konfidenz je Zeichen: {:?}", span.char_confidences);
}

// Gesamtkonfidenz
println!("Total confidence: {:.2}", output.total_confidence);

OcrOutput-Felder

Feld / Methode Typ Beschreibung
spans Vec<OcrSpan> Alle erkannten Textregionen
total_confidence f32 Durchschnittliche Konfidenz über alle Spans
text() String Gesamter Text mit Leerzeichen verbunden
text_in_reading_order() String Text nach Position sortiert (oben nach unten, links nach rechts)

OcrSpan-Felder

Feld Typ Beschreibung
text String Erkannter Text
polygon [[f32; 2]; 4] Viereckiger Begrenzungsrahmen (4 Ecken)
confidence f32 Gesamtkonfidenz (0,0–1,0)
char_confidences Vec<f32> Konfidenzwerte pro Zeichen

Stapel-OCR-Verarbeitung

Ein Verzeichnis gescannter PDFs verarbeiten:

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)?;
    }
}

Paralleles 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 zu Markdown

Gescannte Seiten in Markdown umwandeln:

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:
        # Gescannte Seite — erst OCR, dann formatieren
        text = doc.extract_text_ocr(i)
        md = text  # OCR-Ausgabe ist Klartext
    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);
}

Leistungsaspekte

OCR ist deutlich langsamer als die Textextraktion:

Operation Typische Geschwindigkeit
Textextraktion 0,8 ms pro Seite
OCR (v3/v4) 200–1.000 ms pro Seite
OCR (v5 Server) 500–2.000 ms pro Seite

Die OCR-Geschwindigkeit hängt von Seitenkomplexität, Bildauflösung, Textdichte und Modellversion ab. PP-OCRv5 ist langsamer, dafür aber genauer. Für große Stapel lohnt sich Parallelverarbeitung (siehe Stapel-OCR-Verarbeitung oben).


Modelle aus Bytes laden (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())?;

Verwandte Seiten