Skip to content

OCR de PDF en Python, Node.js, Go, C# y Rust — sin Tesseract

Extrae texto de PDFs escaneados con OCR integrado. Desde v0.3.27, el OCR está expuesto en todos los bindings — Python, Node.js, Go, C# y Rust — a través de una capa FFI unificada (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 trae PaddleOCR sobre ONNX Runtime — sin instalar Tesseract, sin dependencias del sistema, sin llamadas a subprocesos. El motor OCR corre directamente dentro del proceso. Soporta las familias de modelos PP-OCRv3, PP-OCRv4 y PP-OCRv5.

Nota: El OCR no está disponible en WebAssembly (requiere ONNX Runtime nativo). Para Go, Node.js, C# y Rust, compila con la feature ocr. Los wheels de Python ya vienen con OCR habilitado por defecto.

OCR de PDF en Python sin Tesseract

La mayoría de soluciones de OCR de PDF en Python exigen instalar Tesseract como dependencia del sistema — una configuración compleja que varía entre sistemas operativos y entornos de CI. PDF Oxide incluye los modelos de PaddleOCR directamente en el wheel de Python:

  • Sin dependencias del sistema — con pip install pdf_oxide es suficiente
  • Sin llamadas a subprocesos — el OCR corre nativo sobre ONNX Runtime
  • Tres familias de modelos — PP-OCRv3, PP-OCRv4 y PP-OCRv5
  • Detección automática de página — identifica qué páginas son escaneadas y cuáles tienen texto

Comparativa: OCR de PDF Oxide vs PyMuPDF + Tesseract

PDF Oxide PyMuPDF + Tesseract
Instalación pip install pdf_oxide pip install pymupdf + Tesseract del sistema
Motor OCR PaddleOCR (ONNX) Tesseract (subproceso)
Complejidad de setup Una línea Instalación de Tesseract específica del SO
CI/Docker Sin configuración extra Requiere apt-get install tesseract-ocr
Modelos incluidos Sí (en el wheel) No (descarga aparte)

Instalación

Python

pip install pdf_oxide

Los modelos de OCR ya vienen incluidos en el wheel. No hace falta ninguna descarga adicional.

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#

El paquete NuGet distribuye los binarios predeterminados de Linux, macOS y Windows con OCR habilitado, sin configuración adicional.

Cuándo usar OCR

La mayoría de los PDFs tienen texto embebido que extract_text() procesa en 0,8 ms por página. El OCR solo hace falta para:

  • Documentos escaneados — documentos en papel digitalizados a PDF
  • PDFs solo con imagen — PDFs creados desde fotos o capturas de pantalla
  • PDFs con texto como imagen — algunos generadores rasterizan el texto
  • Páginas híbridas — páginas con texto nativo y regiones de imagen escaneadas

Versiones de modelos PP-OCR

PDF Oxide soporta tres generaciones de modelos PaddleOCR. La configuración predeterminada sirve para PP-OCRv3 y PP-OCRv4. Los modelos de servidor PP-OCRv5 requieren una estrategia de redimensionado distinta.

PP-OCRv3 / PP-OCRv4 (predeterminado)

Modelos optimizados para móvil que escalan las imágenes hacia abajo para encajar en un tamaño máximo. Van bien con la mayoría de los documentos.

  • Modelo de detección: DBNet++ (ligero)
  • Modelo de reconocimiento: SVTR
  • Estrategia de redimensionado: MaxSide — reduce el lado más largo hasta 960 px
  • Ideal para: documentos estándar, despliegue móvil/edge

Python

from pdf_oxide import OcrConfig, OcrEngine

# La configuración por defecto sirve para modelos v3/v4
config = OcrConfig()
engine = OcrEngine("det_v4.onnx", "rec_v4.onnx", "dict.txt", config)

Rust

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

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

PP-OCRv5 (servidor)

Modelos de grado servidor que preservan alta resolución escalando las imágenes hacia arriba cuando hace falta. Notablemente más precisos con documentos densos o de letra pequeña.

  • Modelo de detección: DBNet++ (servidor, más grande)
  • Modelo de reconocimiento: SVTR-v5
  • Estrategia de redimensionado: MinSide — garantiza que el lado más corto tenga al menos 64 px, tope de 4000 px
  • Ideal para: extracción de alta precisión, entornos de servidor, texto denso

Python

from pdf_oxide import OcrConfig, OcrEngine

# Config v5: entrada de alta resolución para modelos de servidor
config = OcrConfig(use_v5=True)
engine = OcrEngine("det_v5.onnx", "rec_v5.onnx", "dict_v5.txt", config)

