use crate::Result;
use crate::core::config::OcrConfig;
use crate::plugins::Plugin;
use crate::types::ExtractionResult;
use async_trait::async_trait;
use std::path::Path;
use std::sync::Arc;
#[cfg(not(feature = "tokio-runtime"))]
use crate::KreuzbergError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OcrBackendType {
Tesseract,
EasyOCR,
PaddleOCR,
Custom,
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
pub trait OcrBackend: Plugin {
async fn process_image(&self, image_bytes: &[u8], config: &OcrConfig) -> Result<ExtractionResult>;
async fn process_file(&self, path: &Path, config: &OcrConfig) -> Result<ExtractionResult> {
#[cfg(feature = "tokio-runtime")]
{
use crate::core::io;
let bytes = io::read_file_async(path).await?;
self.process_image(&bytes, config).await
}
#[cfg(not(feature = "tokio-runtime"))]
{
let _ = (path, config);
Err(KreuzbergError::Other(
"File-based OCR processing requires the tokio-runtime feature".to_string(),
))
}
}
fn supports_language(&self, lang: &str) -> bool;
fn backend_type(&self) -> OcrBackendType;
fn supported_languages(&self) -> Vec<String> {
vec![]
}
fn supports_table_detection(&self) -> bool {
false
}
}
pub fn register_ocr_backend(backend: Arc<dyn OcrBackend>) -> crate::Result<()> {
use crate::plugins::registry::get_ocr_backend_registry;
let registry = get_ocr_backend_registry();
let mut registry = registry
.write()
.expect("OCR backend registry lock poisoned - critical runtime error");
registry.register(backend)
}
pub fn unregister_ocr_backend(name: &str) -> crate::Result<()> {
use crate::plugins::registry::get_ocr_backend_registry;
let registry = get_ocr_backend_registry();
let mut registry = registry
.write()
.expect("OCR backend registry lock poisoned - critical runtime error");
registry.remove(name)
}
pub fn list_ocr_backends() -> crate::Result<Vec<String>> {
use crate::plugins::registry::get_ocr_backend_registry;
let registry = get_ocr_backend_registry();
let registry = registry
.read()
.expect("OCR backend registry lock poisoned - critical runtime error");
Ok(registry.list())
}
pub fn clear_ocr_backends() -> crate::Result<()> {
use crate::plugins::registry::get_ocr_backend_registry;
let registry = get_ocr_backend_registry();
let mut registry = registry
.write()
.expect("OCR backend registry lock poisoned - critical runtime error");
registry.shutdown_all()
}
#[cfg(test)]
mod tests {
use super::*;
use std::borrow::Cow;
struct MockOcrBackend {
languages: Vec<String>,
}
impl Plugin for MockOcrBackend {
fn name(&self) -> &str {
"mock-ocr"
}
fn version(&self) -> String {
"1.0.0".to_string()
}
fn initialize(&self) -> Result<()> {
Ok(())
}
fn shutdown(&self) -> Result<()> {
Ok(())
}
}
#[async_trait]
impl OcrBackend for MockOcrBackend {
async fn process_image(&self, _image_bytes: &[u8], _config: &OcrConfig) -> Result<ExtractionResult> {
Ok(ExtractionResult {
content: "Mocked OCR text".to_string(),
mime_type: Cow::Borrowed("text/plain"),
metadata: crate::types::Metadata::default(),
tables: vec![],
detected_languages: None,
chunks: None,
images: None,
djot_content: None,
pages: None,
elements: None,
ocr_elements: None,
document: None,
#[cfg(any(feature = "keywords-yake", feature = "keywords-rake"))]
extracted_keywords: None,
quality_score: None,
processing_warnings: Vec::new(),
annotations: None,
})
}
fn supports_language(&self, lang: &str) -> bool {
self.languages.iter().any(|l| l == lang)
}
fn backend_type(&self) -> OcrBackendType {
OcrBackendType::Custom
}
fn supported_languages(&self) -> Vec<String> {
self.languages.clone()
}
}
#[tokio::test]
async fn test_ocr_backend_process_image() {
let backend = MockOcrBackend {
languages: vec!["eng".to_string(), "deu".to_string()],
};
let config = OcrConfig {
backend: "mock".to_string(),
language: "eng".to_string(),
..Default::default()
};
let result = backend.process_image(b"fake image data", &config).await.unwrap();
assert_eq!(result.content, "Mocked OCR text");
assert_eq!(result.mime_type, "text/plain");
}
#[test]
fn test_ocr_backend_supports_language() {
let backend = MockOcrBackend {
languages: vec!["eng".to_string(), "deu".to_string()],
};
assert!(backend.supports_language("eng"));
assert!(backend.supports_language("deu"));
assert!(!backend.supports_language("fra"));
}
#[test]
fn test_ocr_backend_type() {
let backend = MockOcrBackend {
languages: vec!["eng".to_string()],
};
assert_eq!(backend.backend_type(), OcrBackendType::Custom);
}
#[test]
fn test_ocr_backend_supported_languages() {
let backend = MockOcrBackend {
languages: vec!["eng".to_string(), "deu".to_string(), "fra".to_string()],
};
let supported = backend.supported_languages();
assert_eq!(supported.len(), 3);
assert!(supported.contains(&"eng".to_string()));
assert!(supported.contains(&"deu".to_string()));
assert!(supported.contains(&"fra".to_string()));
}
#[test]
fn test_ocr_backend_type_variants() {
assert_eq!(OcrBackendType::Tesseract, OcrBackendType::Tesseract);
assert_ne!(OcrBackendType::Tesseract, OcrBackendType::EasyOCR);
assert_ne!(OcrBackendType::EasyOCR, OcrBackendType::PaddleOCR);
assert_ne!(OcrBackendType::PaddleOCR, OcrBackendType::Custom);
}
#[test]
fn test_ocr_backend_type_debug() {
let backend_type = OcrBackendType::Tesseract;
let debug_str = format!("{:?}", backend_type);
assert!(debug_str.contains("Tesseract"));
}
#[test]
fn test_ocr_backend_type_clone() {
let backend_type = OcrBackendType::EasyOCR;
let cloned = backend_type;
assert_eq!(backend_type, cloned);
}
#[test]
fn test_ocr_backend_default_table_detection() {
let backend = MockOcrBackend {
languages: vec!["eng".to_string()],
};
assert!(!backend.supports_table_detection());
}
#[tokio::test]
async fn test_ocr_backend_process_file_default_impl() {
use std::io::Write;
use tempfile::NamedTempFile;
let backend = MockOcrBackend {
languages: vec!["eng".to_string()],
};
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(b"fake image data").unwrap();
let path = temp_file.path();
let config = OcrConfig {
backend: "mock".to_string(),
language: "eng".to_string(),
..Default::default()
};
let result = backend.process_file(path, &config).await.unwrap();
assert_eq!(result.content, "Mocked OCR text");
}
#[test]
fn test_ocr_backend_plugin_interface() {
let backend = MockOcrBackend {
languages: vec!["eng".to_string()],
};
assert_eq!(backend.name(), "mock-ocr");
assert_eq!(backend.version(), "1.0.0");
assert!(backend.initialize().is_ok());
assert!(backend.shutdown().is_ok());
}
#[test]
fn test_ocr_backend_empty_languages() {
let backend = MockOcrBackend { languages: vec![] };
let supported = backend.supported_languages();
assert_eq!(supported.len(), 0);
assert!(!backend.supports_language("eng"));
}
#[tokio::test]
async fn test_ocr_backend_with_empty_image() {
let backend = MockOcrBackend {
languages: vec!["eng".to_string()],
};
let config = OcrConfig {
backend: "mock".to_string(),
language: "eng".to_string(),
..Default::default()
};
let result = backend.process_image(b"", &config).await;
assert!(result.is_ok());
}
}