Skip to content

Gestión de Modelos OCR sin Conexión

El OCR de PDF Oxide utiliza modelos ONNX de detección y reconocimiento almacenados en un directorio de caché local. Para compilaciones Docker, CI e implantaciones air-gapped / sin conexión, esos modelos deben estar disponibles antes de la primera llamada OCR; nunca se deben descargar durante una solicitud. PDF Oxide ofrece tres primitivas para ello:

  • prefetch_models — descarga el detector compartido más el modelo de reconocimiento y el diccionario de cada idioma en el directorio de caché (aprovisionamiento en tiempo de compilación).
  • model_manifest — un manifiesto JSON sin acceso a red que lista todos los archivos de modelo y sus URLs de origen, para espejado y verificación en hosts aislados.
  • prefetch_available — indica si esta compilación puede descargar (compilada con el feature ocr).

El directorio de caché es $PDF_OXIDE_MODEL_DIR si está definido; en caso contrario, el caché de la plataforma (~/.cache/pdf_oxide/models en Linux). Una vez que los archivos están disponibles, el OCR funciona completamente sin conexión.

Cobertura de bindings. El aprovisionamiento de modelos está disponible en Rust, Go, C# y Swift. model_manifest y prefetch_available también están disponibles en WASM/JavaScript (donde prefetchAvailable() siempre devuelve false — WASM no tiene descargador de red, así que el aprovisionamiento se realiza en el host usando el manifiesto). Los bindings de Python y Node N-API no exponen estas funciones en v0.3.69.

¿Cómo descargo previamente los modelos OCR para uso sin conexión?

prefetch_models acepta códigos de idioma separados por comas (vacío = inglés), descarga el detector compartido y el modelo de reconocimiento y diccionario de cada idioma en el directorio de caché, y devuelve la ruta de ese directorio. Es idempotente — los archivos que ya existen se omiten.

Rust

use pdf_oxide::extractors::auto::{AutoExtractor, OcrLanguage};

fn main() -> pdf_oxide::Result<()> {
    // AutoExtractor::prefetch_models(langs: &[OcrLanguage])
    //   -> Result<std::path::PathBuf>
    let dir = AutoExtractor::prefetch_models(&[
        OcrLanguage::English,
        OcrLanguage::Chinese,
        OcrLanguage::Arabic,
    ])?;
    println!("models cached in {}", dir.display());

    // One-shot English (the common case):
    let _ = AutoExtractor::prefetch_models_default()?;
    Ok(())
}

Go

package main

import (
	"fmt"
	"log"

	pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)

func main() {
	if !pdfoxide.PrefetchAvailable() {
		log.Fatal("this build cannot download models (built without the ocr feature)")
	}

	// func PrefetchModels(langs ...string) (string, error)
	dir, err := pdfoxide.PrefetchModels("english", "chinese", "arabic")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("models cached in", dir)
}

C#

using System;
using PdfOxide.Core;

if (!PdfDocument.PrefetchAvailable())
    throw new InvalidOperationException("built without the ocr feature; cannot download models");

// static string PdfDocument.PrefetchModels(params string[] languages)
string dir = PdfDocument.PrefetchModels("english", "chinese", "arabic");
Console.WriteLine($"models cached in {dir}");

Swift

import PdfOxide

guard PdfOxide.prefetchAvailable() == 1 else {
    fatalError("built without the ocr feature; cannot download models")
}

// static func prefetchModels(languagesCsv: String) throws -> String
let dir = try PdfOxide.prefetchModels(languagesCsv: "english,chinese,arabic")
print("models cached in \(dir)")

PHP

use PdfOxide\Pdf;

if (!Pdf::prefetchAvailable()) {
    throw new RuntimeException('built without the ocr feature; cannot download models');
}

// static Pdf::prefetchModels(array $languages): string
$dir = Pdf::prefetchModels(['english', 'chinese', 'arabic']);
echo "models cached in {$dir}\n";

Ruby

require 'pdf_oxide'

unless PdfOxide::Pdf.prefetch_available?
  raise 'built without the ocr feature; cannot download models'
end

# PdfOxide::Pdf.prefetch_models(languages) -> String (cache dir)
dir = PdfOxide::Pdf.prefetch_models(%w[english chinese arabic])
puts "models cached in #{dir}"

C++

#include <pdf_oxide/pdf_oxide.hpp>
#include <iostream>

if (pdf_oxide::prefetch_available() == 0)
    throw std::runtime_error("built without the ocr feature; cannot download models");