Rust

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

// Config 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)?;

Comparativa de modelos

Característica PP-OCRv3/v4 PP-OCRv5
Estrategia de redimensionado MaxSide (reduce a 960 px) MinSide (amplía, tope 4000 px)
Resolución de entrada Menor (más rápido) Mayor (más preciso)
Tamaño del modelo de detección ~3 MB ~12 MB
Tamaño del modelo de reconocimiento ~12 MB ~25 MB
Ideal para Móvil, edge, documentos estándar Servidor, texto denso, letra pequeña
OcrConfig OcrConfig() / OcrConfig::default() OcrConfig(use_v5=True) / OcrConfig::v5()

Detección del tipo de página

PDF Oxide clasifica las páginas automáticamente para decidir si hace falta OCR. extract_text_ocr() lo hace por dentro, pero también puedes detectar los tipos de página a mano.

Detectar páginas escaneadas automáticamente

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:
        # Probablemente escaneada — usar 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 => {
            // Tiene texto nativo e imágenes escaneadas — fusiona ambas fuentes
            let text = extract_text_with_ocr(&mut doc, i, Some(&engine), OcrExtractOptions::default())?;
            println!("Page {} (hybrid): {}...", i + 1, &text[..100.min(text.len())]);
        }
    }
}

Variantes de PageType (Rust)

Variante Descripción
NativeText La página tiene texto embebido — no hace falta OCR
ScannedPage La página está totalmente escaneada (imagen grande, sin texto o mínimo) — OCR completo
HybridPage La página tiene texto nativo e imágenes escaneadas grandes — fusiona texto nativo con los resultados de OCR

El helper needs_ocr() devuelve true tanto para ScannedPage como para HybridPage:

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

Cómo funciona

  1. PDF Oxide renderiza internamente la página como imagen (a 300 DPI)
  2. La imagen se redimensiona según la estrategia de detección (MaxSide para v3/v4, MinSide para v5)
  3. El detector de texto DBNet++ ubica las regiones de texto como cajas cuadrilaterales
  4. El reconocedor SVTR lee los caracteres de cada región detectada
  5. Los resultados se ensamblan en texto ordenado por lectura
  6. En páginas híbridas, el texto de OCR se fusiona con el texto nativo

Toda la pipeline corre en proceso sobre ONNX Runtime. Sin binarios externos, sin subprocesos, sin archivos temporales.


Configuración del OCR

Python

from pdf_oxide import OcrConfig, OcrEngine

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

# Modelos de servidor PP-OCRv5
config = OcrConfig(use_v5=True)

# Umbrales personalizados
config = OcrConfig(
    det_threshold=0.5,    # Confianza de detección (0.0-1.0)
    box_threshold=0.7,    # Confianza de caja (0.0-1.0)
    rec_threshold=0.6,    # Confianza de reconocimiento (0.0-1.0)
    num_threads=8,        # Hilos de ONNX Runtime
    max_candidates=500,   # Máx. regiones de texto
)

# v5 con umbrales personalizados
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};

// Predeterminado (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 personalizado
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)        // Activar detección de estilos por geometría OCR
    .build();

// Estrategia de redimensionado personalizada
let config = OcrConfig::builder()
    .det_resize_strategy(DetResizeStrategy::MinSide {
        min_side: 128,
        max_side_limit: 6000,
    })
    .build();

DetResizeStrategy (Rust)

Controla cómo se redimensionan las imágenes de entrada antes de ejecutar el modelo de detección.

Variante Campos Descripción
MaxSide max_side: u32 (predeterminado: 960) REDUCE hasta que el lado más largo quepa en max_side. Predeterminado para PP-OCRv3/v4.
MinSide min_side: u32 (predeterminado: 64), max_side_limit: u32 (predeterminado: 4000) AMPLÍA hasta que el lado más corto alcance min_side, tope en max_side_limit. Predeterminado para PP-OCRv5.

Campos de OcrConfig

Campo Tipo Predeterminado Descripción
det_threshold f32 0.3 Umbral de probabilidad de detección
box_threshold f32 0.6 Umbral de confianza de caja
rec_threshold f32 0.5 Umbral de confianza de reconocimiento
det_max_side u32 960 Dimensión máxima de imagen (compat. v3/v4)
det_resize_strategy DetResizeStrategy MaxSide { 960 } Estrategia de redimensionado
rec_target_height u32 48 Alto objetivo de recortes para reconocimiento
num_threads usize 4 Hilos de inferencia de ONNX Runtime
unclip_ratio f32 1.5 Ratio de expansión de caja
max_candidates usize 1000 Máximo de regiones de texto a detectar
detect_styles bool true Detectar estilos de fuente desde la geometría OCR
det_model_path Option<PathBuf> None Ruta a modelo de detección personalizado
rec_model_path Option<PathBuf> None Ruta a modelo de reconocimiento personalizado
dict_path Option<PathBuf> None Ruta a diccionario de caracteres personalizado

