1use std::sync::Arc;
6
7use crate::document::Page;
8use crate::hash::{FastMap, FastSet};
9use crate::object::{Dict, ObjRef, Object};
10use crate::source::AsyncObjectSource;
11
12const MAX_ELEMENT_DEPTH: usize = 64;
15
16const MAX_NUMBER_TREE_NODES: usize = 4096;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct MarkedContentId {
25 pub parents: u32,
26 pub mcid: u32,
27}
28
29#[derive(Debug, Clone, PartialEq)]
34pub struct StructureTree {
35 root: Dict,
36 root_ref: Option<ObjRef>,
37}
38
39impl StructureTree {
40 pub async fn load_with<S: AsyncObjectSource>(src: &S, trailer: &Dict) -> Option<StructureTree> {
44 let root = trailer.get("Root")?;
45 let catalog = src.resolve(root).await.ok()?;
46 let entry = catalog.as_dict()?.get("StructTreeRoot")?;
47 let root_ref = entry.as_ref();
48 let resolved = src.resolve(entry).await.ok()?;
49 let root = resolved.as_dict()?.clone();
50 Some(StructureTree { root, root_ref })
51 }
52
53 pub async fn ranks_with<S: AsyncObjectSource>(
63 &self,
64 src: &S,
65 page: &Page,
66 ids: &[MarkedContentId],
67 ) -> FastMap<MarkedContentId, u32> {
68 let mut ranks: FastMap<MarkedContentId, u32> = FastMap::default();
69 let Some(parent_tree) = self.root.get("ParentTree") else {
70 return ranks;
71 };
72 let Some(parent_tree) = resolved_dict(src, parent_tree).await else {
73 return ranks;
74 };
75 let mut walk = Walk {
76 src,
77 page_ref: page.object_ref(),
78 root_ref: self.root_ref,
79 dicts: FastMap::default(),
80 paths: FastMap::default(),
81 parents: FastMap::default(),
82 };
83 let mut keyed: Vec<(MarkedContentId, Vec<u32>)> = Vec::new();
84 let mut seen: FastSet<MarkedContentId> = FastSet::default();
85 for id in ids {
86 if !seen.insert(*id) {
87 continue;
88 }
89 let Some(key) = walk.key_of(&parent_tree, *id).await else {
90 continue;
91 };
92 keyed.push((*id, key));
93 }
94 keyed.sort_by(|a, b| a.1.cmp(&b.1));
95 for (rank, (id, _)) in keyed.into_iter().enumerate() {
96 ranks.insert(id, rank as u32);
97 }
98 ranks
99 }
100}
101
102struct Walk<'a, S> {
106 src: &'a S,
107 page_ref: Option<ObjRef>,
108 root_ref: Option<ObjRef>,
109 dicts: FastMap<ObjRef, Option<Arc<Dict>>>,
110 paths: FastMap<ObjRef, Option<Arc<Vec<u32>>>>,
113 parents: FastMap<u32, Option<Arc<Vec<Object>>>>,
115}
116
117impl<S: AsyncObjectSource> Walk<'_, S> {
118 async fn key_of(&mut self, parent_tree: &Dict, id: MarkedContentId) -> Option<Vec<u32>> {
121 let elements = self.parent_array(parent_tree, id.parents).await?;
122 let element = elements.get(id.mcid as usize)?.as_ref()?;
123 let path = self.path_of(element).await?;
124 let dict = self.dict(element).await?;
125 let index = self.mcid_index(&dict, id.mcid).await?;
126 let mut key = Vec::with_capacity(path.len() + 1);
127 key.extend_from_slice(&path);
128 key.push(index);
129 Some(key)
130 }
131
132 async fn parent_array(&mut self, parent_tree: &Dict, key: u32) -> Option<Arc<Vec<Object>>> {
135 if let Some(cached) = self.parents.get(&key) {
136 return cached.clone();
137 }
138 let found = match number_tree_lookup(self.src, parent_tree, i64::from(key)).await {
139 Some(entry) => match self.src.resolve(&entry).await.ok()? {
140 Object::Array(items) => Some(Arc::new(items)),
141 _ => None,
142 },
143 None => None,
144 };
145 self.parents.insert(key, found.clone());
146 found
147 }
148
149 async fn dict(&mut self, r: ObjRef) -> Option<Arc<Dict>> {
150 if let Some(cached) = self.dicts.get(&r) {
151 return cached.clone();
152 }
153 let loaded = match self.src.get(r).await.ok()? {
154 Object::Dict(dict) => Some(Arc::new(dict)),
155 Object::Stream(stream) => Some(Arc::new(stream.dict)),
156 _ => None,
157 };
158 self.dicts.insert(r, loaded.clone());
159 loaded
160 }
161
162 async fn path_of(&mut self, element: ObjRef) -> Option<Arc<Vec<u32>>> {
167 if let Some(cached) = self.paths.get(&element) {
168 return cached.clone();
169 }
170 let mut chain: Vec<ObjRef> = vec![element];
171 let mut stop: Option<(ObjRef, bool, Arc<Vec<u32>>)> = None;
174 let mut current = element;
175 for _ in 0..MAX_ELEMENT_DEPTH {
176 let Some(dict) = self.dict(current).await else {
177 break;
178 };
179 let Some(parent) = dict.get("P").and_then(Object::as_ref) else {
180 break;
181 };
182 if self.is_root(parent).await {
183 stop = Some((parent, true, Arc::new(Vec::new())));
184 break;
185 }
186 if let Some(cached) = self.paths.get(&parent) {
187 stop = cached.clone().map(|path| (parent, false, path));
188 break;
189 }
190 chain.push(parent);
191 current = parent;
192 }
193 let Some((mut parent_ref, mut parent_is_root, known)) = stop else {
194 for r in chain {
195 self.paths.insert(r, None);
196 }
197 return None;
198 };
199 let mut path: Vec<u32> = (*known).clone();
201 let mut resolved: Option<Arc<Vec<u32>>> = None;
202 for child in chain.into_iter().rev() {
203 let index = if parent_is_root {
204 self.root_kid_index(child).await
205 } else {
206 match self.dict(parent_ref).await {
207 Some(parent) => kid_index(&parent, child),
208 None => None,
209 }
210 };
211 let Some(index) = index else {
212 self.paths.insert(child, None);
213 return None;
214 };
215 path.push(index);
216 let shared = Arc::new(path.clone());
217 self.paths.insert(child, Some(shared.clone()));
218 resolved = Some(shared);
219 parent_ref = child;
220 parent_is_root = false;
221 }
222 resolved
223 }
224
225 async fn is_root(&mut self, r: ObjRef) -> bool {
228 if self.root_ref == Some(r) {
229 return true;
230 }
231 let Some(dict) = self.dict(r).await else {
232 return false;
233 };
234 dict.get_name("Type")
235 .is_some_and(|n| n.0 == "StructTreeRoot")
236 }
237
238 async fn root_kid_index(&mut self, child: ObjRef) -> Option<u32> {
240 let root_ref = self.root_ref?;
241 let root = self.dict(root_ref).await?;
242 kid_index(&root, child)
243 }
244
245 async fn mcid_index(&mut self, element: &Dict, mcid: u32) -> Option<u32> {
251 let kids = kids_of(element);
252 let page_ref = self.page_ref;
253 let element_page = element.get_ref("Pg");
254 let direct = kids.iter().position(|kid| match kid {
255 Object::Int(n) => {
256 u32::try_from(*n).is_ok_and(|n| n == mcid) && on_page(element_page, page_ref)
257 }
258 Object::Dict(d) => is_mcr(d, mcid) && on_page(d.get_ref("Pg"), page_ref),
259 _ => false,
260 });
261 if let Some(index) = direct {
262 return u32::try_from(index).ok();
263 }
264 for (index, kid) in kids.iter().enumerate() {
265 let Some(r) = kid.as_ref() else {
266 continue;
267 };
268 let Some(dict) = self.dict(r).await else {
269 continue;
270 };
271 if is_mcr(&dict, mcid) && on_page(dict.get_ref("Pg"), page_ref) {
272 return u32::try_from(index).ok();
273 }
274 }
275 None
276 }
277}
278
279fn on_page(pg: Option<ObjRef>, page_ref: Option<ObjRef>) -> bool {
284 match (pg, page_ref) {
285 (Some(pg), Some(page)) => pg == page,
286 _ => true,
287 }
288}
289
290fn is_mcr(dict: &Dict, mcid: u32) -> bool {
292 dict.get_int("MCID")
293 .and_then(|n| u32::try_from(n).ok())
294 .is_some_and(|n| n == mcid)
295}
296
297fn kids_of(element: &Dict) -> Vec<Object> {
300 match element.get("K") {
301 Some(Object::Array(items)) => items.clone(),
302 Some(single) => vec![single.clone()],
303 None => Vec::new(),
304 }
305}
306
307fn kid_index(parent: &Dict, child: ObjRef) -> Option<u32> {
309 let index = kids_of(parent)
310 .iter()
311 .position(|kid| kid.as_ref() == Some(child))?;
312 u32::try_from(index).ok()
313}
314
315async fn resolved_dict<S: AsyncObjectSource>(src: &S, o: &Object) -> Option<Dict> {
317 match src.resolve(o).await.ok()? {
318 Object::Dict(dict) => Some(dict),
319 Object::Stream(stream) => Some(stream.dict),
320 _ => None,
321 }
322}
323
324async fn number_tree_lookup<S: AsyncObjectSource>(
329 src: &S,
330 root: &Dict,
331 key: i64,
332) -> Option<Object> {
333 let mut pending: Vec<Dict> = vec![root.clone()];
334 let mut visited = 0usize;
335 while let Some(node) = pending.pop() {
336 visited += 1;
337 if visited > MAX_NUMBER_TREE_NODES {
338 return None;
339 }
340 if let Some(nums) = node.get("Nums") {
341 if let Some(found) = leaf_value(src, nums, key).await {
342 return Some(found);
343 }
344 }
345 let Some(kids) = node.get("Kids") else {
346 continue;
347 };
348 let Ok(Object::Array(kids)) = src.resolve(kids).await else {
349 continue;
350 };
351 for kid in kids.iter().rev() {
352 let Some(kid) = resolved_dict(src, kid).await else {
353 continue;
354 };
355 if within_limits(src, &kid, key).await {
356 pending.push(kid);
357 }
358 }
359 }
360 None
361}
362
363async fn within_limits<S: AsyncObjectSource>(src: &S, node: &Dict, key: i64) -> bool {
366 let Some(limits) = node.get("Limits") else {
367 return true;
368 };
369 let Ok(Object::Array(limits)) = src.resolve(limits).await else {
370 return true;
371 };
372 match (bound(src, &limits, 0).await, bound(src, &limits, 1).await) {
373 (Some(lo), Some(hi)) => lo <= key && key <= hi,
374 _ => true,
375 }
376}
377
378async fn bound<S: AsyncObjectSource>(src: &S, limits: &[Object], i: usize) -> Option<i64> {
380 let o = limits.get(i)?;
381 src.resolve(o).await.ok()?.as_int()
382}
383
384async fn leaf_value<S: AsyncObjectSource>(src: &S, nums: &Object, key: i64) -> Option<Object> {
386 let Ok(Object::Array(pairs)) = src.resolve(nums).await else {
387 return None;
388 };
389 for [number, value] in pairs.as_chunks::<2>().0 {
390 let found = match number {
391 Object::Int(n) => *n == key,
392 other => src.resolve(other).await.ok().and_then(|v| v.as_int()) == Some(key),
393 };
394 if found {
395 return Some(value.clone());
396 }
397 }
398 None
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404 use crate::{block_on, Document, Immediate};
405 use pdfboss_testkit::PdfBuilder;
406
407 fn id(parents: u32, mcid: u32) -> MarkedContentId {
408 MarkedContentId { parents, mcid }
409 }
410
411 fn tagged_doc(page_extra: &str, objects: &[(u32, &str)]) -> Document {
415 let mut b = PdfBuilder::new();
416 b.object(
417 1,
418 "<< /Type /Catalog /Pages 2 0 R /StructTreeRoot 10 0 R >>",
419 );
420 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
421 b.object(
422 3,
423 &format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] {page_extra} >>"),
424 );
425 for (num, body) in objects {
426 b.object(*num, body);
427 }
428 Document::load(b.build(1)).expect("load")
429 }
430
431 fn two_paragraphs(parent_tree: &str) -> Document {
434 tagged_doc(
435 "/StructParents 0",
436 &[
437 (
438 10,
439 "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
440 ),
441 (
442 11,
443 "<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R 14 0 R] >>",
444 ),
445 (12, parent_tree),
446 (
447 13,
448 "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0 2] >>",
449 ),
450 (
451 14,
452 "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [1 3] >>",
453 ),
454 ],
455 )
456 }
457
458 fn ranks(doc: &Document, ids: &[MarkedContentId]) -> FastMap<MarkedContentId, u32> {
459 let tree = doc.structure_tree().expect("tree");
460 let page = doc.page(0).unwrap();
461 block_on(tree.ranks_with(&Immediate(doc), &page, ids))
462 }
463
464 #[test]
465 fn no_struct_tree_root_means_no_tree() {
466 let mut b = PdfBuilder::new();
467 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
468 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
469 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
470 let doc = Document::load(b.build(1)).unwrap();
471 assert!(doc.structure_tree().is_none());
472 }
473
474 #[test]
475 fn ranks_follow_the_tree_not_the_ids() {
476 let doc = two_paragraphs("<< /Nums [0 [13 0 R 14 0 R 13 0 R 14 0 R]] >>");
477 let ranks = ranks(&doc, &[id(0, 2), id(0, 3), id(0, 0), id(0, 1)]);
478 assert_eq!(ranks[&id(0, 0)], 0);
479 assert_eq!(ranks[&id(0, 2)], 1);
480 assert_eq!(ranks[&id(0, 1)], 2);
481 assert_eq!(ranks[&id(0, 3)], 3);
482 }
483
484 #[test]
485 fn parent_tree_kids_and_limits_are_descended() {
486 let doc = tagged_doc(
487 "/StructParents 7",
488 &[
489 (
490 10,
491 "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
492 ),
493 (
494 11,
495 "<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R] >>",
496 ),
497 (12, "<< /Kids [15 0 R 16 0 R] >>"),
498 (
499 13,
500 "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0] >>",
501 ),
502 (15, "<< /Limits [0 3] /Nums [0 [] 3 []] >>"),
503 (16, "<< /Limits [7 9] /Nums [7 [13 0 R] 9 []] >>"),
504 ],
505 );
506 let ranks = ranks(&doc, &[id(7, 0)]);
507 assert_eq!(ranks[&id(7, 0)], 0);
508 }
509
510 #[test]
511 fn a_page_without_a_parent_tree_entry_has_no_ranks() {
512 let doc = two_paragraphs("<< /Nums [5 [13 0 R]] >>");
513 assert!(ranks(&doc, &[id(0, 0), id(0, 1)]).is_empty());
514 }
515
516 #[test]
517 fn untagged_and_out_of_range_ids_are_absent() {
518 let doc = two_paragraphs("<< /Nums [0 [13 0 R 14 0 R 13 0 R 14 0 R]] >>");
519 let ranks = ranks(&doc, &[id(0, 0), id(0, 9), id(4, 0)]);
520 assert_eq!(ranks.len(), 1);
521 assert_eq!(ranks[&id(0, 0)], 0);
522 }
523
524 #[test]
525 fn a_broken_ancestry_leaves_only_that_element_unranked() {
526 let doc = tagged_doc(
527 "/StructParents 0",
528 &[
529 (
530 10,
531 "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
532 ),
533 (
534 11,
535 "<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R] >>",
536 ),
537 (12, "<< /Nums [0 [13 0 R 14 0 R]] >>"),
538 (
539 13,
540 "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0] >>",
541 ),
542 (
544 14,
545 "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [1] >>",
546 ),
547 ],
548 );
549 let ranks = ranks(&doc, &[id(0, 0), id(0, 1)]);
550 assert_eq!(ranks.len(), 1);
551 assert_eq!(ranks[&id(0, 0)], 0);
552 }
553
554 #[test]
555 fn marked_content_references_name_their_page() {
556 let doc = tagged_doc(
559 "/StructParents 0",
560 &[
561 (
562 10,
563 "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
564 ),
565 (
566 11,
567 "<< /Type /StructElem /S /P /P 10 0 R \
568 /K [<< /Type /MCR /Pg 99 0 R /MCID 0 >> 15 0 R] >>",
569 ),
570 (12, "<< /Nums [0 [11 0 R]] >>"),
571 (15, "<< /Type /MCR /Pg 3 0 R /MCID 0 >>"),
572 ],
573 );
574 let ranks = ranks(&doc, &[id(0, 0)]);
575 assert_eq!(ranks[&id(0, 0)], 0);
576 let tree = doc.structure_tree().unwrap();
577 let page = doc.page(0).unwrap();
578 let mut walk = Walk {
579 src: &Immediate(&doc),
580 page_ref: page.object_ref(),
581 root_ref: Some(ObjRef { num: 10, gen: 0 }),
582 dicts: FastMap::default(),
583 paths: FastMap::default(),
584 parents: FastMap::default(),
585 };
586 let element = block_on(walk.dict(ObjRef { num: 11, gen: 0 })).unwrap();
587 assert_eq!(block_on(walk.mcid_index(&element, 0)), Some(1));
588 drop(tree);
589 }
590
591 #[test]
592 fn a_bare_integer_on_another_page_is_not_this_page() {
593 let doc = tagged_doc(
594 "/StructParents 0",
595 &[
596 (
597 10,
598 "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
599 ),
600 (
601 11,
602 "<< /Type /StructElem /S /P /P 10 0 R /Pg 99 0 R /K [0] >>",
603 ),
604 (12, "<< /Nums [0 [11 0 R]] >>"),
605 ],
606 );
607 assert!(ranks(&doc, &[id(0, 0)]).is_empty());
608 }
609
610 #[test]
611 fn a_form_key_ranks_alongside_the_page() {
612 let doc = tagged_doc(
615 "/StructParents 0",
616 &[
617 (
618 10,
619 "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
620 ),
621 (
622 11,
623 "<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R 14 0 R] >>",
624 ),
625 (12, "<< /Nums [0 [13 0 R] 1 [14 0 R]] >>"),
626 (
627 13,
628 "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0] >>",
629 ),
630 (
631 14,
632 "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0] >>",
633 ),
634 ],
635 );
636 let ranks = ranks(&doc, &[id(1, 0), id(0, 0)]);
637 assert_eq!(ranks[&id(0, 0)], 0);
638 assert_eq!(ranks[&id(1, 0)], 1);
639 }
640
641 #[test]
642 fn a_single_kid_needs_no_array() {
643 let doc = tagged_doc(
644 "/StructParents 0",
645 &[
646 (
647 10,
648 "<< /Type /StructTreeRoot /K 11 0 R /ParentTree 12 0 R >>",
649 ),
650 (11, "<< /Type /StructElem /S /P /P 10 0 R /Pg 3 0 R /K 0 >>"),
651 (12, "<< /Nums [0 [11 0 R]] >>"),
652 ],
653 );
654 assert_eq!(ranks(&doc, &[id(0, 0)])[&id(0, 0)], 0);
655 }
656}