// std::string pdf_oxide::prefetch_models(const std::string& languages_csv)
auto dir = pdf_oxide::prefetch_models("english,chinese,arabic");
std::cout << "models cached in " << dir << "\n";

Dart

import 'package:pdf_oxide/pdf_oxide.dart' as pdf_oxide;

if (pdf_oxide.prefetchAvailable() == 0) {
  throw StateError('built without the ocr feature; cannot download models');
}

// String prefetchModels(String languagesCsv)
final dir = pdf_oxide.prefetchModels('english,chinese,arabic');
print('models cached in $dir');

R

library(pdfoxide)

if (pdf_prefetch_available() == 0)
  stop("built without the ocr feature; cannot download models")

# pdf_prefetch_models(languages_csv = NULL) -> cache directory path
dir <- pdf_prefetch_models("english,chinese,arabic")
cat("models cached in", dir, "\n")

Julia

using PdfOxide

prefetch_available() != 0 || error("built without the ocr feature; cannot download models")

# prefetch_models(languages_csv::AbstractString) -> cache directory path
dir = prefetch_models("english,chinese,arabic")
println("models cached in ", dir)

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

if (pdf_oxide.prefetchAvailable() == 0) return error.OcrFeatureMissing;

// prefetchModels(alloc, languages_csv) -> []u8 (cache dir; caller frees)
const dir = try pdf_oxide.prefetchModels(a, "english,chinese,arabic");
defer a.free(dir);
std.debug.print("models cached in {s}\n", .{dir});

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

if ([POXModels prefetchAvailable] <= 0) {
    @throw [NSException exceptionWithName:@"PdfOxide" reason:@"no ocr feature" userInfo:nil];
}

// + prefetchModels:error: returns a status JSON (nil on error)
NSString *status = [POXModels prefetchModels:@"english,chinese,arabic" error:&err];
NSLog(@"prefetch status: %@", status);

Elixir

unless PdfOxide.prefetch_available() != 0 do
  raise "built without the ocr feature; cannot download models"
end

# prefetch_models(languages_csv \\ "") -> JSON status string
status = PdfOxide.prefetch_models("english,chinese,arabic")
IO.puts("prefetch status: #{status}")

Códigos de idioma

prefetch_models acepta los siguientes códigos (los desconocidos se omiten; la entrada vacía usa inglés por defecto):

english, chinese, chinese_cht, japan, korean, arabic, cyrillic, latin, devanagari, ta (tamil), te (télugu), ka (canarés)

¿Cómo aprovisiono modelos en un host air-gapped?

En una máquina sin internet (o un target WASM sin descargador), no se puede llamar a prefetch_models. En su lugar, lea model_manifest — un listado JSON estático sin necesidad de red con todos los archivos de modelo y sus URLs upstream. Espeje esas URLs a través de su almacén de artefactos y coloque los archivos en $PDF_OXIDE_MODEL_DIR.

Rust

use pdf_oxide::extractors::auto::AutoExtractor;

fn main() {
    // AutoExtractor::model_manifest() -> String   (JSON, never errors)
    let manifest = AutoExtractor::model_manifest();
    println!("{manifest}");
}

Go

// func ModelManifest() string   (JSON, never errors)
fmt.Println(pdfoxide.ModelManifest())

C#

// static string PdfDocument.ModelManifest()
Console.WriteLine(PdfDocument.ModelManifest());

Swift

// static func modelManifest() -> String   (JSON)
print(PdfOxide.modelManifest())

JavaScript (WASM)

import init, { modelManifest, prefetchAvailable } from "pdf-oxide-wasm";

await init();

// prefetchAvailable() is always false in WASM — provision host-side.
console.log("can download here?", prefetchAvailable()); // false
console.log(modelManifest());                            // JSON manifest

PHP

use PdfOxide\FFI\FunctionBindings;

// (new FunctionBindings())->pdfOxideModelManifest(): string  (JSON, never errors)
$manifest = (new FunctionBindings())->pdfOxideModelManifest();
echo $manifest, "\n";

C++

#include <pdf_oxide/pdf_oxide.hpp>
#include <iostream>

// std::string pdf_oxide::model_manifest()   (JSON, never errors)
std::cout << pdf_oxide::model_manifest() << "\n";

Dart

import 'package:pdf_oxide/pdf_oxide.dart' as pdf_oxide;

// String modelManifest()   (JSON)
print(pdf_oxide.modelManifest());

R

library(pdfoxide)

# pdf_model_manifest() -> JSON string
cat(pdf_model_manifest(), "\n")

Julia

using PdfOxide

