1use std::path::PathBuf;
11
12use image::{imageops, RgbImage};
13use ppocr_rs::{
14 DocOrientation, DocOrientationClassifier, FormulaRecognizer,
15 LayoutAnalyzer, LayoutBox, ModelHub, OcrLite, OcrOptions, Point,
16 PpOcrVersion, PpStructureModel, SemanticClass,
17 TableCellBox, TableStructureRecognizer, TextBlockWithLayout,
18};
19
20use crate::engine::{OcrBlock, OcrFormula, OcrLayoutBox, OcrPageResult, OcrTable, OcrWord};
21use crate::error::{Error, Result};
22
23use super::OcrEngine;
24
25pub struct OcrModelPaths {
29 pub det: PathBuf,
30 pub rec: PathBuf,
31 pub dict: PathBuf,
32}
33
34pub struct TableModelPaths {
36 pub structure_onnx: PathBuf,
38 pub structure_dict: PathBuf,
40 pub input_size: Option<u32>,
43}
44
45pub struct PpOcrEngineConfig {
47 pub tier: PpOcrVersion,
49 pub ori_model: Option<PathBuf>,
51 pub ocr_models: Option<OcrModelPaths>,
53 pub num_threads: usize,
55 pub table_models: Option<TableModelPaths>,
57 pub enable_tables: bool,
60 pub enable_formula_decoder: bool,
64}
65
66impl Default for PpOcrEngineConfig {
67 fn default() -> Self {
68 Self {
69 tier: PpOcrVersion::V6Tiny,
70 ori_model: None,
71 ocr_models: None,
72 num_threads: 4,
73 table_models: None,
74 enable_tables: true,
75 enable_formula_decoder: false,
76 }
77 }
78}
79
80pub struct PpOcrEngine {
83 ocr: OcrLite,
84 ori: Option<DocOrientationClassifier>,
85 table_rec: Option<TableStructureRecognizer>,
86 formula_rec: Option<FormulaRecognizer>,
87}
88
89impl PpOcrEngine {
90 pub fn new(cfg: PpOcrEngineConfig) -> Result<Self> {
96 let hub = ModelHub::with_default_cache()?;
97
98 let (det, rec, dict) = if let Some(p) = cfg.ocr_models {
100 (p.det, p.rec, p.dict)
101 } else {
102 let paths = hub.ensure(cfg.tier)?;
103 (paths.det_onnx, paths.rec_onnx, paths.dict_txt)
104 };
105
106 let mut ocr = OcrLite::new();
107 ocr.init_models_no_angle(
108 det.to_str().ok_or_else(|| Error::Other("det path non UTF-8".into()))?,
109 rec.to_str().ok_or_else(|| Error::Other("rec path non UTF-8".into()))?,
110 dict.to_str().ok_or_else(|| Error::Other("dict path non UTF-8".into()))?,
111 cfg.num_threads,
112 )?;
113
114 let ori = if let Some(ori_path) = cfg.ori_model {
116 match DocOrientationClassifier::from_path(&ori_path) {
117 Ok(clf) => Some(clf),
118 Err(e) => {
119 eprintln!("[df-ocr-switcher] orientamento disabilitato: {e}");
120 None
121 }
122 }
123 } else {
124 match hub.ensure_single(PpStructureModel::DocOrientation) {
125 Ok(sp) => match DocOrientationClassifier::from_path(&sp.onnx) {
126 Ok(clf) => Some(clf),
127 Err(e) => {
128 eprintln!("[df-ocr-switcher] orientamento disabilitato: {e}");
129 None
130 }
131 },
132 Err(_) => None, }
134 };
135
136 let table_rec = if cfg.enable_tables {
138 match load_table_recognizer(&hub, cfg.table_models) {
139 Ok(rec) => Some(rec),
140 Err(e) => {
141 eprintln!("[df-ocr-switcher] table recognition disabilitato: {e}");
142 None
143 }
144 }
145 } else {
146 None
147 };
148
149 let formula_rec = match load_formula_recognizer(&hub, cfg.enable_formula_decoder) {
151 Ok(mut rec) => {
152 rec.decoder_enabled = cfg.enable_formula_decoder;
153 Some(rec)
154 }
155 Err(e) => {
156 if cfg.enable_formula_decoder {
157 eprintln!("[df-ocr-switcher] formula recognition disabilitato: {e}");
158 }
159 None
160 }
161 };
162
163 Ok(Self { ocr, ori, table_rec, formula_rec })
164 }
165}
166
167impl OcrEngine for PpOcrEngine {
168 fn process_page(&mut self, img: &RgbImage, layout: &mut LayoutAnalyzer) -> Result<OcrPageResult> {
169 let (page_angle, upright) = if let Some(clf) = &self.ori {
171 let (orient, _conf) = clf.classify(img)?;
172 let degrees = orient.degrees();
173 let rotated = rotate_upright(img, orient);
174 (degrees, rotated)
175 } else {
176 (0u32, img.clone())
177 };
178 let (w, h) = upright.dimensions();
179
180 let opts = OcrOptions {
182 return_word_box: true,
183 use_doc_orientation: false,
184 ..OcrOptions::default()
185 };
186 let result = self.ocr.detect_with_layout(
187 &upright, layout,
188 10, 960, 0.6, 0.3, 1.6,
189 false, false,
190 opts,
191 )?;
192
193 let tables = if let Some(rec) = &self.table_rec {
195 extract_tables(&upright, &result.layout_boxes, &result.blocks, rec)
196 } else {
197 vec![]
198 };
199
200 let formulas = if let Some(rec) = &self.formula_rec {
202 extract_formulas(&upright, &result.layout_boxes, rec)
203 } else {
204 vec![]
205 };
206
207 let layout_boxes = result.layout_boxes.iter().map(lb_to_ocr).collect();
209 let (blocks, words) = blocks_and_words(&result.blocks);
210
211 Ok(OcrPageResult {
212 page_angle, page_width: w, page_height: h,
213 layout_boxes, blocks, words, tables, formulas,
214 })
215 }
216}
217
218fn extract_tables(
221 img: &RgbImage,
222 lbs: &[LayoutBox],
223 blocks: &[TextBlockWithLayout],
224 rec: &TableStructureRecognizer,
225) -> Vec<OcrTable> {
226 lbs.iter()
227 .enumerate()
228 .filter(|(_, lb)| lb.class.semantic() == SemanticClass::Table)
229 .filter_map(|(i, lb)| {
230 let crop = crop_lb(img, lb);
231 match rec.recognize(&crop) {
232 Ok(structure) => {
233 let gfm = table_to_gfm(&structure.cell_boxes, blocks, lb);
234 Some(OcrTable { layout_idx: i as i32, gfm })
235 }
236 Err(e) => {
237 eprintln!("[df-ocr-switcher] SLANeXt errore su tabella {i}: {e}");
238 None
239 }
240 }
241 })
242 .collect()
243}
244
245fn table_to_gfm(
250 cells: &[TableCellBox],
251 blocks: &[TextBlockWithLayout],
252 lb: &LayoutBox,
253) -> String {
254 if cells.is_empty() {
255 return String::new();
256 }
257
258 let mut indexed: Vec<(usize, f32, f32)> = cells.iter()
260 .enumerate()
261 .map(|(i, c)| (i, (c.x1 + c.x2) * 0.5, (c.y1 + c.y2) * 0.5))
262 .collect();
263 indexed.sort_by(|a, b| a.2.partial_cmp(&b.2)
264 .unwrap_or(std::cmp::Ordering::Equal)
265 .then(a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)));
266
267 let first_y = indexed.first().map(|e| e.2).unwrap_or(0.0);
269 let n_cols = indexed.iter().filter(|e| (e.2 - first_y).abs() < 20.0).count().max(1);
270
271 let mut md = String::new();
272 for (row_i, chunk) in indexed.chunks(n_cols).enumerate() {
273 let mut row = chunk.to_vec();
275 row.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
276
277 md.push('|');
278 for &(ci, _, _) in &row {
279 let text = collect_cell_text(blocks, &cells[ci], lb.x, lb.y);
280 md.push_str(&format!(" {} |", text.replace('|', "\\|")));
281 }
282 md.push('\n');
283
284 if row_i == 0 {
285 md.push('|');
286 for _ in 0..row.len() { md.push_str("---|"); }
287 md.push('\n');
288 }
289 }
290 md
291}
292
293fn collect_cell_text(
296 blocks: &[TextBlockWithLayout],
297 cell: &TableCellBox,
298 lb_x: u32,
299 lb_y: u32,
300) -> String {
301 let cx1 = lb_x as f32 + cell.x1;
302 let cy1 = lb_y as f32 + cell.y1;
303 let cx2 = lb_x as f32 + cell.x2;
304 let cy2 = lb_y as f32 + cell.y2;
305
306 let mut parts: Vec<(u32, &str)> = blocks.iter()
307 .filter(|b| {
308 let bx = b.centroid_x as f32;
309 let by = b.centroid_y as f32;
310 bx >= cx1 && bx <= cx2 && by >= cy1 && by <= cy2
311 })
312 .map(|b| (b.centroid_y, b.block.text.trim()))
313 .collect();
314 parts.sort_by_key(|(y, _)| *y);
315 parts.iter()
316 .filter(|(_, t)| !t.is_empty())
317 .map(|(_, t)| *t)
318 .collect::<Vec<_>>()
319 .join(" ")
320}
321
322fn extract_formulas(
325 img: &RgbImage,
326 lbs: &[LayoutBox],
327 rec: &FormulaRecognizer,
328) -> Vec<OcrFormula> {
329 lbs.iter()
330 .enumerate()
331 .filter(|(_, lb)| lb.class.semantic() == SemanticClass::Equation)
332 .filter_map(|(i, lb)| {
333 let crop = crop_lb(img, lb);
334 match rec.recognize(&crop) {
335 Ok(fr) => Some(OcrFormula { layout_idx: i as i32, latex: fr.latex }),
336 Err(e) => {
337 eprintln!("[df-ocr-switcher] formula rec errore su regione {i}: {e}");
338 None
339 }
340 }
341 })
342 .collect()
343}
344
345fn load_table_recognizer(
348 hub: &ModelHub,
349 paths: Option<TableModelPaths>,
350) -> std::result::Result<TableStructureRecognizer, Box<dyn std::error::Error>> {
351 let (onnx, dict, input_size) = if let Some(p) = paths {
352 (p.structure_onnx, p.structure_dict, p.input_size)
353 } else {
354 let sp = hub.ensure_single(PpStructureModel::TableStructureWired)?;
355 let onnx = sp.onnx;
356 let dict = sp.dict_txt.ok_or("SLANeXt: dict_txt mancante")?;
357 (onnx, dict, None)
358 };
359 let rec = TableStructureRecognizer::from_path_with_dict(&onnx, Some(&dict))?;
360 Ok(if let Some(sz) = input_size { rec.with_input_size(sz) } else { rec })
361}
362
363fn load_formula_recognizer(
364 hub: &ModelHub,
365 enabled: bool,
366) -> std::result::Result<FormulaRecognizer, Box<dyn std::error::Error>> {
367 if !enabled {
368 let sp = hub.ensure_single(PpStructureModel::FormulaRec)?;
371 let tok = sp.tokenizer_json.as_deref();
372 Ok(FormulaRecognizer::from_paths(&sp.onnx, tok)?)
373 } else {
374 let sp = hub.ensure_single(PpStructureModel::FormulaRec)?;
375 let tok = sp.tokenizer_json.as_deref();
376 Ok(FormulaRecognizer::from_paths(&sp.onnx, tok)?)
377 }
378}
379
380fn crop_lb(img: &RgbImage, lb: &LayoutBox) -> RgbImage {
384 let x = lb.x.min(img.width().saturating_sub(1));
385 let y = lb.y.min(img.height().saturating_sub(1));
386 let w = lb.w.min(img.width().saturating_sub(x));
387 let h = lb.h.min(img.height().saturating_sub(y));
388 imageops::crop_imm(img, x, y, w, h).to_image()
389}
390
391fn rotate_upright(img: &RgbImage, orient: DocOrientation) -> RgbImage {
392 match orient {
393 DocOrientation::Deg0 => img.clone(),
394 DocOrientation::Deg90 => imageops::rotate270(img),
395 DocOrientation::Deg180 => imageops::rotate180(img),
396 DocOrientation::Deg270 => imageops::rotate90(img),
397 }
398}
399
400fn aabb(pts: &[Point]) -> (u32, u32, u32, u32) {
401 let x1 = pts.iter().map(|p| p.x).min().unwrap_or(0);
402 let y1 = pts.iter().map(|p| p.y).min().unwrap_or(0);
403 let x2 = pts.iter().map(|p| p.x).max().unwrap_or(0);
404 let y2 = pts.iter().map(|p| p.y).max().unwrap_or(0);
405 (x1, y1, x2, y2)
406}
407
408fn lb_to_ocr(lb: &LayoutBox) -> OcrLayoutBox {
409 OcrLayoutBox {
410 class_name: format!("{:?}", lb.class),
411 semantic: lb.class.semantic(),
412 x1: lb.xmin(), y1: lb.ymin(),
413 x2: lb.xmax(), y2: lb.ymax(),
414 reading_order: lb.reading_order,
415 }
416}
417
418fn blocks_and_words(
419 src: &[TextBlockWithLayout],
420) -> (Vec<OcrBlock>, Vec<OcrWord>) {
421 let mut blocks = Vec::with_capacity(src.len());
422 let mut words = Vec::new();
423
424 for blk in src {
425 let layout_idx = blk.layout_index.map(|i| i as i32).unwrap_or(-1);
426 let (x1, y1, x2, y2) = aabb(&blk.block.box_points);
427
428 blocks.push(OcrBlock {
429 text: blk.block.text.clone(),
430 x1, y1, x2, y2,
431 confidence: blk.block.text_score,
432 layout_idx,
433 });
434
435 if blk.block.words.is_empty() {
436 words.push(OcrWord {
437 text: blk.block.text.clone(),
438 x1, y1, x2, y2,
439 confidence: blk.block.text_score,
440 layout_idx,
441 });
442 } else {
443 for w in &blk.block.words {
444 let (wx1, wy1, wx2, wy2) = aabb(&w.box_points);
445 words.push(OcrWord {
446 text: w.text.clone(),
447 x1: wx1, y1: wy1, x2: wx2, y2: wy2,
448 confidence: w.score,
449 layout_idx,
450 });
451 }
452 }
453 }
454
455 (blocks, words)
456}