How to Implement the OcrEngine Trait for Custom OCR Backends in LiteParse

To implement the OcrEngine trait for custom OCR backends in LiteParse, define a Send + Sync struct that implements the name() and recognize() methods, then register your engine using LiteParse::with_ocr_engine wrapped in an Arc to override the default selection logic.

LiteParse provides a pluggable abstraction for OCR processing through the OcrEngine trait defined in crates/liteparse/src/ocr/mod.rs. This trait enables developers to integrate any OCR backend—including cloud APIs, on-device machine learning models, or custom algorithms—while maintaining full compatibility with LiteParse's async document parsing pipeline.

Understanding the OcrEngine Trait Interface

The OcrEngine trait serves as the core abstraction between LiteParse's parsing logic and underlying OCR implementations. According to the source code in crates/liteparse/src/ocr/mod.rs, the trait definition includes platform-specific requirements:

// crates/liteparse/src/ocr/mod.rs
pub trait OcrEngine: Send + Sync {
    fn name(&self) -> &str;
    fn recognize<'a, 'b: 'a, 'c: 'a>(
        &'a self,
        image_data: &'c [u8],
        width: u32,
        height: u32,
        options: &'b OcrOptions,
    ) -> Pin<
        Box<
            dyn Future<
                Output = Result<Vec<OcrResult>, Box<dyn std::error::Error + Send + Sync>>
            > + Send + '_,
        >,
    >;
}

The trait requires two key methods:

  • name() – Returns a human-readable identifier for logging and debugging purposes.
  • recognize() – An async method receiving raw image bytes (typically PNG), dimensions, and OcrOptions, returning a pinned future that resolves to a vector of OcrResult structs.

