OCR en PDF — Extraer Texto de PDFs Escaneados con PDF Oxide
Extrae texto de PDFs escaneados con OCR integrado. Desde la v0.3.27, el OCR está disponible en todos los bindings de lenguaje — 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}");
Java
import fyi.oxide.pdf.PdfDocument;
try (PdfDocument doc = PdfDocument.open("scanned.pdf")) {
// Lean-tier bindings have no OCR engine handle — extractTextAuto
// routes scanned pages through OCR automatically (graceful fallback).
String text = doc.extractTextAuto(0);
System.out.println(text);
}
PHP
<?php
use PdfOxide\PdfDocument;
$doc = PdfDocument::open("scanned.pdf");
// No OCR engine handle in the lean tier — extractTextAuto routes
// scanned pages through OCR automatically (graceful fallback).
echo $doc->extractTextAuto(0);
Ruby
require "pdf_oxide"
doc = PdfOxide::PdfDocument.open("scanned.pdf")
# No OCR engine handle in the lean tier — extract_text_auto routes
# scanned pages through OCR automatically (graceful fallback).
puts doc.extract_text_auto(0)
C++
#include <pdf_oxide/pdf_oxide.hpp>
auto doc = pdf_oxide::Document::open("scanned.pdf");
auto engine = pdf_oxide::OcrEngine::create("det.onnx", "rec.onnx", "dict.txt");
if (doc.ocr_page_needs_ocr(0)) {
std::string text = doc.ocr_extract_text(0, &engine);
std::cout << text << "\n";
}
Swift
import PdfOxide
let doc = try Document.open("scanned.pdf")
let engine = try OcrEngine.create(
detModelPath: "det.onnx", recModelPath: "rec.onnx", dictPath: "dict.txt")
if try doc.ocrPageNeedsOcr(0) {
let text = try doc.ocrExtractText(0, engine: engine)
print(text)
}
Kotlin
import fyi.oxide.pdf.PdfDocument
PdfDocument.open("scanned.pdf").use { doc ->
// Lean-tier bindings have no OCR engine handle — extractTextAuto
// routes scanned pages through OCR automatically (graceful fallback).
println(doc.extractTextAuto(0))
}
Dart
import 'package:pdf_oxide/pdf_oxide.dart';
final doc = PdfDocument.open('scanned.pdf');
final engine = OcrEngine.create('det.onnx', 'rec.onnx', 'dict.txt');
if (doc.pageNeedsOcr(0)) {
print(doc.ocrExtractText(0, engine));
}
engine.close();
doc.close();
R
library(pdfoxide)
doc <- pdf_open("scanned.pdf")
engine <- pdf_ocr_engine_create("det.onnx", "rec.onnx", "dict.txt")
if (pdf_ocr_page_needs_ocr(doc, 0)) {
text <- pdf_ocr_extract_text(doc, 0, engine)
cat(text)
}
Julia
using PdfOxide
doc = open_document("scanned.pdf")
engine = ocr_engine_create("det.onnx", "rec.onnx", "dict.txt")
if page_needs_ocr(doc, 0)
text = ocr_extract_text(doc, 0, engine)
println(text)
end
Zig
const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;
var doc = try pdf_oxide.Document.open("scanned.pdf");
defer doc.deinit();
var engine = try pdf_oxide.OcrEngine.create("det.onnx", "rec.onnx", "dict.txt");
defer engine.deinit();
if (try doc.ocrPageNeedsOcr(0)) {
const text = try doc.ocrExtractText(a, 0, engine);
defer a.free(text);
std.debug.print("{s}\n", .{text});
}
Scala
import fyi.oxide.pdf.PdfDocument
val doc = PdfDocument.open("scanned.pdf")
// Lean-tier bindings have no OCR engine handle — extractTextAuto
// routes scanned pages through OCR automatically (graceful fallback).
println(doc.extractTextAuto(0))
doc.close()
Clojure
(require '[pdf-oxide.core :as pdf])
(with-open [doc (pdf/open "scanned.pdf")]
;; Lean-tier bindings have no OCR engine handle — the AutoExtractor
;; routes scanned pages through OCR automatically (graceful fallback).
(let [ax (pdf/auto-extractor doc)]
(println (pdf/auto-text ax))))
Objective-C
#import "POXPdfOxide.h"
NSError *err = nil;
POXDocument *doc = [POXDocument openPath:@"scanned.pdf" error:&err];
POXOcrEngine *engine = [POXOcrEngine createWithDetModelPath:@"det.onnx"
recModelPath:@"rec.onnx"
dictPath:@"dict.txt"
error:&err];
if ([doc pageNeedsOcr:0 error:&err]) {
NSString *text = [doc ocrExtractText:0 engine:engine error:&err];
NSLog(@"%@", text);
}
Elixir
{:ok, doc} = PdfOxide.open("scanned.pdf")
{:ok, engine} = PdfOxide.ocr_engine("det.onnx", "rec.onnx", "dict.txt")
case PdfOxide.ocr_page_needs_ocr(doc, 0) do
{:ok, true} ->
{:ok, text} = PdfOxide.ocr_extract_text(doc, 0, engine)
IO.puts(text)
_ ->
:ok
end
PDF Oxide incluye PaddleOCR mediante ONNX Runtime — sin necesidad de instalar Tesseract, sin dependencias del sistema, sin llamadas a subprocesos. El motor OCR se ejecuta directamente dentro del proceso. Compatible con 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# / Rust, compila con el feature
ocr. Los wheels de Python vienen con OCR habilitado por defecto.
OCR de PDF en Python Sin Tesseract
La mayoría de las soluciones de OCR para PDF en Python requieren instalar Tesseract como dependencia del sistema — una configuración compleja que varía según el sistema operativo y el entorno de CI. PDF Oxide incluye los modelos PaddleOCR directamente en el wheel de Python:
- Sin dependencias del sistema —
pip install pdf_oxidees todo lo que necesitas - Sin llamadas a subprocesos — el OCR se ejecuta nativamente mediante ONNX Runtime
- Tres familias de modelos — PP-OCRv3, PP-OCRv4 y PP-OCRv5
- Detección automática de páginas — identifica qué páginas están escaneadas y cuáles tienen texto
Comparativa: PDF Oxide OCR 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 configuración | Una línea | Instalación de Tesseract específica por SO |
| CI/Docker | Sin configuración extra | Requiere apt-get install tesseract-ocr |
| Modelos incluidos | Sí (en el wheel) | No (descarga separada) |
Instalación
Python
pip install pdf_oxide
Los modelos OCR ya están incluidos en el wheel. No se requieren descargas adicionales.
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 incluye OCR habilitado en los binarios predeterminados para Linux / macOS / Windows — no se requiere configuración adicional.
Cuándo Usar OCR
La mayoría de los PDFs contienen texto incrustado que extract_text() procesa a 0,8 ms por página. El OCR solo es necesario para:
- Documentos escaneados — documentos en papel escaneados a PDF
- PDFs solo con imágenes — PDFs creados a partir de 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 escaneada
Versiones de Modelos PP-OCR
PDF Oxide admite tres generaciones de modelos PaddleOCR. La configuración predeterminada funciona con PP-OCRv3 y PP-OCRv4. Los modelos de servidor PP-OCRv5 requieren una estrategia de redimensionamiento diferente.
PP-OCRv3 / PP-OCRv4 (Predeterminado)
Modelos optimizados para móvil que reducen las imágenes para ajustarse a la longitud máxima del lado. Adecuados para la mayoría de documentos.
- Modelo de detección: DBNet++ (ligero)
- Modelo de reconocimiento: SVTR
- Estrategia de redimensionamiento:
MaxSide— reduce el lado más largo a 960 px - Mejor para: documentos estándar, implementación móvil/edge
Python
from pdf_oxide import OcrConfig, OcrEngine
# Default config works with v3/v4 models
config = OcrConfig()
engine = OcrEngine("det_v4.onnx", "rec_v4.onnx", "dict.txt", config)
Rust
use pdf_oxide::ocr::{OcrConfig, OcrEngine};
// Default config: 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 nivel servidor que preservan alta resolución ampliando las imágenes cuando es necesario. Significativamente más precisos en documentos con texto denso o letra pequeña.
- Modelo de detección: DBNet++ (servidor, más grande)
- Modelo de reconocimiento: SVTR-v5
- Estrategia de redimensionamiento:
MinSide— garantiza que el lado más corto tenga al menos 64 px, límite máximo de 4000 px - Mejor para: extracción de alta precisión, entornos de servidor, texto denso
Python
from pdf_oxide import OcrConfig, OcrEngine
# v5 config: high-resolution input for server models
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 config: 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 redimensionamiento | MaxSide (reducir a 960 px) |
MinSide (ampliar, límite 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 |
| Mejor 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 automáticamente las páginas para determinar si se necesita OCR. La función extract_text_ocr() lo gestiona internamente, pero también puedes detectar los tipos de página de forma manual.
Detección Automática de Páginas Escaneadas
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:
# Likely scanned — use 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 => {
// Has both native text and scanned images — merges both sources
let text = extract_text_with_ocr(&mut doc, i, Some(&engine), OcrExtractOptions::default())?;
println!("Page {} (hybrid): {}...", i + 1, &text[..100.min(text.len())]);
}
}
}
Java
import fyi.oxide.pdf.PdfDocument;
try (PdfDocument doc = PdfDocument.open("mixed.pdf")) {
for (int i = 0; i < doc.pageCount(); i++) {
// extractTextAuto classifies each page and routes scanned
// pages through OCR automatically (graceful fallback).
System.out.printf("Page %d: %s%n", i + 1, doc.extractTextAuto(i));
}
}
PHP
<?php
use PdfOxide\PdfDocument;
$doc = PdfDocument::open("mixed.pdf");
for ($i = 0; $i < $doc->pageCount(); $i++) {
// extractTextAuto classifies each page and routes scanned
// pages through OCR automatically (graceful fallback).
printf("Page %d: %s\n", $i + 1, $doc->extractTextAuto($i));
}
Ruby
require "pdf_oxide"
doc = PdfOxide::PdfDocument.open("mixed.pdf")
doc.page_count.times do |i|
# extract_text_auto classifies each page and routes scanned
# pages through OCR automatically (graceful fallback).
puts "Page #{i + 1}: #{doc.extract_text_auto(i)}"
end
C++
#include <pdf_oxide/pdf_oxide.hpp>
auto doc = pdf_oxide::Document::open("mixed.pdf");
auto engine = pdf_oxide::OcrEngine::create("det.onnx", "rec.onnx", "dict.txt");
for (int i = 0; i < doc.page_count(); ++i) {
std::string text = doc.ocr_page_needs_ocr(i)
? doc.ocr_extract_text(i, &engine) // scanned / hybrid → OCR
: doc.extract_text(i); // native text
std::cout << "Page " << (i + 1) << ": " << text << "\n";
}
Swift
import PdfOxide
let doc = try Document.open("mixed.pdf")
let engine = try OcrEngine.create(
detModelPath: "det.onnx", recModelPath: "rec.onnx", dictPath: "dict.txt")
for i in 0..<(try doc.pageCount()) {
let text = try doc.ocrPageNeedsOcr(i)
? doc.ocrExtractText(i, engine: engine) // scanned / hybrid → OCR
: doc.extractText(i) // native text
print("Page \(i + 1): \(text)")
}
Kotlin
import fyi.oxide.pdf.PdfDocument
PdfDocument.open("mixed.pdf").use { doc ->
for (i in 0 until doc.pageCount()) {
// extractTextAuto classifies each page and routes scanned
// pages through OCR automatically (graceful fallback).
println("Page ${i + 1}: ${doc.extractTextAuto(i)}")
}
}
Dart
import 'package:pdf_oxide/pdf_oxide.dart';
final doc = PdfDocument.open('mixed.pdf');
final engine = OcrEngine.create('det.onnx', 'rec.onnx', 'dict.txt');
for (var i = 0; i < doc.pageCount; i++) {
final text = doc.pageNeedsOcr(i)
? doc.ocrExtractText(i, engine) // scanned / hybrid → OCR
: doc.extractText(i); // native text
print('Page ${i + 1}: $text');
}
engine.close();
doc.close();
R
library(pdfoxide)
doc <- pdf_open("mixed.pdf")
engine <- pdf_ocr_engine_create("det.onnx", "rec.onnx", "dict.txt")
for (i in seq_len(pdf_page_count(doc)) - 1) {
text <- if (pdf_ocr_page_needs_ocr(doc, i)) {
pdf_ocr_extract_text(doc, i, engine) # scanned / hybrid -> OCR
} else {
pdf_extract_text(doc, i) # native text
}
cat(sprintf("Page %d: %s\n", i + 1, text))
}
Julia
using PdfOxide
doc = open_document("mixed.pdf")
engine = ocr_engine_create("det.onnx", "rec.onnx", "dict.txt")
for i in 0:(page_count(doc) - 1)
text = page_needs_ocr(doc, i) ?
ocr_extract_text(doc, i, engine) : # scanned / hybrid -> OCR
extract_text(doc, i) # native text
println("Page $(i + 1): $text")
end
Zig
const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;
var doc = try pdf_oxide.Document.open("mixed.pdf");
defer doc.deinit();
var engine = try pdf_oxide.OcrEngine.create("det.onnx", "rec.onnx", "dict.txt");
defer engine.deinit();
var i: i32 = 0;
const n = try doc.pageCount();
while (i < n) : (i += 1) {
const text = if (try doc.ocrPageNeedsOcr(i))
try doc.ocrExtractText(a, i, engine) // scanned / hybrid → OCR
else
try doc.extractText(a, i); // native text
defer a.free(text);
std.debug.print("Page {d}: {s}\n", .{ i + 1, text });
}
Scala
import fyi.oxide.pdf.PdfDocument
val doc = PdfDocument.open("mixed.pdf")
for (i <- 0 until doc.pageCount) {
// extractTextAuto classifies each page and routes scanned
// pages through OCR automatically (graceful fallback).
println(s"Page ${i + 1}: ${doc.extractTextAuto(i)}")
}
doc.close()
Clojure
(require '[pdf-oxide.core :as pdf])
(with-open [doc (pdf/open "mixed.pdf")]
;; The AutoExtractor classifies each page and routes scanned pages
;; through OCR automatically (graceful fallback).
(println (pdf/auto-text (pdf/auto-extractor doc))))
Objective-C
#import "POXPdfOxide.h"
NSError *err = nil;
POXDocument *doc = [POXDocument openPath:@"mixed.pdf" error:&err];
POXOcrEngine *engine = [POXOcrEngine createWithDetModelPath:@"det.onnx"
recModelPath:@"rec.onnx"
dictPath:@"dict.txt"
error:&err];
NSInteger n = [doc pageCountError:&err];
for (NSInteger i = 0; i < n; i++) {
NSString *text = [doc pageNeedsOcr:i error:&err]
? [doc ocrExtractText:i engine:engine error:&err] // scanned / hybrid → OCR
: [doc extractText:i error:&err]; // native text
NSLog(@"Page %ld: %@", (long)(i + 1), text);
}
Elixir
{:ok, doc} = PdfOxide.open("mixed.pdf")
{:ok, engine} = PdfOxide.ocr_engine("det.onnx", "rec.onnx", "dict.txt")
{:ok, n} = PdfOxide.page_count(doc)
for i <- 0..(n - 1) do
{:ok, text} =
case PdfOxide.ocr_page_needs_ocr(doc, i) do
{:ok, true} -> PdfOxide.ocr_extract_text(doc, i, engine) # scanned / hybrid -> OCR
_ -> PdfOxide.extract_text(doc, i) # native text
end
IO.puts("Page #{i + 1}: #{text}")
end
Variantes de PageType (Rust)
| Variante | Descripción |
|---|---|
NativeText |
La página tiene texto incrustado — no se necesita OCR |
ScannedPage |
La página está completamente escaneada (imagen grande, sin texto o mínimo) — OCR completo |
HybridPage |
La página tiene tanto texto nativo como imágenes escaneadas — combina el texto nativo con los resultados del OCR |
El auxiliar 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 la página a una imagen internamente (a 300 DPI)
- La imagen se redimensiona según la estrategia de detección (
MaxSidepara v3/v4,MinSidepara v5) - DBNet++ localiza regiones de texto como cajas delimitadoras cuadrilaterales
- SVTR lee los caracteres de cada región detectada
- Los resultados se ensamblan en texto con ordenación por orden de lectura
- En páginas híbridas, el texto OCR se combina con el texto nativo
Todo el pipeline se ejecuta dentro del proceso mediante ONNX Runtime. Sin binarios externos, sin llamadas a subprocesos, sin archivos temporales.
Configuración de OCR
Python
from pdf_oxide import OcrConfig, OcrEngine
# Default (v3/v4)
config = OcrConfig()
# PP-OCRv5 server models
config = OcrConfig(use_v5=True)
# Custom thresholds
config = OcrConfig(
det_threshold=0.5, # Detection confidence (0.0-1.0)
box_threshold=0.7, # Box confidence (0.0-1.0)
rec_threshold=0.6, # Recognition confidence (0.0-1.0)
num_threads=8, # ONNX Runtime threads
max_candidates=500, # Max text regions
)
# v5 with custom thresholds
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};
// Default (v3/v4): MaxSide { max_side: 960 }
let config = OcrConfig::default();
// PP-OCRv5: MinSide { min_side: 64, max_side_limit: 4000 }
let config = OcrConfig::v5();
// Custom 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) // Enable style detection from OCR geometry
.build();
// Custom resize strategy
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 la imagen para 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 para que el lado más corto tenga al menos min_side, con límite 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 la caja |
rec_threshold |
f32 |
0.5 |
Umbral de confianza del reconocimiento |
det_max_side |
u32 |
960 |
Dimensión máxima de imagen (compatible con v3/v4) |
det_resize_strategy |
DetResizeStrategy |
MaxSide { 960 } |
Estrategia de redimensionamiento de imagen |
rec_target_height |
u32 |
48 |
Altura objetivo para recortes de 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 a partir de la geometría OCR |
det_model_path |
Option<PathBuf> |
None |
Ruta al modelo de detección personalizado |
rec_model_path |
Option<PathBuf> |
None |
Ruta al modelo de reconocimiento personalizado |
dict_path |
Option<PathBuf> |
None |
Ruta al 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
Cuando detect_styles está habilitado (predeterminado), PDF Oxide infiere estilos de fuente (negrita, nivel de encabezado) a partir de la geometría OCR — tamaño del texto, espaciado y posición. Esto mejora la salida de la conversión a Markdown en páginas escaneadas.
let config = OcrConfig::builder()
.detect_styles(true) // Infer styles from text geometry
.build();
OCR vs. Tesseract
| Característica | PDF Oxide OCR | Tesseract (vía PyMuPDF) |
|---|---|---|
| Instalación | pip install pdf_oxide |
Paquete del sistema + pytesseract |
| Dependencias del sistema | Ninguna | Se requiere el binario de Tesseract |
| Runtime | ONNX (en proceso) | Llamada a subproceso |
| Versiones de modelos | PP-OCRv3, v4, v5 | Tesseract LSTM |
| Idiomas | Multilingüe | Requiere paquetes de idioma |
| Complejidad de configuración | Ninguna | Moderada |
| Modelo de detección | DBNet++ | Interno de Tesseract |
| Modelo de reconocimiento | SVTR / SVTR-v5 | Tesseract LSTM |
| Soporte de alta resolución | Estrategia MinSide (v5) |
Ajuste de DPI |
| Detección del tipo de página | Automática (nativo/escaneado/híbrido) | Manual |
DPI Personalizado
Controla la resolución de renderizado al convertir páginas PDF a imágenes para OCR:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("scanned.pdf")
# Default is 300 DPI — good balance of accuracy and speed
text = doc.extract_text_ocr(0)
# Higher DPI for better accuracy on fine print
text = doc.extract_text_ocr(0) # DPI configured via OcrExtractOptions in Rust
Rust
use pdf_oxide::ocr::OcrExtractOptions;
// Higher DPI = better accuracy but slower
let options = OcrExtractOptions::default().with_dpi(300.0);
// Lower DPI = faster but less accurate
let options = OcrExtractOptions::default().with_dpi(150.0);
Estructura de Salida OCR (Rust)
El método OcrEngine::ocr_image() devuelve resultados detallados con puntuaciones 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)?;
// Full text in reading order
println!("{}", output.text_in_reading_order());
// Per-span details
for span in &output.spans {
println!("Text: '{}' (confidence: {:.2})", span.text, span.confidence);
println!(" Bounding box: {:?}", span.bounding_rect());
println!(" Per-char confidence: {:?}", span.char_confidences);
}
// Overall confidence
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 de todos los spans |
text() |
String |
Todo el texto concatenado con espacios |
text_in_reading_order() |
String |
Texto ordenado por posición (arriba a abajo, izquierda a derecha) |
Campos de OcrSpan
| Campo | Tipo | Descripción |
|---|---|---|
text |
String |
Texto reconocido |
polygon |
[[f32; 2]; 4] |
Caja delimitadora cuadrilateral (4 esquinas) |
confidence |
f32 |
Confianza general (0.0–1.0) |
char_confidences |
Vec<f32> |
Puntuaciones de confianza por carácter |
Procesamiento OCR en Lote
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:
# Scanned page — OCR then format
text = doc.extract_text_ocr(i)
md = text # OCR output is plain text
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);
}
Java
import fyi.oxide.pdf.PdfDocument;
try (PdfDocument doc = PdfDocument.open("scanned-report.pdf")) {
for (int i = 0; i < doc.pageCount(); i++) {
String md = doc.toMarkdown(i);
if (md.strip().length() < 50) {
// Scanned page — auto-routing returns OCR text (plain).
md = doc.extractTextAuto(i);
}
System.out.printf("--- Page %d ---%n%s%n", i + 1, md);
}
}
PHP
<?php
use PdfOxide\PdfDocument;
$doc = PdfDocument::open("scanned-report.pdf");
for ($i = 0; $i < $doc->pageCount(); $i++) {
$md = $doc->toMarkdown($i);
if (strlen(trim($md)) < 50) {
// Scanned page — auto-routing returns OCR text (plain).
$md = $doc->extractTextAuto($i);
}
printf("--- Page %d ---\n%s\n", $i + 1, $md);
}
Ruby
require "pdf_oxide"
doc = PdfOxide::PdfDocument.open("scanned-report.pdf")
doc.page_count.times do |i|
md = doc.to_markdown(i)
if md.strip.length < 50
# Scanned page — auto-routing returns OCR text (plain).
md = doc.extract_text_auto(i)
end
puts "--- Page #{i + 1} ---\n#{md}"
end
C++
#include <pdf_oxide/pdf_oxide.hpp>
auto doc = pdf_oxide::Document::open("scanned-report.pdf");
auto engine = pdf_oxide::OcrEngine::create("det.onnx", "rec.onnx", "dict.txt");
for (int i = 0; i < doc.page_count(); ++i) {
std::string text = doc.ocr_page_needs_ocr(i)
? doc.ocr_extract_text(i, &engine) // scanned / hybrid → OCR
: doc.to_markdown(i); // native → Markdown
std::cout << "--- Page " << (i + 1) << " ---\n" << text << "\n";
}
Swift
import PdfOxide
let doc = try Document.open("scanned-report.pdf")
let engine = try OcrEngine.create(
detModelPath: "det.onnx", recModelPath: "rec.onnx", dictPath: "dict.txt")
for i in 0..<(try doc.pageCount()) {
let text = try doc.ocrPageNeedsOcr(i)
? doc.ocrExtractText(i, engine: engine) // scanned / hybrid → OCR
: doc.toMarkdown(i) // native → Markdown
print("--- Page \(i + 1) ---\n\(text)")
}
Kotlin
import fyi.oxide.pdf.PdfDocument
PdfDocument.open("scanned-report.pdf").use { doc ->
for (i in 0 until doc.pageCount()) {
var md = doc.toMarkdown(i)
if (md.trim().length < 50) {
// Scanned page — auto-routing returns OCR text (plain).
md = doc.extractTextAuto(i)
}
println("--- Page ${i + 1} ---\n$md")
}
}
Dart
import 'package:pdf_oxide/pdf_oxide.dart';
final doc = PdfDocument.open('scanned-report.pdf');
final engine = OcrEngine.create('det.onnx', 'rec.onnx', 'dict.txt');
for (var i = 0; i < doc.pageCount; i++) {
final text = doc.pageNeedsOcr(i)
? doc.ocrExtractText(i, engine) // scanned / hybrid → OCR
: doc.toMarkdown(i); // native → Markdown
print('--- Page ${i + 1} ---\n$text');
}
engine.close();
doc.close();
R
library(pdfoxide)
doc <- pdf_open("scanned-report.pdf")
engine <- pdf_ocr_engine_create("det.onnx", "rec.onnx", "dict.txt")
for (i in seq_len(pdf_page_count(doc)) - 1) {
text <- if (pdf_ocr_page_needs_ocr(doc, i)) {
pdf_ocr_extract_text(doc, i, engine) # scanned / hybrid -> OCR
} else {
pdf_to_markdown(doc, i) # native -> Markdown
}
cat(sprintf("--- Page %d ---\n%s\n", i + 1, text))
}
Julia
using PdfOxide
doc = open_document("scanned-report.pdf")
engine = ocr_engine_create("det.onnx", "rec.onnx", "dict.txt")
for i in 0:(page_count(doc) - 1)
text = page_needs_ocr(doc, i) ?
ocr_extract_text(doc, i, engine) : # scanned / hybrid -> OCR
to_markdown(doc, i) # native -> Markdown
println("--- Page $(i + 1) ---\n$text")
end
Zig
const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;
var doc = try pdf_oxide.Document.open("scanned-report.pdf");
defer doc.deinit();
var engine = try pdf_oxide.OcrEngine.create("det.onnx", "rec.onnx", "dict.txt");
defer engine.deinit();
var i: i32 = 0;
const n = try doc.pageCount();
while (i < n) : (i += 1) {
const text = if (try doc.ocrPageNeedsOcr(i))
try doc.ocrExtractText(a, i, engine) // scanned / hybrid → OCR
else
try doc.toMarkdown(a, i); // native → Markdown
defer a.free(text);
std.debug.print("--- Page {d} ---\n{s}\n", .{ i + 1, text });
}
Scala
import fyi.oxide.pdf.PdfDocument
val doc = PdfDocument.open("scanned-report.pdf")
for (i <- 0 until doc.pageCount) {
var md = doc.toMarkdown(i)
if (md.trim.length < 50) {
// Scanned page — auto-routing returns OCR text (plain).
md = doc.extractTextAuto(i)
}
println(s"--- Page ${i + 1} ---\n$md")
}
doc.close()
Clojure
(require '[pdf-oxide.core :as pdf])
(with-open [doc (pdf/open "scanned-report.pdf")]
;; The AutoExtractor routes scanned pages through OCR automatically.
(println (pdf/auto-text (pdf/auto-extractor doc))))
Objective-C
#import "POXPdfOxide.h"
NSError *err = nil;
POXDocument *doc = [POXDocument openPath:@"scanned-report.pdf" error:&err];
POXOcrEngine *engine = [POXOcrEngine createWithDetModelPath:@"det.onnx"
recModelPath:@"rec.onnx"
dictPath:@"dict.txt"
error:&err];
NSInteger n = [doc pageCountError:&err];
for (NSInteger i = 0; i < n; i++) {
NSString *text = [doc pageNeedsOcr:i error:&err]
? [doc ocrExtractText:i engine:engine error:&err] // scanned / hybrid → OCR
: [doc toMarkdown:i error:&err]; // native → Markdown
NSLog(@"--- Page %ld ---\n%@", (long)(i + 1), text);
}
Elixir
{:ok, doc} = PdfOxide.open("scanned-report.pdf")
{:ok, engine} = PdfOxide.ocr_engine("det.onnx", "rec.onnx", "dict.txt")
{:ok, n} = PdfOxide.page_count(doc)
for i <- 0..(n - 1) do
{:ok, text} =
case PdfOxide.ocr_page_needs_ocr(doc, i) do
{:ok, true} -> PdfOxide.ocr_extract_text(doc, i, engine) # scanned / hybrid -> OCR
_ -> PdfOxide.to_markdown(doc, i) # native -> Markdown
end
IO.puts("--- Page #{i + 1} ---\n#{text}")
end
Consideraciones de Rendimiento
El OCR es significativamente 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 la imagen, la densidad del texto y la versión del modelo. PP-OCRv5 es más lento pero más preciso. Para grandes lotes, considera el procesamiento paralelo (ver Procesamiento OCR en Lote 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 (usado internamente por OCR)
- Procesamiento en Lote — patrones de procesamiento paralelo
- Extraer Texto de PDF — guía de extracción de texto