#[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>;
}
#[doc(hidden)]
pub fn rgb_pixels(
image_data: &[u8],
width: u32,
height: u32,
) -> std::result::Result<Vec<u8>, String> {
let pixels = width as usize * height as usize;
if image_data.len() == pixels * 3 {
Ok(image_data.to_vec())
} else if image_data.len() == pixels * 4 {
Ok(image_data
.chunks_exact(4)
.flat_map(|px| px[..3].to_vec())
.collect())
} else {
Err(format!(
"unexpected image size: {} bytes for {}x{} (expected {} RGB or {} RGBA)",
image_data.len(),
width,
height,
pixels * 3,
pixels * 4,
))
}
}
#[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");
}
#[test]
fn rgb_pixels_keeps_rgb_drops_alpha_and_rejects_the_rest() {
assert_eq!(
rgb_pixels(&[1, 2, 3, 4, 5, 6], 2, 1).unwrap(),
vec![1, 2, 3, 4, 5, 6]
);
assert_eq!(
rgb_pixels(&[1, 2, 3, 255, 4, 5, 6, 0], 2, 1).unwrap(),
vec![1, 2, 3, 4, 5, 6]
);
assert!(rgb_pixels(&[1, 2, 3, 4, 5], 2, 1).is_err());
}
}