How To

Use Rust

Using the pure Rust RustO! OCR engine and API in Rust applications.

The rusto-rs crate is the core pure Rust engine powering all RustO! platform bindings. It provides zero-cost abstractions, memory safety, and SIMD-accelerated image preprocessing.

Installation

Add rusto-rs to your Cargo.toml:

[dependencies]
rusto-rs = "0.3.2"

1. Engine Initialization (InitializeConfig)

Initialize the OCR engine once per application lifecycle or thread. RustO! provides presets matching different PaddleOCR model generations:

use rusto::{InitializeConfig, RustO};

// Initialize with the standard PP-OCRv6 preset
let config = InitializeConfig::ppv6("models/det.rten", "models/rec.rten", "models/dict.txt")
    .with_det_thresh(0.3)
    .with_det_box_thresh(0.6)
    .with_limit_side_len(736)
    .with_limit_type("min")
    .with_unclip_ratio(2.0)
    .with_use_dilation(true)
    .with_text_score(0.5)
    .with_rec_batch_num(6);

let mut ocr = RustO::initialize(config)?;

Auxiliary Models (Orientation & CLS)

For rotated scans or inverted text lines, attach orientation classification models:

// PP-OCRv5 with 180° line classifier (CLS) and whole-page rotator
let config = InitializeConfig::ppv5("models/det.rten", "models/rec.rten", "models/dict.txt")
    .with_cls_threshold("models/cls.rten", 0.9)
    .with_orientation_threshold("models/orient.rten", 0.85);

let mut ocr = RustO::initialize(config)?;

Generic & Pluggable Engine Composition

RustO! provides the InferenceSession trait and EngineSession enum to replace the inference backend (with ONNX Runtime ort, Tract, Candle, or custom engines) without altering downstream OCR postprocessing:

use rusto::{InferenceSession, EngineError, RustO, InitializeConfig, TextDetector, TextRecognizer};
use ndarray::ArrayD;
use std::collections::HashMap;

// Option 1: Supply custom inference sessions
let config = InitializeConfig::ppv6("models/det.rten", "models/rec.rten", "models/dict.txt");
let ocr = RustO::with_custom_engines(config, my_det_session, my_rec_session)?;

// Option 2: Compose pre-configured TextDetector and TextRecognizer
let det = TextDetector::new(config.det.clone())?;
let rec = TextRecognizer::new(config.rec.clone())?;
let ocr = RustO::with_components(config.global, det, rec)?;

2. Request-Local Options (OcrRunOptions)

Image bounds, detector resizing, dilation, unclip ratios, and output modes can be modified per detection call via OcrRunOptions without altering the initialized model sessions:

use rusto::{
    DetectionRunOptions, ImageSource, OcrRunOptions, OutputGranularity,
    PostprocessRunOptions,
};

let options = OcrRunOptions {
    output: OutputGranularity::Lines, // OutputGranularity::Words or OutputGranularity::Spatial
    line_y_threshold: Some(0.5),      // Dynamic row clustering tolerance
    word_x_threshold: Some(0.4),      // Word/column spacing tolerance
    text_score: Some(0.55),           // Minimum recognition score cutoff
    classification: Some(true),       // Evaluate CLS if configured
    max_side_len: Some(1600.0),       // Max image side length bound
    min_height: Some(32.0),           // Minimum text box height
    detection: Some(DetectionRunOptions {
        limit_side_len: Some(960),
        limit_type: Some("max".into()),
        ..Default::default()
    }),
    postprocess: Some(PostprocessRunOptions {
        use_dilation: Some(true),     // Morphological dilation for faint receipts
        unclip_ratio: Some(2.0),
        threshold: Some(0.3),
        box_threshold: Some(0.6),
        ..Default::default()
    }),
    ..Default::default()
};

3. Supported Image Sources

ImageSource accepts filesystem paths or in-memory byte buffers:

use rusto::ImageSource;
use std::path::PathBuf;

