use docling_core::{DoclingDocument, Node};
pub type AssembledPage = (Vec<Node>, Vec<(String, String)>);
use crate::layout::{label_threshold, Region};
use crate::pdfium_backend::{PdfPage, TextCell};
pub fn refine_regions(
regions: Vec<Region>,
cells: &[TextCell],
page_w: f32,
page_h: f32,
) -> Vec<Region> {
let mut regions = regions;
regions.retain(|r| r.score >= label_threshold(r.label));
let mut regions = crate::assemble::resolve(regions);
crate::assemble::add_orphan_regions(&mut regions, cells);
crate::assemble::drop_false_pictures(&mut regions, cells, page_w, page_h);
crate::assemble::drop_contained_regulars(&mut regions);
crate::assemble::recover_text_panels(&mut regions, cells);
regions
}
pub fn drop_duplicate_text_claims(regions: Vec<Region>, cells: &[TextCell]) -> Vec<Region> {
let claimed: Vec<Vec<usize>> = regions
.iter()
.map(|r| {
cells
.iter()
.enumerate()
.filter(|(_, c)| {
let ca = ((c.r - c.l) * (c.b - c.t)).max(1.0);
let iw = (c.r.min(r.r) - c.l.max(r.l)).max(0.0);
let ih = (c.b.min(r.b) - c.t.max(r.t)).max(0.0);
iw * ih / ca > 0.5
})
.map(|(i, _)| i)
.collect()
})
.collect();
let text_like = |label: &str| matches!(label, "text" | "list_item" | "section_header");
let mut drop = vec![false; regions.len()];
for a in 0..regions.len() {
if !text_like(regions[a].label) || claimed[a].is_empty() {
continue;
}
for b in 0..regions.len() {
if a == b || drop[b] || !text_like(regions[b].label) {
continue;
}
let subset = claimed[a].iter().all(|i| claimed[b].contains(i));
if !subset {
continue;
}
let identical = claimed[a].len() == claimed[b].len();
if identical && (regions[a].score, b) > (regions[b].score, a) {
continue;
}
drop[a] = true;
break;
}
}
regions
.into_iter()
.zip(drop)
.filter(|(_, d)| !d)
.map(|(r, _)| r)
.collect()
}
pub fn assemble_page(page: &PdfPage, regions: Vec<Region>) -> AssembledPage {
assemble_page_with_tables(page, regions, vec![None; 0].into_iter().collect())
}
pub fn assemble_page_with_tables(
page: &PdfPage,
regions: Vec<Region>,
mut table_rows: Vec<Option<Vec<Vec<String>>>>,
) -> AssembledPage {
table_rows.resize(regions.len(), None);
let enrich = vec![None; regions.len()];
crate::assemble::assemble_page(page, regions, &table_rows, &enrich)
}
pub fn finish_document(name: &str, pages: Vec<AssembledPage>) -> DoclingDocument {
let mut doc = DoclingDocument::new(name);
for (nodes, links) in pages {
doc.nodes.extend(nodes);
doc.links.extend(links);
}
crate::assemble::merge_continuations(&mut doc.nodes);
doc
}
#[cfg(test)]
mod duplicate_claims {
use crate::layout::Region;
use crate::pdfium_backend::TextCell;
fn cell(l: f32, t: f32, r: f32, b: f32) -> TextCell {
TextCell {
text: "x".into(),
l,
t,
r,
b,
}
}
fn region(label: &'static str, score: f32, l: f32, t: f32, r: f32, b: f32) -> Region {
Region {
label,
score,
l,
t,
r,
b,
}
}
#[test]
fn part_regions_of_a_block_are_dropped() {
let cells = vec![cell(10.0, 10.0, 90.0, 20.0), cell(10.0, 30.0, 90.0, 40.0)];
let regions = vec![
region("text", 0.9, 5.0, 5.0, 95.0, 45.0), region("text", 0.8, 5.0, 5.0, 95.0, 25.0), region("text", 0.8, 5.0, 25.0, 95.0, 45.0), ];
let kept = super::drop_duplicate_text_claims(regions, &cells);
assert_eq!(kept.len(), 1, "kept: {kept:?}");
assert_eq!((kept[0].t, kept[0].b), (5.0, 45.0), "the block survives");
}
#[test]
fn disjoint_and_non_text_regions_are_kept() {
let cells = vec![cell(10.0, 10.0, 90.0, 20.0), cell(10.0, 50.0, 90.0, 60.0)];
let regions = vec![
region("text", 0.9, 5.0, 5.0, 95.0, 25.0),
region("text", 0.9, 5.0, 45.0, 95.0, 65.0),
region("table", 0.9, 5.0, 5.0, 95.0, 65.0), ];
assert_eq!(super::drop_duplicate_text_claims(regions, &cells).len(), 3);
}
#[test]
fn identical_claims_keep_the_higher_score() {
let cells = vec![cell(10.0, 10.0, 90.0, 20.0)];
let regions = vec![
region("text", 0.7, 5.0, 5.0, 95.0, 25.0),
region("text", 0.9, 6.0, 6.0, 94.0, 24.0),
];
let kept = super::drop_duplicate_text_claims(regions, &cells);
assert_eq!(kept.len(), 1);
assert_eq!(kept[0].score, 0.9);
}
}