edgeparse_core/pdf/ocr/
mod.rs1#[cfg(all(target_arch = "wasm32", feature = "image"))]
11mod host_engine;
12#[cfg(feature = "ocr-ocrs")]
13mod ocrs_engine;
14#[cfg(not(target_arch = "wasm32"))]
15mod tesseract_cli;
16
17#[cfg(all(target_arch = "wasm32", feature = "image"))]
18pub use host_engine::{host_ocr_registered, register_host_ocr_backend, HostOcrEngine};
19#[cfg(feature = "ocr-ocrs")]
20pub use ocrs_engine::OcrsEngine;
21#[cfg(not(target_arch = "wasm32"))]
22pub(crate) use tesseract_cli::parse_tesseract_tsv;
23#[cfg(not(target_arch = "wasm32"))]
24pub use tesseract_cli::TesseractCliEngine;
25
26use image::GrayImage;
27use std::sync::Arc;
28
29#[derive(Debug, Clone)]
31pub struct OcrWord {
32 pub line_key: (u32, u32, u32),
34 pub left: u32,
36 pub top: u32,
38 pub width: u32,
40 pub height: u32,
42 pub text: String,
44 pub confidence: f64,
46}
47
48pub trait OcrEngine: Send + Sync {
50 fn recognize(&self, gray: &GrayImage) -> Vec<OcrWord>;
52
53 fn recognize_psm(&self, gray: &GrayImage, _psm: &str) -> Vec<OcrWord> {
55 self.recognize(gray)
56 }
57}
58
59pub fn default_engine() -> Arc<dyn OcrEngine> {
64 #[cfg(all(target_arch = "wasm32", feature = "image"))]
65 {
66 if HostOcrEngine::try_new().is_some() {
67 #[cfg(feature = "ocr-ocrs")]
68 let secondary: Option<Arc<dyn OcrEngine>> =
69 OcrsEngine::try_from_env().map(|e| Arc::new(e) as Arc<dyn OcrEngine>);
70 #[cfg(not(feature = "ocr-ocrs"))]
71 let secondary: Option<Arc<dyn OcrEngine>> = None;
72 return Arc::new(FallbackOcrEngine {
73 primary: Arc::new(HostOcrEngine),
74 secondary,
75 });
76 }
77 }
78 #[cfg(feature = "ocr-ocrs")]
79 {
80 if let Some(engine) = OcrsEngine::try_from_env() {
81 return Arc::new(engine);
82 }
83 }
84 #[cfg(not(target_arch = "wasm32"))]
85 {
86 Arc::new(TesseractCliEngine::default())
87 }
88 #[cfg(target_arch = "wasm32")]
89 {
90 Arc::new(NullOcrEngine)
91 }
92}
93
94#[cfg(all(target_arch = "wasm32", feature = "image"))]
96struct FallbackOcrEngine {
97 primary: Arc<dyn OcrEngine>,
98 secondary: Option<Arc<dyn OcrEngine>>,
99}
100
101#[cfg(all(target_arch = "wasm32", feature = "image"))]
102impl OcrEngine for FallbackOcrEngine {
103 fn recognize(&self, gray: &GrayImage) -> Vec<OcrWord> {
104 let words = self.primary.recognize(gray);
105 if !words.is_empty() {
106 return words;
107 }
108 if let Some(sec) = &self.secondary {
109 return sec.recognize(gray);
110 }
111 Vec::new()
112 }
113}
114
115#[cfg(target_arch = "wasm32")]
116struct NullOcrEngine;
117
118#[cfg(target_arch = "wasm32")]
119impl OcrEngine for NullOcrEngine {
120 fn recognize(&self, _gray: &GrayImage) -> Vec<OcrWord> {
121 Vec::new()
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 struct FakeOcrEngine;
131
132 impl OcrEngine for FakeOcrEngine {
133 fn recognize(&self, _gray: &GrayImage) -> Vec<OcrWord> {
134 vec![
135 OcrWord {
136 line_key: (1, 1, 1),
137 left: 10,
138 top: 10,
139 width: 80,
140 height: 14,
141 text: "Tube".into(),
142 confidence: 90.0,
143 },
144 OcrWord {
145 line_key: (1, 1, 1),
146 left: 145,
147 top: 10,
148 width: 110,
149 height: 14,
150 text: "Enzyme".into(),
151 confidence: 90.0,
152 },
153 OcrWord {
154 line_key: (1, 1, 2),
155 left: 10,
156 top: 42,
157 width: 80,
158 height: 14,
159 text: "1".into(),
160 confidence: 90.0,
161 },
162 OcrWord {
163 line_key: (1, 1, 2),
164 left: 145,
165 top: 42,
166 width: 110,
167 height: 14,
168 text: "BamHI".into(),
169 confidence: 90.0,
170 },
171 ]
172 }
173 }
174
175 #[test]
176 fn fake_engine_returns_columnar_words() {
177 let img = GrayImage::new(400, 100);
178 let words = FakeOcrEngine.recognize(&img);
179 assert_eq!(words.len(), 4);
180 assert_eq!(words[0].text, "Tube");
181 assert_eq!(words[1].text, "Enzyme");
182 let lefts: Vec<_> = words.iter().map(|w| w.left).collect();
183 assert!(lefts.contains(&10));
184 assert!(lefts.contains(&145));
185 }
186}