Skip to content

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 sistemapip install pdf_oxide es 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

  1. PDF Oxide renderiza la página a una imagen internamente (a 300 DPI)
  2. La imagen se redimensiona según la estrategia de detección (MaxSide para v3/v4, MinSide para v5)
  3. DBNet++ localiza regiones de texto como cajas delimitadoras cuadrilaterales
  4. SVTR lee los caracteres de cada región detectada
  5. Los resultados se ensamblan en texto con ordenación por orden de lectura
  6. 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