Modelos personalizados

Usa tus propios modelos ONNX en lugar de los incluidos:

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

Detección de estilos

Con detect_styles activo (predeterminado), PDF Oxide infiere estilos de fuente (negrita, nivel de encabezado) a partir de la geometría OCR — tamaño de texto, espaciado y posición. Esto mejora la conversión a Markdown desde páginas escaneadas.

let config = OcrConfig::builder()
    .detect_styles(true)    // Inferir estilos a partir de la geometría del texto
    .build();

OCR vs Tesseract

Característica OCR de PDF Oxide Tesseract (vía PyMuPDF)
Instalación pip install pdf_oxide Paquete del sistema + pytesseract
Dependencias del sistema Ninguna Binario de Tesseract requerido
Runtime ONNX (en proceso) Llamada a subproceso
Versiones de modelo PP-OCRv3, v4, v5 Tesseract LSTM
Idiomas Multilingüe Requiere paquetes de idioma
Complejidad de setup Cero Moderada
Modelo de detección DBNet++ Interno de Tesseract
Modelo de reconocimiento SVTR / SVTR-v5 Tesseract LSTM
Soporte alta resolución Estrategia MinSide (v5) Ajuste de DPI
Detección de tipo de página Automática (nativo/escaneado/híbrido) Manual

DPI personalizado

Controla la resolución de renderizado cuando se convierten páginas de PDF en imágenes para OCR:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("scanned.pdf")

# 300 DPI por defecto — buen equilibrio entre precisión y velocidad
text = doc.extract_text_ocr(0)

# Más DPI para mejor precisión con letra pequeña
text = doc.extract_text_ocr(0)  # DPI se configura en Rust vía OcrExtractOptions

Rust

use pdf_oxide::ocr::OcrExtractOptions;

// Más DPI = mejor precisión, pero más lento
let options = OcrExtractOptions::default().with_dpi(300.0);

// Menos DPI = más rápido, pero menos preciso
let options = OcrExtractOptions::default().with_dpi(150.0);

Estructura de salida de OCR (Rust)

El método OcrEngine::ocr_image() devuelve resultados detallados con puntuación de confianza por span:

use pdf_oxide::ocr::OcrEngine;

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

// Texto completo en orden de lectura
println!("{}", output.text_in_reading_order());

// Detalles por span
for span in &output.spans {
    println!("Text: '{}' (confidence: {:.2})", span.text, span.confidence);
    println!("  Caja: {:?}", span.bounding_rect());
    println!("  Confianza por carácter: {:?}", span.char_confidences);
}

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

Campos de OcrOutput

Campo / Método Tipo Descripción
spans Vec<OcrSpan> Todas las regiones de texto reconocidas
total_confidence f32 Confianza promedio entre todos los spans
text() String Todo el texto concatenado con espacios
text_in_reading_order() String Texto ordenado por posición (arriba-abajo, izquierda-derecha)

Campos de OcrSpan

Campo Tipo Descripción
text String Texto reconocido
polygon [[f32; 2]; 4] Caja cuadrilateral (4 esquinas)
confidence f32 Confianza global (0,0–1,0)
char_confidences Vec<f32> Puntuaciones de confianza por carácter

Procesamiento OCR por lotes

Procesa un directorio de PDFs escaneados:

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 en paralelo (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 a Markdown

Convierte páginas escaneadas a 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:
        # Página escaneada — OCR y luego formato
        text = doc.extract_text_ocr(i)
        md = text  # la salida de OCR es texto plano
    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);
}

Consideraciones de rendimiento

El OCR es bastante más lento que la extracción de texto:

Operación Velocidad típica
Extracción de texto 0,8 ms por página
OCR (v3/v4) 200–1.000 ms por página
OCR (v5 servidor) 500–2.000 ms por página

La velocidad del OCR depende de la complejidad de la página, la resolución de imagen, la densidad del texto y la versión del modelo. PP-OCRv5 es más lento pero más preciso. Para lotes grandes, usa procesamiento paralelo (ver Procesamiento OCR por lotes arriba).


Cargar modelos desde bytes (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())?;

Páginas relacionadas