# model_manifest() -> JSON String
println(model_manifest())

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

// modelManifest(alloc) -> []u8 (JSON; caller frees)
const manifest = try pdf_oxide.modelManifest(a);
defer a.free(manifest);
std.debug.print("{s}\n", .{manifest});

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

// + manifestWithError: -> JSON string (nil on error)
NSString *manifest = [POXModels manifestWithError:&err];
NSLog(@"%@", manifest);

Elixir

# model_manifest() -> JSON string
IO.puts(PdfOxide.model_manifest())

¿Qué aspecto tiene el manifiesto?

{
  "detector": {
    "file": "det.onnx",
    "url": "https://.../det.onnx"
  },
  "languages": [
    {
      "language": "english",
      "rec_file": "rec.onnx",
      "dict_file": "en_dict.txt",
      "rec_url": "https://.../rec.onnx",
      "dict_url": "https://.../en_dict.txt"
    }
  ],
  "note": "Hebrew has no upstream PaddleOCR recognition model; the loader is ready if one is provided."
}

Espeje detector.url y el rec_url / dict_url de cada idioma, luego coloque det.onnx y cada rec_file / dict_file en su PDF_OXIDE_MODEL_DIR. A partir de ahí, el OCR funciona sin ningún acceso a la red.

¿Esta compilación admite la descarga de modelos?

prefetch_available indica si la biblioteca nativa fue compilada con el feature ocr (que incluye el descargador HTTP). Cuando devuelve false, prefetch_models sigue creando el directorio de caché pero no realiza ninguna descarga — verifíquelo antes de depender de una descarga.

Rust

use pdf_oxide::extractors::auto::AutoExtractor;

// AutoExtractor::prefetch_available() -> bool
if AutoExtractor::prefetch_available() {
    let _ = AutoExtractor::prefetch_models_default();
} else {
    eprintln!("OCR feature not compiled in — provision via model_manifest()");
}

Gopdfoxide.PrefetchAvailable() bool C#PdfDocument.PrefetchAvailable() -> bool SwiftPdfOxide.prefetchAvailable() -> Int32 (1 == yes)

Ejemplo de Dockerfile

Incorpore los modelos en la imagen durante el build para que el contenedor en ejecución nunca los descargue:

FROM rust:1 AS models
WORKDIR /app
COPY . .
# Build the CLI / your binary with the `ocr` feature, then prefetch.
ENV PDF_OXIDE_MODEL_DIR=/models
RUN cargo run --features ocr --bin prefetch -- english chinese

FROM debian:stable-slim
ENV PDF_OXIDE_MODEL_DIR=/models
COPY --from=models /models /models
# OCR now runs fully offline against /models

Configuración global del motor

Dos setters globales de proceso ajustan el motor de extracción. Ambos están disponibles en los bindings C-ABI, ambos devuelven el valor anterior y ninguno tiene canal de error (no pueden fallar). Al ser globales al proceso, configurarlos en un hilo afecta a todas las extracciones concurrentes.

¿Cómo aumento el límite de operadores de content-stream?

PDF Oxide limita los operadores de content-stream por stream (valor por defecto: 1.000.000) para acotar el coste de entradas adversariales. Los PDFs técnicos legítimos de gran tamaño (libros de texto, normas ISO) pueden superar ese límite. set_max_ops_per_stream sube (o baja) el límite y devuelve el valor anterior.

Rust

// pdf_oxide::content::parser::set_max_ops_per_stream(limit: Option<usize>)
//   -> Option<usize>   (None restores the 1,000,000 default)
use pdf_oxide::content::parser::set_max_ops_per_stream;

let prev = set_max_ops_per_stream(Some(5_000_000));
// ... extract a huge trusted PDF ...
set_max_ops_per_stream(prev); // restore

Go

// func SetMaxOpsPerStream(limit int64) int64   (returns previous cap)
prev := pdfoxide.SetMaxOpsPerStream(5_000_000)
defer pdfoxide.SetMaxOpsPerStream(prev)

C#

// static long CAbi.SetMaxOpsPerStream(long limit)   (returns previous cap)
long prev = PdfOxide.Core.CAbi.SetMaxOpsPerStream(5_000_000);
try { /* extract huge trusted PDF */ }
finally { PdfOxide.Core.CAbi.SetMaxOpsPerStream(prev); }

Swift

// static func setMaxOpsPerStream(_ limit: Int64) -> Int64
let prev = PdfOxide.setMaxOpsPerStream(5_000_000)
defer { _ = PdfOxide.setMaxOpsPerStream(prev) }

