use anyhow::Result;
pub struct GrayImage {
pub data: Vec<u8>,
pub width: u32,
pub height: u32,
}
impl GrayImage {
pub fn new(width: u32, height: u32, data: Vec<u8>) -> Self {
Self { data, width, height }
}
}
#[derive(Debug, Clone, Copy)]
pub enum OcrHint {
SingleChar,
SingleWord,
SingleLine,
Block,
}
pub trait OcrEngine {
fn recognize(&self, img: &GrayImage, hint: OcrHint) -> Result<String>;
fn recognize_scored(&self, img: &GrayImage, hint: OcrHint) -> Result<Vec<(String, f32)>> {
Ok(self.recognize(img, hint)?.split_whitespace().map(|w| (w.to_string(), 100.0)).collect())
}
}
pub struct MockOcr(pub String);
impl OcrEngine for MockOcr {
fn recognize(&self, _img: &GrayImage, _hint: OcrHint) -> Result<String> {
Ok(self.0.clone())
}
}
#[cfg(feature = "ocr-tesseract")]
pub use tesseract_backend::TesseractOcr;
#[cfg(feature = "ocr-tesseract")]
mod tesseract_backend {
use std::path::PathBuf;
use anyhow::{anyhow, Result};
use tesseract5_rs::{Ocr5Engine, OcrOptions};
use super::{GrayImage, OcrEngine, OcrHint};
fn psm_for(hint: OcrHint) -> u8 {
match hint {
OcrHint::SingleChar => 10,
OcrHint::SingleWord => 8,
OcrHint::SingleLine => 7,
OcrHint::Block => 6,
}
}
pub struct TesseractOcr {
engine: Ocr5Engine,
}
impl TesseractOcr {
pub fn new(tessdata: Option<PathBuf>, lang: &str, hint: OcrHint) -> Result<Self> {
let engine = Ocr5Engine::new(OcrOptions {
lang: lang.to_string(),
psm: Some(psm_for(hint)),
tessdata_dir: tessdata,
with_hierarchy: true, })
.map_err(|e| anyhow!("tesseract5 init: {e}"))?;
Ok(Self { engine })
}
pub fn for_glyph(lang: &str) -> Result<Self> {
Self::new(None, lang, OcrHint::SingleChar)
}
}
impl OcrEngine for TesseractOcr {
fn recognize(&self, img: &GrayImage, _hint: OcrHint) -> Result<String> {
let out = self
.engine
.recognize(&img.data, img.width as i32, img.height as i32, 1, img.width as i32)
.map_err(|e| anyhow!("ocr recognize: {e}"))?;
Ok(out.text.trim().to_string())
}
fn recognize_scored(&self, img: &GrayImage, _hint: OcrHint) -> Result<Vec<(String, f32)>> {
let out = self
.engine
.recognize(&img.data, img.width as i32, img.height as i32, 1, img.width as i32)
.map_err(|e| anyhow!("ocr recognize: {e}"))?;
let mut words = Vec::new();
if let Some(h) = out.hierarchy {
for block in h.blocks {
for para in block.paragraphs {
for line in para.lines {
for word in line.words {
words.push((word.text.trim().to_string(), word.confidence));
}
}
}
}
}
Ok(words)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mock_ocr_returns_scripted() {
let ocr = MockOcr("hello".into());
let img = GrayImage::new(1, 1, vec![255]);
assert_eq!(ocr.recognize(&img, OcrHint::SingleWord).unwrap(), "hello");
}
#[cfg(feature = "ocr-tesseract")]
#[test]
fn tesseract_recognizes_rendered_text() {
use std::path::PathBuf;
fn find_tessdata() -> Option<PathBuf> {
let mut cands: Vec<PathBuf> = Vec::new();
if let Ok(p) = std::env::var("TESSDATA_PREFIX") {
cands.push(PathBuf::from(p));
}
if let Ok(appdata) = std::env::var("APPDATA") {
cands.push(PathBuf::from(&appdata).join("tesseract-rs/aarch64/dynamic/tessdata"));
cands.push(PathBuf::from(&appdata).join("tesseract-rs/tessdata"));
}
cands.push(PathBuf::from("C:/Program Files/Tesseract-OCR/tessdata"));
cands.into_iter().find(|d| d.join("eng.traineddata").is_file())
}
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let (gray, dim) = (dir.join("ocrtest.gray"), dir.join("ocrtest.dim"));
if !gray.is_file() || !dim.is_file() {
eprintln!("skip: immagine di test assente");
return;
}
let Some(tessdata) = find_tessdata() else {
eprintln!("skip: eng.traineddata non trovato");
return;
};
let d = std::fs::read_to_string(&dim).unwrap();
let mut it = d.split_whitespace();
let w: u32 = it.next().unwrap().parse().unwrap();
let h: u32 = it.next().unwrap().parse().unwrap();
let img = GrayImage::new(w, h, std::fs::read(&gray).unwrap());
let ocr = TesseractOcr::new(Some(tessdata), "eng", OcrHint::SingleLine).expect("init tesseract");
let text = ocr.recognize(&img, OcrHint::SingleLine).expect("ocr");
eprintln!("OCR result: {text:?}");
assert!(text.to_uppercase().contains("HELLO"), "atteso HELLO, ottenuto {text:?}");
}
}