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_oxidees 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
- PDF Oxide renderiza internamente la página como imagen (a 300 DPI)
- La imagen se redimensiona según la estrategia de detección (
MaxSidepara v3/v4,MinSidepara v5) - El detector de texto DBNet++ ubica las regiones de texto como cajas cuadrilaterales
- El reconocedor SVTR lee los caracteres de cada región detectada
- Los resultados se ensamblan en texto ordenado por lectura
- 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
- Extracción de texto — extracción de texto estándar
- Conversión a Markdown — Markdown con detección de encabezados
- Renderizado de páginas — renderizar páginas como imágenes (uso interno del OCR)
- Procesamiento por lotes — patrones de procesamiento paralelo
- Extraer texto de PDF — guía de extracción de texto