#[derive(Debug, Clone)]
pub struct OcrWord {
pub text: String,
pub bbox_px: [u32; 4],
pub confidence: f32,
}
#[derive(Debug, Clone)]
pub struct OcrPageResult {
pub words: Vec<OcrWord>,
pub confidence: f32,
pub image_width: u32,
pub image_height: u32,
}
impl OcrPageResult {
pub fn full_text(&self) -> String {
self.words
.iter()
.map(|w| w.text.as_str())
.collect::<Vec<_>>()
.join(" ")
}
}
pub trait OcrEngine: Send + Sync {
fn recognize(
&self,
image_data: &[u8],
width: u32,
height: u32,
dpi: u32,
) -> std::result::Result<OcrPageResult, String>;
fn supported_languages(&self) -> Vec<String>;
}
#[derive(Debug, Default)]
pub struct NoOpEngine;
impl OcrEngine for NoOpEngine {
fn recognize(
&self,
_image_data: &[u8],
width: u32,
height: u32,
_dpi: u32,
) -> std::result::Result<OcrPageResult, String> {
Ok(OcrPageResult {
words: Vec::new(),
confidence: 0.0,
image_width: width,
image_height: height,
})
}
fn supported_languages(&self) -> Vec<String> {
Vec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn noop_engine_returns_empty() {
let engine = NoOpEngine;
let result = engine.recognize(&[], 100, 100, 300).unwrap();
assert!(result.words.is_empty());
assert_eq!(result.confidence, 0.0);
assert_eq!(result.image_width, 100);
assert_eq!(result.image_height, 100);
assert!(engine.supported_languages().is_empty());
}
#[test]
fn ocr_page_result_full_text() {
let result = OcrPageResult {
words: vec![
OcrWord {
text: "Hello".to_string(),
bbox_px: [0, 0, 50, 20],
confidence: 0.95,
},
OcrWord {
text: "World".to_string(),
bbox_px: [60, 0, 110, 20],
confidence: 0.90,
},
],
confidence: 0.92,
image_width: 200,
image_height: 100,
};
assert_eq!(result.full_text(), "Hello World");
}
}