Use Rust
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());
}
});