use anyhow::{anyhow, Context, Result};
use image::{DynamicImage, RgbImage};
use ppocr_rs::{
derive_grid, grid_to_gfm, CellDetector, LayoutAnalyzer, OcrLite, OcrOptions,
SemanticClass, TextBlockWithLayout,
};
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use std::time::Instant;
use tiff::decoder::{Decoder, DecodingResult};
use tiff::ColorType as TiffColorType;
fn models_root() -> String {
std::env::var("PPOCR_MODELS_DIR").unwrap_or_else(|_| "models/paddleocr".to_string())
}
fn tiff_path_from_args() -> PathBuf {
if let Some(p) = std::env::args().nth(1) {
return PathBuf::from(p);
}
if let Ok(p) = std::env::var("PPOCR_TEST_TIFF") {
return PathBuf::from(p);
}
eprintln!("Uso: cell_aware_reorder <tiff_path> [page_index]");
eprintln!(" oppure: PPOCR_TEST_TIFF=path/to/doc.tiff PPOCR_TEST_PAGE=1");
std::process::exit(1);
}
fn page_index_from_args() -> usize {
std::env::args().nth(2)
.or_else(|| std::env::var("PPOCR_TEST_PAGE").ok())
.and_then(|s| s.parse().ok())
.unwrap_or(0)
}
fn main() -> Result<()> {
let t_total = Instant::now();
let tiff_path = tiff_path_from_args();
let page_idx = page_index_from_args();
let models_root = PathBuf::from(models_root());
if !tiff_path.is_file() {
eprintln!("[errore] TIFF non trovato: {}", tiff_path.display());
std::process::exit(1);
}
println!("TIFF: {} pagina: {}", tiff_path.display(), page_idx);
let (mut ocr, mut layout, mut cell_det) = init_paddle_pipeline(&models_root)?;
let gt_path = PathBuf::from("");
run_test_case(&tiff_path, page_idx, >_path, &mut ocr, &mut layout, &mut cell_det)?;
println!("\nDone in {:?}", t_total.elapsed());
Ok(())
}
fn init_paddle_pipeline(
models_root: &PathBuf,
) -> Result<(OcrLite, LayoutAnalyzer, CellDetector)> {
let det_path = models_root.join("latin/det.onnx");
let rec_path = models_root.join("latin/rec_latin.onnx");
let dict_path = models_root.join("latin/dict_latin.txt");
let cls_path = models_root.join("cls/ch_ppocr_mobile_v2.0_cls_infer.onnx");
let layout_path = models_root.join("layout/PP-DocLayoutV3.onnx");
let cell_path = models_root.join("table/RT-DETR-L_wired_table_cell_det.onnx");
for p in [&det_path, &rec_path, &dict_path, &cls_path, &layout_path, &cell_path] {
if !p.exists() {
return Err(anyhow!("Modello mancante: {}", p.display()));
}
}
let t = Instant::now();
let mut ocr = OcrLite::new();
ocr.init_models_with_dict(
det_path.to_str().unwrap(),
cls_path.to_str().unwrap(),
rec_path.to_str().unwrap(),
dict_path.to_str().unwrap(),
4,
).context("init paddle det+cls+rec")?;
println!("[init] det+cls+rec in {:?}", t.elapsed());
let t = Instant::now();
let layout = LayoutAnalyzer::from_path(&layout_path).context("init layout analyzer")?;
println!("[init] PP-DocLayoutV3 in {:?}", t.elapsed());
let t = Instant::now();
let cell = CellDetector::from_path(&cell_path).context("init cell detector")?;
println!("[init] RT-DETR-L cell in {:?}", t.elapsed());
Ok((ocr, layout, cell))
}
fn run_test_case(
tiff_path: &PathBuf,
page_idx: usize,
gt_path: &PathBuf,
ocr: &mut OcrLite,
layout: &mut LayoutAnalyzer,
cell_det: &mut CellDetector,
) -> Result<()> {
let img = decode_tiff_page(tiff_path, page_idx)?;
println!(
"[1] TIFF decoded: page {}, {}×{} px",
page_idx + 1, img.width(), img.height(),
);
let t = Instant::now();
let result = ocr
.detect_with_layout(
&img,
layout,
50, 1024, 0.5, 0.3, 1.6, true, true, OcrOptions { return_word_box: false, lang: None, ..OcrOptions::default() },
)
.context("paddle full-page layout-aware OCR")?;
println!(
"[2] OCR in {:?}: {} layout boxes, {} text blocks",
t.elapsed(),
result.layout_boxes.len(),
result.blocks.len(),
);
let gt_text = std::fs::read_to_string(gt_path).ok();
if let Some(ref gt) = gt_text {
println!("[gt] caricato: {} char, {} righe", gt.len(), gt.lines().count());
} else {
eprintln!("[gt] NON CARICATO: {}", gt_path.display());
}
let mut all_drag_select_lines: Vec<String> = Vec::new();
for (i, lb) in result.layout_boxes.iter().enumerate() {
if lb.class.semantic() != SemanticClass::Table {
continue;
}
println!("\n=== TABLE BLOCK #{i} ===");
println!("Layout bbox: ({}, {}) → ({}, {}) [size {}×{}]",
lb.x, lb.y, lb.x + lb.w, lb.y + lb.h, lb.w, lb.h);
let (x0, y0, w, h) = (
lb.x.max(0) as u32,
lb.y.max(0) as u32,
lb.w.max(0) as u32,
lb.h.max(0) as u32,
);
let crop = image::imageops::crop_imm(&img, x0, y0, w, h).to_image();
let t = Instant::now();
let cells_detected = cell_det.detect(&crop)
.context("cell detection")?;
println!("[4a] Cell detection: {} cell(s) in {:?}",
cells_detected.len(), t.elapsed());
let grid = derive_grid(cells_detected.clone());
let n_rows = grid.len();
let n_cols = grid.iter().map(|r| r.len()).max().unwrap_or(0);
println!("[4b] derive_grid: {n_rows} rows × {n_cols} cols");
let table_blocks: Vec<&TextBlockWithLayout> = result.blocks.iter()
.filter(|tbl| tbl.layout_index == Some(i))
.collect();
println!("[4c] OCR lines in this table: {}", table_blocks.len());
println!("\n--- ORIGINAL ORDER (PaddleOCR raster scan) ---");
for (j, tbl) in table_blocks.iter().enumerate() {
let bb = bbox_from_points(&tbl.block.box_points);
println!(" [{j:>2}] {:>5},{:<5} {:>5}×{:<5} '{}'",
bb.left, bb.top, bb.right - bb.left, bb.bottom - bb.top,
tbl.block.text);
}
println!("\n--- GFM (derive_grid + cell text from OCR lines) ---");
let gfm = grid_to_gfm(&grid, |r, c| {
if r >= grid.len() || c >= grid[r].len() {
return String::new();
}
let cell = &grid[r][c];
let cl = cell.left + x0 as i32;
let ct = cell.top + y0 as i32;
let cr = cell.right + x0 as i32;
let cb = cell.bottom + y0 as i32;
let mut texts: Vec<&str> = Vec::new();
for tbl in &table_blocks {
let bb = bbox_from_points(&tbl.block.box_points);
let cx = (bb.left + bb.right) / 2;
let cy = (bb.top + bb.bottom) / 2;
if cx >= cl && cx <= cr && cy >= ct && cy <= cb {
texts.push(&tbl.block.text);
}
}
texts.join(" ")
});
println!("{gfm}");
println!("\n--- CELL-AWARE REORDER (new algorithm, geometric) ---");
let cells_aware = reorder_cell_aware(&table_blocks);
for (cell_idx, cell) in cells_aware.iter().enumerate() {
for (line_idx, &line_global_idx) in cell.iter().enumerate() {
let tbl = &table_blocks[line_global_idx];
let prefix = if line_idx == 0 {
format!("CELL[{cell_idx:>2}]:")
} else {
" ".to_string()
};
println!(" {prefix} '{}'", tbl.block.text);
}
}
println!("\n--- SIMULATED DRAG-SELECT OUTPUT ---");
for cell in &cells_aware {
for &i in cell {
let line = table_blocks[i].block.text.clone();
println!("{}", line);
all_drag_select_lines.push(line);
}
}
}
if let Some(gt) = gt_text {
println!("\n=== GROUND TRUTH COMPARISON ===");
compare_vs_ground_truth(&all_drag_select_lines, >);
}
Ok(())
}
fn compare_vs_ground_truth(drag_select: &[String], gt: &str) {
let gt_lines: Vec<String> = gt.lines()
.map(|l| l.trim().trim_start_matches(|c: char| c == '●' || c.is_whitespace() || c == '*').trim().to_string())
.filter(|l| !l.is_empty())
.collect();
let drag_set: std::collections::HashSet<&str> = drag_select.iter().map(|s| s.as_str()).collect();
let mut hits = 0usize;
let mut misses: Vec<&str> = Vec::new();
for gtl in >_lines {
if drag_set.contains(gtl.as_str()) {
hits += 1;
} else {
let tokens: Vec<&str> = gtl.split(" ").map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
if tokens.len() > 1 && tokens.iter().all(|t| drag_set.contains(*t)) {
hits += 1;
} else {
misses.push(gtl.as_str());
}
}
}
println!("GT lines: {}, drag-select lines: {}", gt_lines.len(), drag_select.len());
println!("Match: {}/{} ({:.0}%)", hits, gt_lines.len(),
(hits as f32 / gt_lines.len() as f32) * 100.0);
if !misses.is_empty() {
println!("Missing in drag-select ({}):", misses.len());
for m in &misses {
println!(" - {m:?}");
}
}
}
#[derive(Debug, Clone, Copy)]
struct AABB { left: i32, top: i32, right: i32, bottom: i32 }
fn bbox_from_points(points: &[ppocr_rs::Point]) -> AABB {
if points.is_empty() {
return AABB { left: 0, top: 0, right: 0, bottom: 0 };
}
let xs: Vec<i32> = points.iter().map(|p| p.x as i32).collect();
let ys: Vec<i32> = points.iter().map(|p| p.y as i32).collect();
AABB {
left: *xs.iter().min().unwrap(),
top: *ys.iter().min().unwrap(),
right: *xs.iter().max().unwrap(),
bottom: *ys.iter().max().unwrap(),
}
}
fn reorder_cell_aware(blocks: &[&TextBlockWithLayout]) -> Vec<Vec<usize>> {
let n = blocks.len();
if n == 0 { return Vec::new(); }
let bbs: Vec<AABB> = blocks.iter().map(|b| bbox_from_points(&b.block.box_points)).collect();
let cx: Vec<i32> = bbs.iter().map(|b| (b.left + b.right) / 2).collect();
let cy: Vec<i32> = bbs.iter().map(|b| (b.top + b.bottom) / 2).collect();
let mut hs: Vec<i32> = bbs.iter().map(|b| b.bottom - b.top).collect();
hs.sort();
let median_h = (*hs.get(hs.len() / 2).unwrap_or(&20)).max(8);
let col_threshold = (median_h * 4).max(40);
let mut col_of: Vec<usize> = vec![0; n];
let mut col_cx: Vec<i32> = Vec::new();
let mut col_count: Vec<i32> = Vec::new();
let mut order_x: Vec<usize> = (0..n).collect();
order_x.sort_by_key(|&i| cx[i]);
for &i in &order_x {
let x = cx[i];
match col_cx.iter().position(|&v| (x - v).abs() < col_threshold) {
Some(c) => {
col_of[i] = c;
col_cx[c] = (col_cx[c] * col_count[c] + x) / (col_count[c] + 1);
col_count[c] += 1;
}
None => {
col_of[i] = col_cx.len();
col_cx.push(x);
col_count.push(1);
}
}
}
let n_cols = col_cx.len();
eprintln!("[reorder] median_h={median_h}, col_threshold={col_threshold}, n_cols={n_cols}");
let cell_threshold = ((median_h as f32) * 1.2) as i32;
let cell_threshold = cell_threshold.max(12);
eprintln!("[reorder] cell_threshold={cell_threshold}");
let mut cells: Vec<Vec<usize>> = Vec::new();
let mut cell_cy: Vec<i32> = Vec::new();
let mut cell_left: Vec<i32> = Vec::new();
for c in 0..n_cols {
let mut col_indices: Vec<usize> = (0..n).filter(|&i| col_of[i] == c).collect();
col_indices.sort_by_key(|&i| cy[i]);
let mut current: Vec<usize> = Vec::new();
let mut last_cy: Option<i32> = None;
for i in col_indices {
let yi = cy[i];
let join = matches!(last_cy, Some(prev) if (yi - prev).abs() < cell_threshold);
if join {
current.push(i);
last_cy = Some(yi);
} else {
flush_cell(&mut current, &cy, &bbs, &mut cells, &mut cell_cy, &mut cell_left);
current = vec![i];
last_cy = Some(yi);
}
}
flush_cell(&mut current, &cy, &bbs, &mut cells, &mut cell_cy, &mut cell_left);
}
eprintln!("[reorder] {} celle dopo X+Y clustering", cells.len());
let row_threshold = ((median_h as f32) * 1.5) as i32;
let row_threshold = row_threshold.max(10);
eprintln!("[reorder] row_threshold={row_threshold}");
let mut row_of: Vec<usize> = vec![0; cells.len()];
let mut row_cy: Vec<i32> = Vec::new();
let mut row_count: Vec<i32> = Vec::new();
let mut cell_order_y: Vec<usize> = (0..cells.len()).collect();
cell_order_y.sort_by_key(|&i| cell_cy[i]);
for &i in &cell_order_y {
let y = cell_cy[i];
match row_cy.iter().position(|&v| (y - v).abs() < row_threshold) {
Some(r) => {
row_of[i] = r;
row_cy[r] = (row_cy[r] * row_count[r] + y) / (row_count[r] + 1);
row_count[r] += 1;
}
None => {
row_of[i] = row_cy.len();
row_cy.push(y);
row_count.push(1);
}
}
}
let n_rows = row_cy.len();
eprintln!("[reorder] n_rows={n_rows}");
let mut row_rank: Vec<usize> = (0..n_rows).collect();
row_rank.sort_by_key(|&r| row_cy[r]);
let mut emitted: Vec<Vec<usize>> = Vec::new();
for &row_idx in &row_rank {
let mut cells_in_row: Vec<usize> = (0..cells.len())
.filter(|&i| row_of[i] == row_idx)
.collect();
cells_in_row.sort_by_key(|&i| cell_left[i]);
for cell_idx in cells_in_row {
let mut idxs = std::mem::take(&mut cells[cell_idx]);
if idxs.is_empty() { continue; }
idxs.sort_by_key(|&i| bbs[i].top);
emitted.push(idxs);
}
}
emitted
}
fn flush_cell(
current: &mut Vec<usize>,
cy: &[i32],
bbs: &[AABB],
cells: &mut Vec<Vec<usize>>,
cell_cy: &mut Vec<i32>,
cell_left: &mut Vec<i32>,
) {
if current.is_empty() { return; }
let avg_cy = current.iter().map(|&i| cy[i]).sum::<i32>() / (current.len() as i32);
let min_l = current.iter().map(|&i| bbs[i].left).min().unwrap_or(0);
cells.push(std::mem::take(current));
cell_cy.push(avg_cy);
cell_left.push(min_l);
}
fn decode_tiff_page(path: &PathBuf, page_index: usize) -> Result<RgbImage> {
let file = BufReader::new(File::open(path).context("open TIFF")?);
let mut decoder = Decoder::new(file).context("init TIFF decoder")?;
for _ in 0..page_index {
if !decoder.more_images() {
return Err(anyhow!(
"TIFF ha solo {} pagine, richiesta page {} (0-based)",
page_index, page_index
));
}
decoder.next_image().context("next TIFF page")?;
}
let (w, h) = decoder.dimensions().context("TIFF dimensions")?;
let color = decoder.colortype().context("TIFF color type")?;
let buf = decoder.read_image().context("read TIFF page")?;
let dyn_img = match (color, buf) {
(TiffColorType::Gray(8), DecodingResult::U8(b)) => DynamicImage::ImageLuma8(
image::ImageBuffer::from_raw(w, h, b).context("build Luma8")?
),
(TiffColorType::RGB(8), DecodingResult::U8(b)) => DynamicImage::ImageRgb8(
image::ImageBuffer::from_raw(w, h, b).context("build RGB8")?
),
(TiffColorType::RGBA(8), DecodingResult::U8(b)) => DynamicImage::ImageRgba8(
image::ImageBuffer::from_raw(w, h, b).context("build RGBA8")?
),
(c, _) => return Err(anyhow!("TIFF color type non gestito: {c:?}")),
};
Ok(dyn_img.to_rgb8())
}