PHP

use PdfOxide\FFI\FunctionBindings;

$bindings = new FunctionBindings();
// pdfOxideSetMaxOpsPerStream(int $limit): int   (returns previous cap; -1 = default was active)
$prev = $bindings->pdfOxideSetMaxOpsPerStream(5_000_000);
try { /* extract huge trusted PDF */ }
finally { $bindings->pdfOxideSetMaxOpsPerStream($prev); }

Ruby

require 'pdf_oxide'

# PdfOxide.set_max_ops_per_stream(limit) -> previous cap (-1 = default was active)
prev = PdfOxide.set_max_ops_per_stream(5_000_000)
begin
  # ... extract a huge trusted PDF ...
ensure
  PdfOxide.set_max_ops_per_stream(prev)
end

C++

#include <pdf_oxide/pdf_oxide.hpp>

// std::int64_t pdf_oxide::set_max_ops_per_stream(std::int64_t limit) -> previous cap
auto prev = pdf_oxide::set_max_ops_per_stream(5'000'000);
// ... extract a huge trusted PDF ...
pdf_oxide::set_max_ops_per_stream(prev); // restore

Dart

import 'package:pdf_oxide/pdf_oxide.dart' as pdf_oxide;

// int setMaxOpsPerStream(int limit) -> previous cap
final prev = pdf_oxide.setMaxOpsPerStream(5000000);
// ... extract a huge trusted PDF ...
pdf_oxide.setMaxOpsPerStream(prev); // restore

R

library(pdfoxide)

# pdf_set_max_ops_per_stream(limit) -> previous cap (negative limit restores default)
prev <- pdf_set_max_ops_per_stream(5000000)
# ... extract a huge trusted PDF ...
pdf_set_max_ops_per_stream(prev)  # restore

Julia

using PdfOxide

# set_max_ops_per_stream(limit::Integer) -> previous cap
prev = set_max_ops_per_stream(5_000_000)
# ... extract a huge trusted PDF ...
set_max_ops_per_stream(prev)  # restore

Zig

const pdf_oxide = @import("pdf_oxide");

// setMaxOpsPerStream(limit: i64) i64   (returns previous cap)
const prev = pdf_oxide.setMaxOpsPerStream(5_000_000);
// ... extract a huge trusted PDF ...
_ = pdf_oxide.setMaxOpsPerStream(prev); // restore

Objective-C

#import "POXPdfOxide.h"

// + setMaxOpsPerStream: -> previous cap
int64_t prev = [POXConfig setMaxOpsPerStream:5000000];
// ... extract a huge trusted PDF ...
[POXConfig setMaxOpsPerStream:prev]; // restore

Elixir

# set_max_ops_per_stream(limit) -> previous cap (-1 = default was active)
prev = PdfOxide.set_max_ops_per_stream(5_000_000)
# ... extract a huge trusted PDF ...
PdfOxide.set_max_ops_per_stream(prev)

A nivel de C ABI, pdf_oxide_set_max_ops_per_stream(limit) interpreta un limit negativo como “restaurar el valor por defecto” y devuelve -1 cuando el valor por defecto estaba activo anteriormente.

¿Cómo preservo los glifos no mapeados (U+FFFD)?

Por defecto, los accesores de alto nivel (extract_text / extract_words / extract_spans) filtran los glifos sin mapeo Unicode (aparecerían como U+FFFD ). En páginas cuyos glifos visibles se mapean todos a U+FFFD — por ejemplo, fuentes de símbolos matemáticos como MSAM10 — esto puede producir salida vacía. set_preserve_unmapped_glyphs(true) hace que esos accesores conserven los caracteres de sustitución para que pueda verlos y postprocesarlos; devuelve el valor de configuración anterior.

Rust

// pdf_oxide::extractors::text::set_preserve_unmapped_glyphs(preserve: bool)
//   -> bool   (returns previous value)
use pdf_oxide::extractors::text::set_preserve_unmapped_glyphs;

let prev = set_preserve_unmapped_glyphs(true);
// ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
set_preserve_unmapped_glyphs(prev);

Go

// func SetPreserveUnmappedGlyphs(preserve int) int   (1 = preserve; returns previous)
prev := pdfoxide.SetPreserveUnmappedGlyphs(1)
defer pdfoxide.SetPreserveUnmappedGlyphs(prev)

C#

