1mod dump;
22mod element;
23
24pub use dump::render as dump_tree;
25pub use element::Kid;
26pub use element::StructElement;
27
28use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
29use pdfrum_object::{Dict, Object, Resolve};
30
31use crate::names;
32use crate::nav::number_tree;
33
34#[derive(Debug, Clone, Default, PartialEq)]
57pub struct StructTree {
58 pub elements: Vec<StructElement>,
61 pub top: Vec<Option<usize>>,
64}
65
66impl StructTree {
67 #[must_use]
102 pub fn load_page<R: Resolve>(
103 catalog: &Dict,
104 page: &Dict,
105 page_obj_num: u32,
106 r: &R,
107 limits: &Limits,
108 diags: &mut Diagnostics,
109 ) -> Option<StructTree> {
110 if !is_tagged(catalog, r) {
111 return None;
112 }
113 let root = catalog.dict(names::STRUCT_TREE_ROOT, r);
114 let mut tree = StructTree::default();
115 let Some(root) = root else {
116 return Some(tree);
117 };
118 let role_map = root.dict(names::ROLE_MAP, r);
119
120 let Some(kids) = root.get(names::K, r).map(|k| k.get().clone()) else {
121 return Some(tree);
122 };
123 tree.top = match &kids {
124 Object::Dict(_) => vec![None],
125 Object::Array(array) => vec![None; array.len()],
126 _ => return Some(tree),
127 };
128
129 let Some(parent_tree) = root.dict(names::PARENT_TREE, r) else {
130 return Some(tree);
131 };
132 let parents_id = page.int(names::STRUCT_PARENTS, r).unwrap_or(-1);
133 if parents_id < 0 {
134 return Some(tree);
135 }
136 let entry = number_tree::find(&parent_tree, parents_id, r, limits, diags);
137 let Some(Object::Array(owners)) = entry else {
138 return Some(tree);
139 };
140
141 let mut build = Build {
142 root: &root,
143 root_kids: &kids,
144 role_map: role_map.as_ref(),
145 page_obj_num,
146 memo: Vec::new(),
147 };
148 for index in 0..owners.len() {
149 if let Some(dict) = owners.dict_at(index, r) {
150 let reference = owners.reference_at(index);
151 build.add_page_node(
152 &mut tree,
153 Node {
154 dict: &dict,
155 reference,
156 },
157 0,
158 r,
159 limits,
160 diags,
161 );
162 }
163 }
164 Some(tree)
165 }
166}
167
168fn is_tagged<R: Resolve>(catalog: &Dict, r: &R) -> bool {
170 catalog
171 .dict(names::MARK_INFO, r)
172 .and_then(|info| info.int(names::MARKED, r))
173 .is_some_and(|marked| marked != 0)
174}
175
176#[derive(Debug, Clone, Copy)]
179struct Node<'a> {
180 dict: &'a Dict,
181 reference: Option<pdfrum_object::ObjRef>,
182}
183
184struct Build<'a> {
186 root: &'a Dict,
187 root_kids: &'a Object,
188 role_map: Option<&'a Dict>,
189 page_obj_num: u32,
190 memo: Vec<(Dict, usize)>,
193}
194
195impl Build<'_> {
196 fn add_page_node<R: Resolve>(
198 &mut self,
199 tree: &mut StructTree,
200 node: Node,
201 depth: u32,
202 r: &R,
203 limits: &Limits,
204 diags: &mut Diagnostics,
205 ) -> Option<usize> {
206 let Node { dict, reference } = node;
207 if depth > limits.max_name_tree_depth {
208 diags.record(Severity::Suspicious, DiagKind::TreeDepthExceeded, None);
209 return None;
210 }
211 if let Some((_, index)) = self.memo.iter().find(|(seen, _)| seen == dict) {
212 return Some(*index);
213 }
214
215 let kind = dict
216 .name(names::S)
217 .map_or_else(Vec::new, |s| s.as_bytes().to_vec());
218 let element = StructElement {
219 dict: dict.clone(),
220 reference,
221 kind: element::map_role(self.role_map, &kind),
222 kids: StructElement::load_kids(dict, self.page_obj_num, r),
223 parent: None,
224 };
225 let index = tree.elements.len();
226 tree.elements.push(element);
227 self.memo.push((dict.clone(), index));
228
229 let parent = dict.dict(names::P, r);
232 let is_top = match &parent {
233 None => true,
234 Some(p) => p.name(names::TYPE) == Some(names::STRUCT_TREE_ROOT),
235 };
236 if is_top {
237 if !self.add_top_level_node(tree, dict, reference, index) {
238 self.forget(tree, dict, index, diags);
239 }
240 return Some(index);
241 }
242
243 let Some(parent) = parent else {
244 return Some(index);
245 };
246 let parent_ref = dict.reference(names::P);
247 let parent_index = self.add_page_node(
248 tree,
249 Node {
250 dict: &parent,
251 reference: parent_ref,
252 },
253 depth + 1,
254 r,
255 limits,
256 diags,
257 )?;
258 let linked = tree
259 .elements
260 .get_mut(parent_index)
261 .is_some_and(|p| p.link_kid(dict, reference, index));
262 if !linked {
263 self.forget(tree, dict, index, diags);
264 return Some(index);
265 }
266 if let Some(child) = tree.elements.get_mut(index) {
267 child.parent = Some(parent_index);
268 }
269 Some(index)
270 }
271
272 fn forget(
277 &mut self,
278 tree: &mut StructTree,
279 dict: &Dict,
280 index: usize,
281 diags: &mut Diagnostics,
282 ) {
283 self.memo.retain(|(seen, at)| seen != dict || *at != index);
284 let _ = tree;
285 diags.record(Severity::Recovered, DiagKind::StructElementDropped, None);
286 }
287
288 fn add_top_level_node(
295 &self,
296 tree: &mut StructTree,
297 dict: &Dict,
298 reference: Option<pdfrum_object::ObjRef>,
299 index: usize,
300 ) -> bool {
301 let _ = self.root;
302 match self.root_kids {
303 Object::Dict(root_kid) => {
304 if root_kid != dict {
305 return false;
306 }
307 if let Some(slot) = tree.top.first_mut() {
308 *slot = Some(index);
309 }
310 true
312 }
313 Object::Array(array) => {
314 let mut saved = false;
315 for (slot, item) in array.iter().enumerate() {
316 if let (Some(item_ref), Some(reference)) = (item.as_ref_id(), reference)
317 && item_ref.num == reference.num
318 && let Some(cell) = tree.top.get_mut(slot)
319 {
320 *cell = Some(index);
321 saved = true;
322 }
323 }
324 saved
325 }
326 _ => true,
327 }
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::StructTree;
334 use pdfrum_common::{Diagnostics, Limits};
335 use pdfrum_object::{Dict, Name, NoResolve, Object};
336
337 fn dict(pairs: &[(&str, Object)]) -> Dict {
338 Dict::from_pairs(
339 pairs
340 .iter()
341 .map(|(k, v)| (Name::from(*k), v.clone()))
342 .collect::<Vec<_>>(),
343 )
344 }
345
346 #[test]
347 fn an_untagged_document_has_no_tree_at_all() {
348 let (l, mut d) = (Limits::default(), Diagnostics::default());
349 assert!(
350 StructTree::load_page(&Dict::new(), &Dict::new(), 0, &NoResolve, &l, &mut d).is_none()
351 );
352 }
353
354 #[test]
355 fn the_marked_flag_reads_as_an_integer_whichever_way_it_is_written() {
356 let (l, mut d) = (Limits::default(), Diagnostics::default());
357 for marked in [Object::Bool(true), Object::Int(1)] {
358 let catalog = dict(&[("MarkInfo", Object::Dict(dict(&[("Marked", marked)])))]);
359 assert!(
360 StructTree::load_page(&catalog, &Dict::new(), 0, &NoResolve, &l, &mut d).is_some()
361 );
362 }
363 for marked in [
365 Object::Bool(false),
366 Object::Int(0),
367 Object::Name(Name::from("yes")),
368 ] {
369 let catalog = dict(&[("MarkInfo", Object::Dict(dict(&[("Marked", marked)])))]);
370 assert!(
371 StructTree::load_page(&catalog, &Dict::new(), 0, &NoResolve, &l, &mut d).is_none()
372 );
373 }
374 }
375
376 #[test]
377 fn a_tagged_document_with_no_root_yields_an_empty_tree() {
378 let catalog = dict(&[(
379 "MarkInfo",
380 Object::Dict(dict(&[("Marked", Object::Int(1))])),
381 )]);
382 let (l, mut d) = (Limits::default(), Diagnostics::default());
383 let tree = StructTree::load_page(&catalog, &Dict::new(), 0, &NoResolve, &l, &mut d);
384 assert_eq!(tree, Some(StructTree::default()));
385 }
386}