mod dump;
mod element;
pub use dump::render as dump_tree;
pub use element::Kid;
pub use element::StructElement;
use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
use pdfrum_object::{Dict, Object, Resolve};
use crate::names;
use crate::nav::number_tree;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct StructTree {
pub elements: Vec<StructElement>,
pub top: Vec<Option<usize>>,
}
impl StructTree {
#[must_use]
pub fn load_page<R: Resolve>(
catalog: &Dict,
page: &Dict,
page_obj_num: u32,
r: &R,
limits: &Limits,
diags: &mut Diagnostics,
) -> Option<StructTree> {
if !is_tagged(catalog, r) {
return None;
}
let root = catalog.dict(names::STRUCT_TREE_ROOT, r);
let mut tree = StructTree::default();
let Some(root) = root else {
return Some(tree);
};
let role_map = root.dict(names::ROLE_MAP, r);
let Some(kids) = root.get(names::K, r).map(|k| k.get().clone()) else {
return Some(tree);
};
tree.top = match &kids {
Object::Dict(_) => vec![None],
Object::Array(array) => vec![None; array.len()],
_ => return Some(tree),
};
let Some(parent_tree) = root.dict(names::PARENT_TREE, r) else {
return Some(tree);
};
let parents_id = page.int(names::STRUCT_PARENTS, r).unwrap_or(-1);
if parents_id < 0 {
return Some(tree);
}
let entry = number_tree::find(&parent_tree, parents_id, r, limits, diags);
let Some(Object::Array(owners)) = entry else {
return Some(tree);
};
let mut build = Build {
root: &root,
root_kids: &kids,
role_map: role_map.as_ref(),
page_obj_num,
memo: Vec::new(),
};
for index in 0..owners.len() {
if let Some(dict) = owners.dict_at(index, r) {
let reference = owners.reference_at(index);
build.add_page_node(
&mut tree,
Node {
dict: &dict,
reference,
},
0,
r,
limits,
diags,
);
}
}
Some(tree)
}
}
fn is_tagged<R: Resolve>(catalog: &Dict, r: &R) -> bool {
catalog
.dict(names::MARK_INFO, r)
.and_then(|info| info.int(names::MARKED, r))
.is_some_and(|marked| marked != 0)
}
#[derive(Debug, Clone, Copy)]
struct Node<'a> {
dict: &'a Dict,
reference: Option<pdfrum_object::ObjRef>,
}
struct Build<'a> {
root: &'a Dict,
root_kids: &'a Object,
role_map: Option<&'a Dict>,
page_obj_num: u32,
memo: Vec<(Dict, usize)>,
}
impl Build<'_> {
fn add_page_node<R: Resolve>(
&mut self,
tree: &mut StructTree,
node: Node,
depth: u32,
r: &R,
limits: &Limits,
diags: &mut Diagnostics,
) -> Option<usize> {
let Node { dict, reference } = node;
if depth > limits.max_name_tree_depth {
diags.record(Severity::Suspicious, DiagKind::TreeDepthExceeded, None);
return None;
}
if let Some((_, index)) = self.memo.iter().find(|(seen, _)| seen == dict) {
return Some(*index);
}
let kind = dict
.name(names::S)
.map_or_else(Vec::new, |s| s.as_bytes().to_vec());
let element = StructElement {
dict: dict.clone(),
reference,
kind: element::map_role(self.role_map, &kind),
kids: StructElement::load_kids(dict, self.page_obj_num, r),
parent: None,
};
let index = tree.elements.len();
tree.elements.push(element);
self.memo.push((dict.clone(), index));
let parent = dict.dict(names::P, r);
let is_top = match &parent {
None => true,
Some(p) => p.name(names::TYPE) == Some(names::STRUCT_TREE_ROOT),
};
if is_top {
if !self.add_top_level_node(tree, dict, reference, index) {
self.forget(tree, dict, index, diags);
}
return Some(index);
}
let Some(parent) = parent else {
return Some(index);
};
let parent_ref = dict.reference(names::P);
let parent_index = self.add_page_node(
tree,
Node {
dict: &parent,
reference: parent_ref,
},
depth + 1,
r,
limits,
diags,
)?;
let linked = tree
.elements
.get_mut(parent_index)
.is_some_and(|p| p.link_kid(dict, reference, index));
if !linked {
self.forget(tree, dict, index, diags);
return Some(index);
}
if let Some(child) = tree.elements.get_mut(index) {
child.parent = Some(parent_index);
}
Some(index)
}
fn forget(
&mut self,
tree: &mut StructTree,
dict: &Dict,
index: usize,
diags: &mut Diagnostics,
) {
self.memo.retain(|(seen, at)| seen != dict || *at != index);
let _ = tree;
diags.record(Severity::Recovered, DiagKind::StructElementDropped, None);
}
fn add_top_level_node(
&self,
tree: &mut StructTree,
dict: &Dict,
reference: Option<pdfrum_object::ObjRef>,
index: usize,
) -> bool {
let _ = self.root;
match self.root_kids {
Object::Dict(root_kid) => {
if root_kid != dict {
return false;
}
if let Some(slot) = tree.top.first_mut() {
*slot = Some(index);
}
true
}
Object::Array(array) => {
let mut saved = false;
for (slot, item) in array.iter().enumerate() {
if let (Some(item_ref), Some(reference)) = (item.as_ref_id(), reference)
&& item_ref.num == reference.num
&& let Some(cell) = tree.top.get_mut(slot)
{
*cell = Some(index);
saved = true;
}
}
saved
}
_ => true,
}
}
}
#[cfg(test)]
mod tests {
use super::StructTree;
use pdfrum_common::{Diagnostics, Limits};
use pdfrum_object::{Dict, Name, NoResolve, Object};
fn dict(pairs: &[(&str, Object)]) -> Dict {
Dict::from_pairs(
pairs
.iter()
.map(|(k, v)| (Name::from(*k), v.clone()))
.collect::<Vec<_>>(),
)
}
#[test]
fn an_untagged_document_has_no_tree_at_all() {
let (l, mut d) = (Limits::default(), Diagnostics::default());
assert!(
StructTree::load_page(&Dict::new(), &Dict::new(), 0, &NoResolve, &l, &mut d).is_none()
);
}
#[test]
fn the_marked_flag_reads_as_an_integer_whichever_way_it_is_written() {
let (l, mut d) = (Limits::default(), Diagnostics::default());
for marked in [Object::Bool(true), Object::Int(1)] {
let catalog = dict(&[("MarkInfo", Object::Dict(dict(&[("Marked", marked)])))]);
assert!(
StructTree::load_page(&catalog, &Dict::new(), 0, &NoResolve, &l, &mut d).is_some()
);
}
for marked in [
Object::Bool(false),
Object::Int(0),
Object::Name(Name::from("yes")),
] {
let catalog = dict(&[("MarkInfo", Object::Dict(dict(&[("Marked", marked)])))]);
assert!(
StructTree::load_page(&catalog, &Dict::new(), 0, &NoResolve, &l, &mut d).is_none()
);
}
}
#[test]
fn a_tagged_document_with_no_root_yields_an_empty_tree() {
let catalog = dict(&[(
"MarkInfo",
Object::Dict(dict(&[("Marked", Object::Int(1))])),
)]);
let (l, mut d) = (Limits::default(), Diagnostics::default());
let tree = StructTree::load_page(&catalog, &Dict::new(), 0, &NoResolve, &l, &mut d);
assert_eq!(tree, Some(StructTree::default()));
}
}