// 1. Filesystem path (loads and decodes efficiently)
let source_path = ImageSource::Path(PathBuf::from("data/invoice.jpg"));

// 2. In-memory encoded bytes (PNG, JPEG, WebP, BMP)
let raw_bytes: Vec<u8> = std::fs::read("data/invoice.jpg")?;
let source_bytes = ImageSource::Bytes(raw_bytes);

4. Practical Recipes & Output Handling

Recipe A: Extracting Structured Line Blocks

use rusto::{DetectTextResult, ImageSource, OcrRunOptions, OutputGranularity, RustO};

let mut options = OcrRunOptions::default();
options.output = OutputGranularity::Lines;

let result = ocr.detect_text(&ImageSource::Path("invoice.png".into()), &options)?;

if let DetectTextResult::Structured(items) = result {
    for item in items {
        println!("Text: {}", item.text);
        println!("Score: {:.3}", item.score);
        println!("Polygon Points [TL, TR, BR, BL]: {:?}", item.box_points);
        println!(
            "Axis-Aligned Frame: Left={}, Top={}, Width={}, Height={}",
            item.frame.left, item.frame.top, item.frame.width, item.frame.height
        );
    }
}

Example Output:

[
  {
    "text": "INVOICE #98214",
    "score": 0.992,
    "box_points": [
      [42.0, 35.0],
      [210.0, 35.0],
      [210.0, 58.0],
      [42.0, 58.0]
    ],
    "frame": { "left": 42.0, "top": 35.0, "width": 168.0, "height": 23.0 }
  },
  {
    "text": "Total Amount: $450.00",
    "score": 0.985,
    "box_points": [
      [42.0, 280.0],
      [245.0, 280.0],
      [245.0, 304.0],
      [42.0, 304.0]
    ],
    "frame": { "left": 42.0, "top": 280.0, "width": 203.0, "height": 24.0 }
  }
]

Recipe B: Reconstructing Document Spatial Layout

Preserve multi-column table alignment, invoice columns, and form fields into formatted text:

use rusto::{DetectTextResult, ImageSource, OcrRunOptions, OutputGranularity};

let mut options = OcrRunOptions::default();
options.output = OutputGranularity::Spatial;
options.line_y_threshold = Some(0.5);
options.word_x_threshold = Some(0.4);

if let DetectTextResult::Spatial(layout_text) = ocr.detect_text(
    &ImageSource::Path("table.png".into()),
    &options,
)? {
    println!("{layout_text}");
}

Example Output:

Item Description           Quantity     Unit Price     Subtotal
Widget Pro X100                   2         $75.00      $150.00
Heavy Duty Cable                  4         $12.50       $50.00
Mounting Bracket                  1         $35.00       $35.00
───────────────────────────────────────────────────────────────
Total Due:                                              $235.00

Recipe C: Multi-Threaded Batch Processing with Rayon

Because RustO instances hold inference session state, use thread-local instances or worker thread pools for concurrent high-throughput workloads:

use rayon::prelude::*;
use rusto::{DetectTextResult, ImageSource, InitializeConfig, OcrRunOptions, RustO};
use std::path::PathBuf;

let image_paths: Vec<PathBuf> = vec![
    "doc1.jpg".into(),
    "doc2.jpg".into(),
    "doc3.jpg".into(),
    "doc4.jpg".into(),
];

// Process concurrently across CPU cores
image_paths.par_iter().for_each(|path| {
    // Each thread initializes its own lightweight session
    let config = InitializeConfig::ppv6("models/det.rten", "models/rec.rten", "models/dict.txt");
    let mut thread_ocr = RustO::initialize(config).expect("Failed to init thread OCR");

    let result = thread_ocr.detect_text(
        &ImageSource::Path(path.clone()),
        &OcrRunOptions::default(),
    ).expect("OCR error");

    if let DetectTextResult::Structured(lines) = result {
        println!("Processed {:?}: {} lines found", path, lines.len());
    }
});