// static int CAbi.SetPreserveUnmappedGlyphs(bool preserve)   (returns previous, 0/1)
int prev = PdfOxide.Core.CAbi.SetPreserveUnmappedGlyphs(true);
try { /* extract math-heavy PDF */ }
finally { PdfOxide.Core.CAbi.SetPreserveUnmappedGlyphs(prev != 0); }

Swift

// static func setPreserveUnmappedGlyphs(_ preserve: Int32) -> Int32
let prev = PdfOxide.setPreserveUnmappedGlyphs(1)
defer { _ = PdfOxide.setPreserveUnmappedGlyphs(prev) }

PHP

use PdfOxide\FFI\FunctionBindings;

$bindings = new FunctionBindings();
// pdfOxideSetPreserveUnmappedGlyphs(int $preserve): int   (1 = preserve; returns previous, 0/1)
$prev = $bindings->pdfOxideSetPreserveUnmappedGlyphs(1);
try { /* extract math-heavy PDF */ }
finally { $bindings->pdfOxideSetPreserveUnmappedGlyphs($prev); }

Ruby

require 'pdf_oxide'

# PdfOxide.set_preserve_unmapped_glyphs(preserve) -> previous value (0 or 1)
prev = PdfOxide.set_preserve_unmapped_glyphs(true)
begin
  # ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
ensure
  PdfOxide.set_preserve_unmapped_glyphs(prev)
end

C++

#include <pdf_oxide/pdf_oxide.hpp>

// int pdf_oxide::set_preserve_unmapped_glyphs(int preserve) -> previous value
int prev = pdf_oxide::set_preserve_unmapped_glyphs(1);
// ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
pdf_oxide::set_preserve_unmapped_glyphs(prev); // restore

Dart

import 'package:pdf_oxide/pdf_oxide.dart' as pdf_oxide;

// int setPreserveUnmappedGlyphs(int preserve) -> previous value
final prev = pdf_oxide.setPreserveUnmappedGlyphs(1);
// ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
pdf_oxide.setPreserveUnmappedGlyphs(prev); // restore

R

library(pdfoxide)

# pdf_set_preserve_unmapped_glyphs(preserve) -> previous value (0 or 1)
prev <- pdf_set_preserve_unmapped_glyphs(1L)
# ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
pdf_set_preserve_unmapped_glyphs(prev)  # restore

Julia

using PdfOxide

# set_preserve_unmapped_glyphs(preserve::Integer) -> previous value (0 or 1)
prev = set_preserve_unmapped_glyphs(1)
# ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
set_preserve_unmapped_glyphs(prev)  # restore

Zig

const pdf_oxide = @import("pdf_oxide");

// setPreserveUnmappedGlyphs(preserve: bool) i32   (returns previous value)
const prev = pdf_oxide.setPreserveUnmappedGlyphs(true);
// ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
_ = pdf_oxide.setPreserveUnmappedGlyphs(prev != 0); // restore

Objective-C

#import "POXPdfOxide.h"

// + setPreserveUnmappedGlyphs: -> previous value (0 or 1)
int32_t prev = [POXConfig setPreserveUnmappedGlyphs:1];
// ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
[POXConfig setPreserveUnmappedGlyphs:prev]; // restore

Elixir

# set_preserve_unmapped_glyphs(preserve) -> previous value (0 or 1)
prev = PdfOxide.set_preserve_unmapped_glyphs(1)
# ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
PdfOxide.set_preserve_unmapped_glyphs(prev)

A nivel de C ABI, pdf_oxide_set_preserve_unmapped_glyphs(preserve) recibe 1 para preservar / 0 para filtrar y devuelve el valor anterior como 0 o 1.

Preguntas frecuentes

¿Dónde se almacenan los modelos OCR? En $PDF_OXIDE_MODEL_DIR si está definida; de lo contrario, en el caché de la plataforma (~/.cache/pdf_oxide/models en Linux). Esa ruta también es lo que devuelve prefetch_models.

¿Es seguro llamar a prefetch_models repetidamente? Sí — es idempotente. Los archivos existentes se omiten, así que es seguro llamarla en cada inicio como medida de seguridad.

¿Por qué prefetch_available devuelve false aunque haya llamado a prefetch? La compilación se realizó sin el feature ocr, por lo que no hay descargador HTTP. prefetch_models sigue creando el directorio de caché pero no descarga nada — aprovisione los archivos manualmente usando model_manifest.

¿Hay que restablecer los setters globales? Son globales al proceso y persisten hasta que se cambian, así que restaure el valor anterior (que cada setter devuelve) cuando solo desee aplicar la configuración a un documento concreto. Ambos setters no pueden fallar y no tienen canal de error.

Páginas relacionadas