Skip to main content

df_ocr_switcher/output/
markdown.rs

1//! Generatore Markdown da `OcrPageResult`.
2//!
3//! Strategia:
4//! - Per ogni `layout_box` in `reading_order`, raccoglie i `blocks` associati
5//!   (`layout_idx` corrispondente) e li concatena.
6//! - La formattazione dipende da `SemanticClass`:
7//!   - `Title`    → `# testo`
8//!   - `Text`     → paragrafo
9//!   - `List`     → paragrafo (le righe sono già in `text`)
10//!   - `Table`    → GFM table da `result.tables` se disponibile, altrimenti testo OCR
11//!   - `Equation` → `$$latex$$` da `result.formulas` se disponibile, altrimenti testo OCR
12//!   - `Figure`   → `![figure]()`
13//!   - `Header`/`Footer` → skip (non contribuiscono al testo principale)
14//! - I blocchi "orfani" (layout_idx == -1) sono emessi per ultimi come testo
15//!   semplice, ordinati per Y.
16
17use ppocr_rs::SemanticClass;
18
19use crate::engine::{OcrBlock, OcrFormula, OcrPageResult, OcrTable};
20
21/// Converte una singola pagina OCR in stringa Markdown.
22pub fn page_to_markdown(result: &OcrPageResult) -> String {
23    let mut out = String::new();
24
25    if result.layout_boxes.is_empty() {
26        // Nessun layout: emetti tutto il testo in ordine di blocchi
27        for blk in &result.blocks {
28            push_text(&mut out, blk.text.trim());
29        }
30        return out;
31    }
32
33    // ── Layout-aware output ────────────────────────────────────────────────
34    // Costruiamo una mappa layout_idx → blocchi ordinati per Y
35    let mut by_layout: Vec<Vec<&OcrBlock>> = vec![Vec::new(); result.layout_boxes.len()];
36    let mut orphans: Vec<&OcrBlock> = Vec::new();
37
38    for blk in &result.blocks {
39        if blk.layout_idx >= 0 && (blk.layout_idx as usize) < by_layout.len() {
40            by_layout[blk.layout_idx as usize].push(blk);
41        } else {
42            orphans.push(blk);
43        }
44    }
45
46    // Ordina blocchi all'interno di ogni regione per Y poi X
47    for group in &mut by_layout {
48        group.sort_by_key(|b| (b.y1, b.x1));
49    }
50
51    // ── Emetti ogni regione layout in reading_order ────────────────────────
52    // Costruiamo un vettore ordinato di (reading_order, idx)
53    let mut order: Vec<(i32, usize)> = result.layout_boxes.iter()
54        .enumerate()
55        .map(|(i, lb)| (lb.reading_order, i))
56        .collect();
57    // reading_order == -1 (non disponibile) va in fondo
58    order.sort_by_key(|&(ro, _)| if ro < 0 { i32::MAX } else { ro });
59
60    for (_, idx) in &order {
61        let lb    = &result.layout_boxes[*idx];
62        let group = &by_layout[*idx];
63        if group.is_empty() { continue; }
64
65        let text = concat_blocks(group);
66        if text.trim().is_empty() { continue; }
67
68        match lb.semantic {
69            SemanticClass::Title => {
70                out.push_str("# ");
71                out.push_str(text.trim());
72                out.push_str("\n\n");
73            }
74            SemanticClass::Text | SemanticClass::List => {
75                push_text(&mut out, text.trim());
76            }
77            SemanticClass::Table => {
78                push_table(&mut out, *idx, &result.tables, text.trim());
79            }
80            SemanticClass::Equation => {
81                push_formula(&mut out, *idx, &result.formulas, text.trim());
82            }
83            SemanticClass::Figure => {
84                out.push_str("![figure]()\n\n");
85            }
86            SemanticClass::Header | SemanticClass::Footer => {
87                // skip: non contribuiscono al testo principale
88            }
89        }
90    }
91
92    // ── Orfani (nessuna regione layout assegnata) ──────────────────────────
93    orphans.sort_by_key(|b| (b.y1, b.x1));
94    for blk in orphans {
95        push_text(&mut out, blk.text.trim());
96    }
97
98    out
99}
100
101/// Converte più pagine in Markdown, separandole con `---`.
102pub fn to_markdown(pages: &[OcrPageResult]) -> String {
103    pages.iter()
104        .enumerate()
105        .map(|(i, p)| {
106            let header = if pages.len() > 1 {
107                format!("## Page {}\n\n", i + 1)
108            } else {
109                String::new()
110            };
111            header + &page_to_markdown(p)
112        })
113        .collect::<Vec<_>>()
114        .join("\n---\n\n")
115}
116
117// ─── Helper ──────────────────────────────────────────────────────────────────
118
119fn concat_blocks(blocks: &[&OcrBlock]) -> String {
120    blocks.iter().map(|b| b.text.as_str()).collect::<Vec<_>>().join(" ")
121}
122
123fn push_text(out: &mut String, text: &str) {
124    if !text.is_empty() {
125        out.push_str(text);
126        out.push_str("\n\n");
127    }
128}
129
130/// Emette la tabella GFM da `tables` (se trovata e non vuota) oppure il testo OCR raw.
131fn push_table(out: &mut String, layout_idx: usize, tables: &[OcrTable], fallback: &str) {
132    if let Some(t) = tables.iter().find(|t| t.layout_idx == layout_idx as i32) {
133        if !t.gfm.is_empty() {
134            out.push_str(&t.gfm);
135            out.push('\n');
136            return;
137        }
138    }
139    // Fallback: testo OCR grezzo (nessun modello tabella disponibile)
140    push_text(out, fallback);
141}
142
143/// Emette la formula LaTeX da `formulas` (se trovata e non vuota) oppure il testo OCR raw.
144fn push_formula(out: &mut String, layout_idx: usize, formulas: &[OcrFormula], fallback: &str) {
145    if let Some(f) = formulas.iter().find(|f| f.layout_idx == layout_idx as i32) {
146        if !f.latex.is_empty() {
147            out.push_str("$$\n");
148            out.push_str(f.latex.trim());
149            out.push_str("\n$$\n\n");
150            return;
151        }
152    }
153    // Fallback: testo OCR grezzo
154    push_text(out, fallback);
155}