#[cfg(feature = "ocr-tesseract")]
use crate::graphics::ImageFormat;
#[cfg(feature = "ocr-tesseract")]
use crate::text::{
FragmentType, OcrEngine, OcrError, OcrOptions, OcrProcessingResult, OcrProvider, OcrResult,
OcrTextFragment,
};
#[cfg(feature = "ocr-tesseract")]
use rusty_tesseract::{image_to_data, Args, Image};
#[cfg(feature = "ocr-tesseract")]
use std::collections::HashMap;
#[cfg(feature = "ocr-tesseract")]
use std::time::Instant;
#[cfg(feature = "ocr-tesseract")]
#[derive(Debug, Clone)]
pub struct RustyTesseractConfig {
pub language: String,
pub psm: Option<u8>,
pub oem: Option<u8>,
pub dpi: Option<u32>,
pub config_variables: HashMap<String, String>,
}
#[cfg(feature = "ocr-tesseract")]
impl Default for RustyTesseractConfig {
fn default() -> Self {
let mut config_vars = HashMap::new();
config_vars.insert("preserve_interword_spaces".to_string(), "1".to_string());
Self {
language: "eng".to_string(),
psm: Some(3), oem: Some(3), dpi: Some(300), config_variables: config_vars,
}
}
}
#[cfg(feature = "ocr-tesseract")]
pub struct RustyTesseractProvider {
config: RustyTesseractConfig,
}
#[cfg(feature = "ocr-tesseract")]
impl RustyTesseractProvider {
pub fn new() -> Self {
Self::with_config(RustyTesseractConfig::default())
}
pub fn with_config(config: RustyTesseractConfig) -> Self {
Self { config }
}
pub fn for_contracts() -> Self {
let mut config_vars = HashMap::new();
config_vars.insert("preserve_interword_spaces".to_string(), "1".to_string());
config_vars.insert("tessedit_create_hocr".to_string(), "0".to_string());
config_vars.insert("load_system_dawg".to_string(), "1".to_string()); config_vars.insert("load_freq_dawg".to_string(), "1".to_string()); config_vars.insert("textord_debug_tabfind".to_string(), "0".to_string());
config_vars.insert("textord_use_cjk_fp_model".to_string(), "0".to_string());
let config = RustyTesseractConfig {
language: "eng".to_string(),
psm: Some(1), oem: Some(1), dpi: Some(300),
config_variables: config_vars,
};
Self { config }
}
pub fn for_large_documents() -> Self {
let mut config_vars = HashMap::new();
config_vars.insert("preserve_interword_spaces".to_string(), "1".to_string());
config_vars.insert("tessedit_create_hocr".to_string(), "0".to_string());
config_vars.insert("load_system_dawg".to_string(), "1".to_string());
config_vars.insert("load_freq_dawg".to_string(), "1".to_string());
config_vars.insert("classify_enable_learning".to_string(), "0".to_string());
config_vars.insert("tessedit_do_invert".to_string(), "0".to_string());
config_vars.insert("textord_debug_tabfind".to_string(), "0".to_string());
config_vars.insert("textord_use_cjk_fp_model".to_string(), "0".to_string());
let config = RustyTesseractConfig {
language: "eng".to_string(),
psm: Some(1), oem: Some(1), dpi: Some(150), config_variables: config_vars,
};
Self { config }
}
pub fn test_availability() -> OcrResult<bool> {
let _args = Args {
lang: "eng".to_string(),
config_variables: HashMap::new(),
dpi: Some(150),
psm: Some(6),
oem: Some(3),
};
Ok(true)
}
pub fn config(&self) -> &RustyTesseractConfig {
&self.config
}
fn create_args(&self, options: &OcrOptions) -> Args {
let mut config_vars = self.config.config_variables.clone();
if options.min_confidence > 0.0 {
config_vars.insert("tessedit_reject_mode".to_string(), "2".to_string());
}
Args {
lang: self.config.language.clone(),
config_variables: config_vars,
dpi: self.config.dpi.map(|v| v as i32),
psm: self.config.psm.map(|v| v as i32),
oem: self.config.oem.map(|v| v as i32),
}
}
}
#[cfg(feature = "ocr-tesseract")]
impl OcrProvider for RustyTesseractProvider {
fn coordinate_space(&self) -> crate::text::OcrCoordinateSpace {
crate::text::OcrCoordinateSpace::ImagePixels
}
fn supported_formats(&self) -> Vec<ImageFormat> {
vec![
ImageFormat::Png,
ImageFormat::Jpeg,
ImageFormat::Tiff,
]
}
fn engine_name(&self) -> &str {
"rusty-tesseract"
}
fn engine_type(&self) -> OcrEngine {
OcrEngine::Tesseract
}
fn process_image(
&self,
image_data: &[u8],
options: &OcrOptions,
) -> OcrResult<OcrProcessingResult> {
let start_time = Instant::now();
use std::io::Cursor;
let cursor = Cursor::new(image_data);
let dynamic_image = rusty_tesseract::image::ImageReader::new(cursor)
.with_guessed_format()
.map_err(|e| {
OcrError::ProcessingFailed(format!("Failed to guess image format: {}", e))
})?
.decode()
.map_err(|e| OcrError::ProcessingFailed(format!("Failed to decode image: {}", e)))?;
let image = Image::from_dynamic_image(&dynamic_image).map_err(|e| {
OcrError::ProcessingFailed(format!("Failed to create tesseract image: {}", e))
})?;
let args = self.create_args(options);
let output = image_to_data(&image, &args)
.map_err(|e| OcrError::ProcessingFailed(format!("OCR processing failed: {e}")))?;
let dimensions = (dynamic_image.width(), dynamic_image.height());
let mut fragments = Vec::new();
let mut text = String::new();
let mut previous_line = None;
for word in output
.data
.iter()
.filter(|word| word.level == 5 && !word.text.trim().is_empty())
{
if word.left < 0
|| word.top < 0
|| word.width <= 0
|| word.height <= 0
|| i64::from(word.left) + i64::from(word.width) > i64::from(dimensions.0)
|| i64::from(word.top) + i64::from(word.height) > i64::from(dimensions.1)
|| !word.conf.is_finite()
|| !(0.0..=100.0).contains(&word.conf)
{
return Err(OcrError::ProcessingFailed(
"Tesseract returned an invalid word region or confidence".into(),
));
}
let confidence = f64::from(word.conf) / 100.0;
if confidence < options.min_confidence {
continue;
}
let line = (word.page_num, word.block_num, word.par_num, word.line_num);
if let Some(previous) = previous_line {
text.push(if previous == line { ' ' } else { '\n' });
}
previous_line = Some(line);
text.push_str(&word.text);
fragments.push(OcrTextFragment {
text: word.text.clone(),
x: f64::from(word.left),
y: f64::from(dimensions.1) - f64::from(word.top) - f64::from(word.height),
width: f64::from(word.width),
height: f64::from(word.height),
font_size: f64::from(word.height),
confidence,
word_confidences: None,
fragment_type: FragmentType::Word,
});
}
let confidence = if fragments.is_empty() {
0.0
} else {
fragments
.iter()
.map(|fragment| fragment.confidence)
.sum::<f64>()
/ fragments.len() as f64
};
let processing_time = start_time.elapsed();
Ok(OcrProcessingResult {
text: text.trim().to_string(),
confidence,
fragments,
processing_time_ms: processing_time.as_millis() as u64,
engine_name: "rusty-tesseract".to_string(),
language: self.config.language.clone(),
processed_region: None,
image_dimensions: dimensions,
})
}
}
#[cfg(feature = "ocr-tesseract")]
impl Default for RustyTesseractProvider {
fn default() -> Self {
Self::new()
}
}
#[cfg(not(feature = "ocr-tesseract"))]
pub struct RustyTesseractProvider;
#[cfg(not(feature = "ocr-tesseract"))]
impl RustyTesseractProvider {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
Err("OCR feature not enabled. Compile with --features ocr-tesseract".into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "ocr-tesseract")]
#[test]
fn test_config_default() {
let config = RustyTesseractConfig::default();
assert_eq!(config.language, "eng");
assert_eq!(config.psm, Some(3)); assert_eq!(config.oem, Some(3));
assert_eq!(config.dpi, Some(300));
}
#[cfg(feature = "ocr-tesseract")]
#[test]
fn test_provider_creation() {
let _provider = RustyTesseractProvider::new();
}
#[cfg(feature = "ocr-tesseract")]
#[test]
fn test_engine_info() {
let provider = RustyTesseractProvider::new();
assert_eq!(provider.engine_type(), OcrEngine::Tesseract);
assert_eq!(provider.engine_name(), "rusty-tesseract");
}
}