Platform-specific constraints: On native targets (#[cfg(not(target_arch = "wasm32"))]), the returned future must be Send to allow the async runtime to move the engine across threads. On WebAssembly (wasm32), the trait remains Send + Sync but the future does not require Send because the runtime is single-threaded.

Step-by-Step Implementation Guide

Creating the Engine Struct

First, define a struct to hold your backend's configuration and state. This struct must be thread-safe for native platforms.

// crates/liteparse/src/ocr/my_custom.rs
use super::{OcrEngine, OcrOptions, OcrResult};
use std::future::Future;
use std::pin::Pin;

/// Custom OCR engine integrating with an external API.
pub struct MyCustomEngine {
    api_key: String,
    client: reqwest::Client,
}

impl MyCustomEngine {
    pub fn new(api_key: String) -> Self {
        Self {
            api_key,
            client: reqwest::Client::new(),
        }
    }
}

Implementing the Trait Methods

Implement OcrEngine for your struct, ensuring the future is Send for native targets. The recognize method receives raw image data and must return bounding boxes in [x1, y1, x2, y2] pixel coordinates.

impl OcrEngine for MyCustomEngine {
    fn name(&self) -> &str {
        "my-custom-ocr"
    }

    fn recognize<'a, 'b: 'a, 'c: 'a>(
        &'a self,
        image_data: &'c [u8],
        width: u32,
        height: u32,
        options: &'b OcrOptions,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<OcrResult>, Box<dyn std::error::Error + Send + Sync>>> + Send + '_>>
    {
        Box::pin(async move {
            // Send image_data to your OCR service
            // Parse the response and map to OcrResult structs
            
            Ok(vec![OcrResult {
                text: "Extracted text".to_string(),
                bbox: [0.0, 0.0, width as f32, height as f32],
                confidence: 0.95,
            }])
        })
    }
}

Handling Error Types

Errors must be boxed as dyn std::error::Error + Send + Sync. LiteParse surfaces these as LiteParseError in the calling code.

Complete Working Example

Below is a minimal "echo" implementation demonstrating the trait structure without external dependencies:

// crates/liteparse/src/ocr/echo_engine.rs
use super::{OcrEngine, OcrOptions, OcrResult};
use std::future::Future;
use std::pin::Pin;

/// A demonstration OCR engine that returns the requested language as text.
pub struct EchoEngine;

impl EchoEngine {
    pub fn new() -> Self {
        EchoEngine
    }
}

impl OcrEngine for EchoEngine {
    fn name(&self) -> &str {
        "echo"
    }

    fn recognize<'a, 'b: 'a, 'c: 'a>(
        &'a self,
        _image_data: &'c [u8],
        _width: u32,
        _height: u32,
        options: &'b OcrOptions,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<OcrResult>, Box<dyn std::error::Error + Send + Sync>>> + Send + '_>>
    {
        Box::pin(async move {
            Ok(vec![OcrResult {
                text: format!("language={}", options.language),
                bbox: [0.0, 0.0, 100.0, 20.0],
                confidence: 1.0,
            }])
        })
    }
}

Registering Your Custom Engine

The LiteParse struct in crates/liteparse/src/parser.rs (lines 43-57 and 71-78) selects OCR engines based on configuration. To override this selection and use your custom implementation, wrap your engine in Arc and call with_ocr_engine:

use liteparse::parser::LiteParse;
use liteparse::ocr::echo_engine::EchoEngine;
use std::sync::Arc;

let config = liteparse::config::LiteParseConfig::default();

let parser = LiteParse::new(config)
    .with_ocr_engine(Arc::new(EchoEngine::new()));

// Subsequent calls to parser.parse_input(...) will use EchoEngine
// instead of the default HTTP OCR or Tesseract backends.

The with_ocr_engine method stores the engine in the ocr_engine_override field, which takes precedence over the built-in selection logic.

Critical Implementation Details

When implementing custom OCR backends in LiteParse, adhere to the following constraints:

  • Thread Safety: On native platforms, ensure all internal state (HTTP clients, model handles) is Send + Sync. Use reqwest::Client for HTTP connections or protect mutable state with Mutex/RwLock.
  • Bounding Box Format: The bbox field in OcrResult expects [x1, y1, x2, y2] coordinates in pixels relative to the input image dimensions. Ensure your OCR service returns coordinates matching the width and height parameters passed to recognize.
  • Language Handling: The OcrOptions struct currently exposes the language field from LiteParseConfig. Respect this value when constructing requests to multilingual OCR services.
  • Future Type: The boxed future must include + Send on native builds but exclude it for wasm32 targets. Use conditional compilation or the trait's built-in platform abstraction to handle this.

Reference Implementations

Study the following source files for production-ready patterns:

Summary

  • Implement OcrEngine in crates/liteparse/src/ocr/mod.rs by defining a struct that is Send + Sync and providing the name() and recognize() methods.
  • Return a pinned future from recognize() that resolves to Vec<OcrResult> with text content, bounding boxes in [x1, y1, x2, y2] format, and confidence scores.
  • Register via with_ocr_engine by wrapping your engine in Arc<dyn OcrEngine> to override the default HTTP or Tesseract selection in crates/liteparse/src/parser.rs.
  • Handle platform differences by ensuring Send bounds on futures for native targets while maintaining compatibility with WebAssembly's single-threaded runtime.

Frequently Asked Questions

What image format does the recognize method receive?

The image_data parameter receives raw PNG bytes by default. LiteParse renders the input document to a PNG image before passing it to the OCR engine. Your implementation should either process PNG directly or decode it using an image library before sending to your backend service.

How do I handle authentication keys for external OCR APIs?

Store sensitive credentials in your engine struct (e.g., api_key: String) during construction. Implement Send + Sync for the struct to ensure thread safety. Use Arc<str> or String for the API key, and consider using environment variables or secure vaults to load these values when calling MyCustomEngine::new().

Can I use different OCR engines for different document types?

While LiteParse currently selects one engine per LiteParse instance, you can implement a composite engine that internally routes to different backends based on the OcrOptions or by analyzing the image content. Alternatively, create multiple LiteParse instances with different engines and route documents at the application level based on file extension or content type.

Why does my custom engine fail to compile on WebAssembly?

The OcrEngine trait requires Send + Sync on all platforms, but the future returned by recognize must only be Send on native targets. If you are manually implementing the future bounds, ensure you use the same conditional compilation flags (#[cfg(not(target_arch = "wasm32"))]) used in the LiteParse source code to add or remove the Send bound on the boxed future type.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →