feat(tables): extract tables from tagged PDF structure tree (#17)

When a PDF has a well-formed structure tree with /Table > /TR > /TD|TH
elements linked to MCIDs, build tables directly from the semantic
hierarchy. Runs as highest-priority detection (step 0) before rect-based,
line-based, and heuristic strategies.

- Add StructTree::extract_tables() to walk the tree and collect table
  descriptors with row/cell/MCID info
- Add detect_tables_from_struct_tree() to match MCIDs to TextItems
- Reject tables with <30% MCID cell coverage (stale structure trees)
- Update 2013-app2 snapshot (struct-tree gives valid but different
  column ordering)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-03-25 12:33:39 -07:00
co-authored by Claude Opus 4.6
parent 61a4c7a088
commit b6764e7ca9
6 changed files with 809 additions and 17 deletions
+18 -14
View File
@@ -386,20 +386,23 @@ fn process_document(
};
// Parse structure tree for tagged PDFs (reuses the loaded document)
let struct_roles = structure_tree::StructTree::from_doc(&doc).and_then(|tree| {
let page_ids = doc.get_pages();
let roles = tree.mcid_to_roles(&page_ids);
if roles.is_empty() {
None
} else {
log::debug!(
"structure tree: {} pages with MCID roles, {} total MCIDs",
roles.len(),
tree.mcid_count()
);
Some(roles)
}
});
let (struct_roles, struct_tables) = structure_tree::StructTree::from_doc(&doc)
.map(|tree| {
let page_ids = doc.get_pages();
let roles = tree.mcid_to_roles(&page_ids);
let tables = tree.extract_tables(&page_ids);
if !roles.is_empty() {
log::debug!(
"structure tree: {} pages with MCID roles, {} total MCIDs, {} tagged tables",
roles.len(),
tree.mcid_count(),
tables.len()
);
}
let roles = if roles.is_empty() { None } else { Some(roles) };
(roles, tables)
})
.unwrap_or((None, Vec::new()));
let (markdown, layout, has_encoding_issues, gid_pages) = match extracted {
Some(((items, rects, lines), page_thresholds, gid_encoded_pages)) => {
@@ -463,6 +466,7 @@ fn process_document(
&lines,
&page_thresholds,
struct_roles.as_ref(),
&struct_tables,
))
};