Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ tokenizers = { version = "0.23", optional = true, default-features = false, feat
# OCR - PaddleOCR via ONNX Runtime (optional)
ort = { version = "=2.0.0-rc.11", optional = true, default-features = false, features = [
"ndarray",
"load-dynamic",
"std",
] }
imageproc = { version = "0.27.0", optional = true }

Expand Down Expand Up @@ -414,8 +414,16 @@ python = [
ocr-tract = ["dep:tract-onnx", "dep:ndarray"]
ml = ["ocr-tract", "dep:linfa", "dep:linfa-clustering", "dep:tokenizers"]
table-ml = ["ml", "dep:pdfium-render"]
ocr = ["dep:ort", "dep:imageproc", "dep:ndarray", "dep:ureq"]
gpu = ["dep:ort", "ml"]
# Native ONNX Runtime OCR backend with loader/linker policy left to the final
# application. Static targets such as iOS can use their existing ORT linkage
# without Cargo feature unification forcing `disable-linking`.
ocr-ort = ["dep:ort", "dep:imageproc", "dep:ndarray", "dep:ureq"]
# Backwards-compatible OCR feature: existing consumers keep loading an ONNX
# Runtime shared library at runtime (for example via `ORT_DYLIB_PATH`).
ocr = ["ocr-ort", "ort/load-dynamic"]
# Preserve the existing GPU feature's dynamic-loader behavior. Only `ocr-ort`
# is intentionally loader-neutral for embedding applications.
gpu = ["dep:ort", "ort/load-dynamic", "ml"]
wasm = [
"dep:wasm-bindgen",
"dep:web-sys",
Expand Down
46 changes: 33 additions & 13 deletions docs/OCR_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ PDFOxide automatically detects whether a page is scanned or has native text, so
# 1. Download recommended models (~12.5 MB total)
./scripts/setup_ocr_models.sh

# 2. Run (Rust)
# 2. Run (Rust, loading an installed ONNX Runtime at runtime)
cargo run --features ocr --example ocr_scanned_pdf -- \
--pdf scanned.pdf \
--det .models/det.onnx \
Expand All @@ -28,15 +28,17 @@ cargo run --features ocr --example ocr_scanned_pdf -- \

## OCR Support by Binding

OCR *recognition* needs the native `ocr` feature compiled in **plus** an
ONNX Runtime shared library and provisioned models at runtime.
OCR *recognition* needs the native ONNX Runtime backend compiled in **plus**
an ONNX Runtime library and provisioned models. The backwards-compatible
`ocr` feature loads the runtime dynamically. Applications that already
manage ONNX Runtime linkage can use the loader-neutral `ocr-ort` feature.
**Auto mode works in every binding regardless**: when OCR is unavailable
it degrades gracefully to native text with a typed
`ocr_requested_but_unavailable` reason — never a crash or silent empty.

| Binding | OCR recognition | How |
|---|---|---|
| Rust | yes | build with `--features ocr` |
| Rust | yes | use `ocr` for runtime loading, or `ocr-ort` for caller-managed linking |
| Python | yes | the published wheel ships `ocr`; supply ONNX Runtime + models |
| Node.js / TypeScript | yes (v0.3.52+) | the published prebuilt ships `ocr`; `npm i onnxruntime-node` + models |
| Go (cgo + purego) | yes (v0.3.52+) | the published native lib ships `ocr`; supply ONNX Runtime + models |
Expand Down Expand Up @@ -356,7 +358,19 @@ The `setup_ocr_models.sh` script handles this automatically.

## ONNX Runtime Setup

The OCR feature requires ONNX Runtime v1.23+ at runtime.
The OCR feature requires ONNX Runtime v1.23+. PDFOxide offers two feature
levels so the final application can own loader/linker policy when needed:

- `ocr` is the backwards-compatible feature. It includes `ocr-ort` and enables
`ort/load-dynamic`. Use `ORT_DYLIB_PATH` at runtime or put the shared library
on the platform loader search path.
- `ocr-ort` enables the same ONNX Runtime OCR backend without forcing a loading
strategy. Configure normal `ort-sys` linking for the target, such as with
`ORT_LIB_LOCATION` or `ORT_IOS_XCFWK_LOCATION` at build time.

Cargo features are additive. If any crate in the final dependency graph
enables `ocr` (or `ort/load-dynamic` directly), dynamic loading
applies to the shared `ort` dependency for the whole application.

### Option 1: System Install

Expand All @@ -369,12 +383,12 @@ wget https://github.com/microsoft/onnxruntime/releases/download/v1.23.0/onnxrunt
tar xzf onnxruntime-linux-x64-1.23.0.tgz
```

### Option 2: Point to an installed ONNX Runtime (runtime)
### Option 2: Point to an installed ONNX Runtime (runtime loading)

This crate loads ONNX Runtime **dynamically at runtime** (the `ort` `load-dynamic`
feature), so you point it at an installed library — no rebuild needed. Note that
`ORT_LIB_LOCATION` is a *build-time* variable for `ort`'s download/static-link
strategies and has **no effect** here; use one of:
With `ocr`, PDFOxide loads ONNX Runtime **dynamically at runtime**,
so you point it at an installed library — no rebuild needed. Note that
`ORT_LIB_LOCATION` is a *build-time* variable for linked builds and has **no
effect** in this mode; use one of:

```bash
# Either give ort the full path to the shared-library FILE:
Expand All @@ -386,6 +400,12 @@ export LD_LIBRARY_PATH=/path/to/onnxruntime/lib:$LD_LIBRARY_PATH # DYLD_LIBRA
cargo run --features ocr --example ocr_scanned_pdf -- ...
```

For a linked build, enable `ocr-ort` instead and configure `ort-sys` at build time.
For example, `ORT_LIB_LOCATION` points to the ONNX Runtime library directory;
`ORT_PREFER_DYNAMIC_LINK=1` requests ordinary loader-linked shared-library
linking rather than static linking. iOS applications can supply an existing
XCFramework with `ORT_IOS_XCFWK_LOCATION`.

### macOS

```bash
Expand All @@ -402,8 +422,8 @@ export ORT_DYLIB_PATH="$(brew --prefix onnxruntime)/lib/libonnxruntime.dylib"

The **default** `pdf-oxide-wasm` package ships **without** OCR — its
`WasmOcrEngine` / `extractTextOcr` throw an error directing you to the
`wasm-ocr` build. (The native `ort` OCR backend links a native ONNX
Runtime shared library and does not target `wasm32`.) Auto mode still
`wasm-ocr` build. (The native `ort` OCR backend requires a native ONNX
Runtime library and does not target `wasm32`.) Auto mode still
works there, falling back to native text with a typed reason.

The **`wasm-ocr` build** (issue #524, *experimental*) runs OCR entirely
Expand Down Expand Up @@ -510,7 +530,7 @@ Make sure you're using `OcrConfig::v5()` (Rust) or `OcrConfig(use_v5=True)` (Pyt

### Build error: `no method named tls_config`

This is a known bug in `ort-sys` 2.0.0-rc.11 when using the `download-binaries` feature. This crate uses the `load-dynamic` feature instead, so install ONNX Runtime manually and point to it at runtime — `ORT_DYLIB_PATH=/path/to/libonnxruntime.<so|dylib|dll>`, or add its directory to `LD_LIBRARY_PATH` (`DYLD_LIBRARY_PATH` on macOS). See "Point to an installed ONNX Runtime" above.
This is a known bug in `ort-sys` 2.0.0-rc.11 when using the `download-binaries` feature. Use `ocr`, install ONNX Runtime manually, and point to it at runtime — `ORT_DYLIB_PATH=/path/to/libonnxruntime.<so|dylib|dll>`, or add its directory to `LD_LIBRARY_PATH` (`DYLD_LIBRARY_PATH` on macOS). See "Point to an installed ONNX Runtime" above.

### Python segfault (exit code 139)

Expand Down
9 changes: 7 additions & 2 deletions docs/getting-started-rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,13 +359,18 @@ converter.convert("input.pdf", "archive.pdf")?;

> For a comprehensive guide covering model selection, configuration reference, resize strategies, and troubleshooting, see the [OCR Guide](OCR_GUIDE.md).

PDFOxide can extract text from scanned PDFs using PaddleOCR models via ONNX Runtime. Enable the `ocr` feature:
PDFOxide can extract text from scanned PDFs using PaddleOCR models via ONNX
Runtime. Enable `ocr` to load an installed runtime dynamically:

```toml
[dependencies]
pdf_oxide = { version = "0.3", features = ["ocr"] }
```

Applications that already manage ONNX Runtime linkage can enable `ocr-ort`
instead. That enables the same OCR backend without forcing
`ort/load-dynamic`; configure `ort-sys` for the target at build time.

### Model Setup

PDFOxide supports PaddleOCR v3, v4, and v5 models. You can mix detection and recognition models from different versions.
Expand Down Expand Up @@ -444,7 +449,7 @@ let config = OcrConfig::v5();
let engine = OcrEngine::new("v5_det.onnx", "v5_rec.onnx", "v5_dict.txt", config)?;
```

> **Note:** ONNX Runtime (`libonnxruntime` v1.23+) is loaded dynamically at runtime. Install it (system package, `brew install onnxruntime`, or a manual download) and either set `ORT_DYLIB_PATH` to the shared-library **file** (`libonnxruntime.so` / `.dylib` / `onnxruntime.dll`), or add its directory to `LD_LIBRARY_PATH` (`DYLD_LIBRARY_PATH` on macOS). `ORT_LIB_LOCATION` is a build-time variable and has no effect with the dynamic backend this crate uses.
> **Note:** With `ocr`, ONNX Runtime (`libonnxruntime` v1.23+) is loaded dynamically at runtime. Install it (system package, `brew install onnxruntime`, or a manual download) and either set `ORT_DYLIB_PATH` to the shared-library **file** (`libonnxruntime.so` / `.dylib` / `onnxruntime.dll`), or add its directory to `LD_LIBRARY_PATH` (`DYLD_LIBRARY_PATH` on macOS). With `ocr-ort`, loader/linker policy remains caller-controlled and `ORT_LIB_LOCATION` or `ORT_IOS_XCFWK_LOCATION` can be supplied at build time.

## Lower-Level APIs

Expand Down
12 changes: 6 additions & 6 deletions examples/ocr_scanned_pdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,28 @@
//! --v5
//! ```

#[cfg(feature = "ocr")]
#[cfg(feature = "ocr-ort")]
use pdf_oxide::document::PdfDocument;
#[cfg(feature = "ocr")]
#[cfg(feature = "ocr-ort")]
use pdf_oxide::ocr::{self, OcrConfig, OcrEngine, OcrExtractOptions};
#[cfg(feature = "ocr")]
#[cfg(feature = "ocr-ort")]
use std::env;

fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(not(feature = "ocr"))]
#[cfg(not(feature = "ocr-ort"))]
{
eprintln!("This example requires the 'ocr' feature to be enabled.");
eprintln!("Run with: cargo run --features ocr --example ocr_scanned_pdf");
Err("OCR feature not enabled".into())
}

#[cfg(feature = "ocr")]
#[cfg(feature = "ocr-ort")]
{
run_ocr()
}
}

#[cfg(feature = "ocr")]
#[cfg(feature = "ocr-ort")]
fn run_ocr() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();

Expand Down
28 changes: 14 additions & 14 deletions src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5428,10 +5428,10 @@ impl PdfDocument {
/// assembled exactly as [`extract_text_with_options`](Self::extract_text_with_options)
/// would, so the native text is byte-for-byte preserved; the extra spans
/// only add content, sorted in by their bounding box.
// Only the Auto extractor (behind the `ocr` feature) and the unit test that
// Only the Auto extractor (behind the `ocr-ort` feature) and the unit test that
// pins span placement call this, so it is compiled only in those configs —
// a plain non-`ocr` `--lib` build omits it entirely (no dead code).
#[cfg(any(feature = "ocr", test))]
// a build without the OCR backend omits it entirely (no dead code).
#[cfg(any(feature = "ocr-ort", test))]
pub(crate) fn extract_text_with_extra_spans(
&self,
page_index: usize,
Expand Down Expand Up @@ -6032,7 +6032,7 @@ impl PdfDocument {
spans.extend(marginalia_trailing);

// OCR fallback for scanned PDFs
#[cfg(feature = "ocr")]
#[cfg(feature = "ocr-ort")]
if spans.is_empty() || spans.iter().map(|s| s.text.len()).sum::<usize>() < 50 {
if let Ok(true) = crate::ocr::needs_ocr(self, page_index) {
log::debug!(
Expand Down Expand Up @@ -6740,7 +6740,7 @@ impl PdfDocument {
/// // Automatically uses native text or OCR as needed
/// let text = doc.extract_text_with_ocr(0, Some(&engine), OcrExtractOptions::default())?;
/// ```
#[cfg(feature = "ocr")]
#[cfg(feature = "ocr-ort")]
pub fn extract_text_with_ocr(
&self,
page_index: usize,
Expand Down Expand Up @@ -6776,7 +6776,7 @@ impl PdfDocument {
/// when the OCR backend fails to initialise (e.g. missing
/// `libonnxruntime.so`) — the [`catch_unwind`](std::panic::catch_unwind)
/// in `OrtBackend::from_bytes` keeps that path panic-free.
#[cfg(feature = "ocr")]
#[cfg(feature = "ocr-ort")]
pub fn extract_text_ocr_only(
&self,
page_index: usize,
Expand Down Expand Up @@ -6808,7 +6808,7 @@ impl PdfDocument {
/// # Returns
///
/// Vector of TextSpans, either from native PDF or OCR.
#[cfg(feature = "ocr")]
#[cfg(feature = "ocr-ort")]
pub fn extract_spans_with_ocr(
&self,
page_index: usize,
Expand Down Expand Up @@ -19717,9 +19717,9 @@ impl PdfDocument {
/// [`extract_text_with_extra_spans`](Self::extract_text_with_extra_spans).
/// The Auto extractor uses this to drop OCR'd image text into its figure's
/// reading-order slot for Markdown, so auto markdown is a superset of native.
// Only the (ocr-gated) Auto extractor calls this, so compile it only with
// the `ocr` feature — a non-`ocr` build omits it (no dead code).
#[cfg(feature = "ocr")]
// Only the (`ocr-ort`-gated) Auto extractor calls this, so a build without
// the OCR backend omits it (no dead code).
#[cfg(feature = "ocr-ort")]
pub(crate) fn to_markdown_with_extra_spans(
&self,
page_index: usize,
Expand Down Expand Up @@ -20194,7 +20194,7 @@ impl PdfDocument {
/// &OcrExtractOptions::default()
/// )?;
/// ```
#[cfg(feature = "ocr")]
#[cfg(feature = "ocr-ort")]
pub fn to_markdown_with_ocr(
&self,
page_index: usize,
Expand Down Expand Up @@ -20305,9 +20305,9 @@ impl PdfDocument {
/// Convert a page to HTML with caller-supplied extra spans merged into the
/// converter's reading-order pass — the HTML companion to
/// [`to_markdown_with_extra_spans`](Self::to_markdown_with_extra_spans).
// Only the (ocr-gated) Auto extractor calls this, so compile it only with
// the `ocr` feature — a non-`ocr` build omits it (no dead code).
#[cfg(feature = "ocr")]
// Only the (`ocr-ort`-gated) Auto extractor calls this, so a build without
// the OCR backend omits it (no dead code).
#[cfg(feature = "ocr-ort")]
pub(crate) fn to_html_with_extra_spans(
&self,
page_index: usize,
Expand Down
6 changes: 3 additions & 3 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ pub enum Error {
Ml(String),

/// OCR error. Available whenever the OCR module is compiled —
/// `ocr` (native ONNX Runtime) or `ocr-tract` (pure-Rust tract /
/// wasm, which `ml` implies — issue #524).
#[cfg(any(feature = "ocr", feature = "ocr-tract"))]
/// `ocr-ort` (native ONNX Runtime; implied by legacy `ocr`) or
/// `ocr-tract` (pure-Rust tract / wasm, which `ml` implies — issue #524).
#[cfg(any(feature = "ocr-ort", feature = "ocr-tract"))]
#[error("OCR error: {0}")]
Ocr(String),

Expand Down
Loading