#[cfg(all(target_arch = "wasm32", feature = "image"))]
mod host_engine;
#[cfg(feature = "ocr-ocrs")]
mod ocrs_engine;
#[cfg(not(target_arch = "wasm32"))]
mod tesseract_cli;
#[cfg(all(target_arch = "wasm32", feature = "image"))]
pub use host_engine::{host_ocr_registered, register_host_ocr_backend, HostOcrEngine};
#[cfg(feature = "ocr-ocrs")]
pub use ocrs_engine::OcrsEngine;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use tesseract_cli::parse_tesseract_tsv;
#[cfg(not(target_arch = "wasm32"))]
pub use tesseract_cli::TesseractCliEngine;
use image::GrayImage;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct OcrWord {
pub line_key: (u32, u32, u32),
pub left: u32,
pub top: u32,
pub width: u32,
pub height: u32,
pub text: String,
pub confidence: f64,
}
pub trait OcrEngine: Send + Sync {
fn recognize(&self, gray: &GrayImage) -> Vec<OcrWord>;
fn recognize_psm(&self, gray: &GrayImage, _psm: &str) -> Vec<OcrWord> {
self.recognize(gray)
}
}
pub fn default_engine() -> Arc<dyn OcrEngine> {
#[cfg(all(target_arch = "wasm32", feature = "image"))]
{
if HostOcrEngine::try_new().is_some() {
#[cfg(feature = "ocr-ocrs")]
let secondary: Option<Arc<dyn OcrEngine>> =
OcrsEngine::try_from_env().map(|e| Arc::new(e) as Arc<dyn OcrEngine>);
#[cfg(not(feature = "ocr-ocrs"))]
let secondary: Option<Arc<dyn OcrEngine>> = None;
return Arc::new(FallbackOcrEngine {
primary: Arc::new(HostOcrEngine),
secondary,
});
}
}
#[cfg(feature = "ocr-ocrs")]
{
if let Some(engine) = OcrsEngine::try_from_env() {
return Arc::new(engine);
}
}
#[cfg(not(target_arch = "wasm32"))]
{
Arc::new(TesseractCliEngine::default())
}
#[cfg(target_arch = "wasm32")]
{
Arc::new(NullOcrEngine)
}
}
#[cfg(all(target_arch = "wasm32", feature = "image"))]
struct FallbackOcrEngine {
primary: Arc<dyn OcrEngine>,
secondary: Option<Arc<dyn OcrEngine>>,
}
#[cfg(all(target_arch = "wasm32", feature = "image"))]
impl OcrEngine for FallbackOcrEngine {
fn recognize(&self, gray: &GrayImage) -> Vec<OcrWord> {
let words = self.primary.recognize(gray);
if !words.is_empty() {
return words;
}
if let Some(sec) = &self.secondary {
return sec.recognize(gray);
}
Vec::new()
}
}
#[cfg(target_arch = "wasm32")]
struct NullOcrEngine;
#[cfg(target_arch = "wasm32")]
impl OcrEngine for NullOcrEngine {
fn recognize(&self, _gray: &GrayImage) -> Vec<OcrWord> {
Vec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct FakeOcrEngine;
impl OcrEngine for FakeOcrEngine {
fn recognize(&self, _gray: &GrayImage) -> Vec<OcrWord> {
vec![
OcrWord {
line_key: (1, 1, 1),
left: 10,
top: 10,
width: 80,
height: 14,
text: "Tube".into(),
confidence: 90.0,
},
OcrWord {
line_key: (1, 1, 1),
left: 145,
top: 10,
width: 110,
height: 14,
text: "Enzyme".into(),
confidence: 90.0,
},
OcrWord {
line_key: (1, 1, 2),
left: 10,
top: 42,
width: 80,
height: 14,
text: "1".into(),
confidence: 90.0,
},
OcrWord {
line_key: (1, 1, 2),
left: 145,
top: 42,
width: 110,
height: 14,
text: "BamHI".into(),
confidence: 90.0,
},
]
}
}
#[test]
fn fake_engine_returns_columnar_words() {
let img = GrayImage::new(400, 100);
let words = FakeOcrEngine.recognize(&img);
assert_eq!(words.len(), 4);
assert_eq!(words[0].text, "Tube");
assert_eq!(words[1].text, "Enzyme");
let lefts: Vec<_> = words.iter().map(|w| w.left).collect();
assert!(lefts.contains(&10));
assert!(lefts.contains(&145));
}
}