use std::sync::Arc;
use crate::document::Page;
use crate::hash::{FastMap, FastSet};
use crate::object::{Dict, ObjRef, Object};
use crate::source::AsyncObjectSource;
const MAX_ELEMENT_DEPTH: usize = 64;
const MAX_NUMBER_TREE_NODES: usize = 4096;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MarkedContentId {
pub parents: u32,
pub mcid: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StructureTree {
root: Dict,
root_ref: Option<ObjRef>,
}
impl StructureTree {
pub async fn load_with<S: AsyncObjectSource>(src: &S, trailer: &Dict) -> Option<StructureTree> {
let root = trailer.get("Root")?;
let catalog = src.resolve(root).await.ok()?;
let entry = catalog.as_dict()?.get("StructTreeRoot")?;
let root_ref = entry.as_ref();
let resolved = src.resolve(entry).await.ok()?;
let root = resolved.as_dict()?.clone();
Some(StructureTree { root, root_ref })
}
pub async fn ranks_with<S: AsyncObjectSource>(
&self,
src: &S,
page: &Page,
ids: &[MarkedContentId],
) -> FastMap<MarkedContentId, u32> {
let mut ranks: FastMap<MarkedContentId, u32> = FastMap::default();
let Some(parent_tree) = self.root.get("ParentTree") else {
return ranks;
};
let Some(parent_tree) = resolved_dict(src, parent_tree).await else {
return ranks;
};
let mut walk = Walk {
src,
page_ref: page.object_ref(),
root_ref: self.root_ref,
dicts: FastMap::default(),
paths: FastMap::default(),
parents: FastMap::default(),
};
let mut keyed: Vec<(MarkedContentId, Vec<u32>)> = Vec::new();
let mut seen: FastSet<MarkedContentId> = FastSet::default();
for id in ids {
if !seen.insert(*id) {
continue;
}
let Some(key) = walk.key_of(&parent_tree, *id).await else {
continue;
};
keyed.push((*id, key));
}
keyed.sort_by(|a, b| a.1.cmp(&b.1));
for (rank, (id, _)) in keyed.into_iter().enumerate() {
ranks.insert(id, rank as u32);
}
ranks
}
}
struct Walk<'a, S> {
src: &'a S,
page_ref: Option<ObjRef>,
root_ref: Option<ObjRef>,
dicts: FastMap<ObjRef, Option<Arc<Dict>>>,
paths: FastMap<ObjRef, Option<Arc<Vec<u32>>>>,
parents: FastMap<u32, Option<Arc<Vec<Object>>>>,
}
impl<S: AsyncObjectSource> Walk<'_, S> {
async fn key_of(&mut self, parent_tree: &Dict, id: MarkedContentId) -> Option<Vec<u32>> {
let elements = self.parent_array(parent_tree, id.parents).await?;
let element = elements.get(id.mcid as usize)?.as_ref()?;
let path = self.path_of(element).await?;
let dict = self.dict(element).await?;
let index = self.mcid_index(&dict, id.mcid).await?;
let mut key = Vec::with_capacity(path.len() + 1);
key.extend_from_slice(&path);
key.push(index);
Some(key)
}
async fn parent_array(&mut self, parent_tree: &Dict, key: u32) -> Option<Arc<Vec<Object>>> {
if let Some(cached) = self.parents.get(&key) {
return cached.clone();
}
let found = match number_tree_lookup(self.src, parent_tree, i64::from(key)).await {
Some(entry) => match self.src.resolve(&entry).await.ok()? {
Object::Array(items) => Some(Arc::new(items)),
_ => None,
},
None => None,
};
self.parents.insert(key, found.clone());
found
}
async fn dict(&mut self, r: ObjRef) -> Option<Arc<Dict>> {
if let Some(cached) = self.dicts.get(&r) {
return cached.clone();
}
let loaded = match self.src.get(r).await.ok()? {
Object::Dict(dict) => Some(Arc::new(dict)),
Object::Stream(stream) => Some(Arc::new(stream.dict)),
_ => None,
};
self.dicts.insert(r, loaded.clone());
loaded
}
async fn path_of(&mut self, element: ObjRef) -> Option<Arc<Vec<u32>>> {
if let Some(cached) = self.paths.get(&element) {
return cached.clone();
}
let mut chain: Vec<ObjRef> = vec![element];
let mut stop: Option<(ObjRef, bool, Arc<Vec<u32>>)> = None;
let mut current = element;
for _ in 0..MAX_ELEMENT_DEPTH {
let Some(dict) = self.dict(current).await else {
break;
};
let Some(parent) = dict.get("P").and_then(Object::as_ref) else {
break;
};
if self.is_root(parent).await {
stop = Some((parent, true, Arc::new(Vec::new())));
break;
}
if let Some(cached) = self.paths.get(&parent) {
stop = cached.clone().map(|path| (parent, false, path));
break;
}
chain.push(parent);
current = parent;
}
let Some((mut parent_ref, mut parent_is_root, known)) = stop else {
for r in chain {
self.paths.insert(r, None);
}
return None;
};
let mut path: Vec<u32> = (*known).clone();
let mut resolved: Option<Arc<Vec<u32>>> = None;
for child in chain.into_iter().rev() {
let index = if parent_is_root {
self.root_kid_index(child).await
} else {
match self.dict(parent_ref).await {
Some(parent) => kid_index(&parent, child),
None => None,
}
};
let Some(index) = index else {
self.paths.insert(child, None);
return None;
};
path.push(index);
let shared = Arc::new(path.clone());
self.paths.insert(child, Some(shared.clone()));
resolved = Some(shared);
parent_ref = child;
parent_is_root = false;
}
resolved
}
async fn is_root(&mut self, r: ObjRef) -> bool {
if self.root_ref == Some(r) {
return true;
}
let Some(dict) = self.dict(r).await else {
return false;
};
dict.get_name("Type")
.is_some_and(|n| n.0 == "StructTreeRoot")
}
async fn root_kid_index(&mut self, child: ObjRef) -> Option<u32> {
let root_ref = self.root_ref?;
let root = self.dict(root_ref).await?;
kid_index(&root, child)
}
async fn mcid_index(&mut self, element: &Dict, mcid: u32) -> Option<u32> {
let kids = kids_of(element);
let page_ref = self.page_ref;
let element_page = element.get_ref("Pg");
let direct = kids.iter().position(|kid| match kid {
Object::Int(n) => {
u32::try_from(*n).is_ok_and(|n| n == mcid) && on_page(element_page, page_ref)
}
Object::Dict(d) => is_mcr(d, mcid) && on_page(d.get_ref("Pg"), page_ref),
_ => false,
});
if let Some(index) = direct {
return u32::try_from(index).ok();
}
for (index, kid) in kids.iter().enumerate() {
let Some(r) = kid.as_ref() else {
continue;
};
let Some(dict) = self.dict(r).await else {
continue;
};
if is_mcr(&dict, mcid) && on_page(dict.get_ref("Pg"), page_ref) {
return u32::try_from(index).ok();
}
}
None
}
}
fn on_page(pg: Option<ObjRef>, page_ref: Option<ObjRef>) -> bool {
match (pg, page_ref) {
(Some(pg), Some(page)) => pg == page,
_ => true,
}
}
fn is_mcr(dict: &Dict, mcid: u32) -> bool {
dict.get_int("MCID")
.and_then(|n| u32::try_from(n).ok())
.is_some_and(|n| n == mcid)
}
fn kids_of(element: &Dict) -> Vec<Object> {
match element.get("K") {
Some(Object::Array(items)) => items.clone(),
Some(single) => vec![single.clone()],
None => Vec::new(),
}
}
fn kid_index(parent: &Dict, child: ObjRef) -> Option<u32> {
let index = kids_of(parent)
.iter()
.position(|kid| kid.as_ref() == Some(child))?;
u32::try_from(index).ok()
}
async fn resolved_dict<S: AsyncObjectSource>(src: &S, o: &Object) -> Option<Dict> {
match src.resolve(o).await.ok()? {
Object::Dict(dict) => Some(dict),
Object::Stream(stream) => Some(stream.dict),
_ => None,
}
}
async fn number_tree_lookup<S: AsyncObjectSource>(
src: &S,
root: &Dict,
key: i64,
) -> Option<Object> {
let mut pending: Vec<Dict> = vec![root.clone()];
let mut visited = 0usize;
while let Some(node) = pending.pop() {
visited += 1;
if visited > MAX_NUMBER_TREE_NODES {
return None;
}
if let Some(nums) = node.get("Nums") {
if let Some(found) = leaf_value(src, nums, key).await {
return Some(found);
}
}
let Some(kids) = node.get("Kids") else {
continue;
};
let Ok(Object::Array(kids)) = src.resolve(kids).await else {
continue;
};
for kid in kids.iter().rev() {
let Some(kid) = resolved_dict(src, kid).await else {
continue;
};
if within_limits(src, &kid, key).await {
pending.push(kid);
}
}
}
None
}
async fn within_limits<S: AsyncObjectSource>(src: &S, node: &Dict, key: i64) -> bool {
let Some(limits) = node.get("Limits") else {
return true;
};
let Ok(Object::Array(limits)) = src.resolve(limits).await else {
return true;
};
match (bound(src, &limits, 0).await, bound(src, &limits, 1).await) {
(Some(lo), Some(hi)) => lo <= key && key <= hi,
_ => true,
}
}
async fn bound<S: AsyncObjectSource>(src: &S, limits: &[Object], i: usize) -> Option<i64> {
let o = limits.get(i)?;
src.resolve(o).await.ok()?.as_int()
}
async fn leaf_value<S: AsyncObjectSource>(src: &S, nums: &Object, key: i64) -> Option<Object> {
let Ok(Object::Array(pairs)) = src.resolve(nums).await else {
return None;
};
for [number, value] in pairs.as_chunks::<2>().0 {
let found = match number {
Object::Int(n) => *n == key,
other => src.resolve(other).await.ok().and_then(|v| v.as_int()) == Some(key),
};
if found {
return Some(value.clone());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{block_on, Document, Immediate};
use pdfboss_testkit::PdfBuilder;
fn id(parents: u32, mcid: u32) -> MarkedContentId {
MarkedContentId { parents, mcid }
}
fn tagged_doc(page_extra: &str, objects: &[(u32, &str)]) -> Document {
let mut b = PdfBuilder::new();
b.object(
1,
"<< /Type /Catalog /Pages 2 0 R /StructTreeRoot 10 0 R >>",
);
b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
b.object(
3,
&format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] {page_extra} >>"),
);
for (num, body) in objects {
b.object(*num, body);
}
Document::load(b.build(1)).expect("load")
}
fn two_paragraphs(parent_tree: &str) -> Document {
tagged_doc(
"/StructParents 0",
&[
(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
),
(
11,
"<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R 14 0 R] >>",
),
(12, parent_tree),
(
13,
"<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0 2] >>",
),
(
14,
"<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [1 3] >>",
),
],
)
}
fn ranks(doc: &Document, ids: &[MarkedContentId]) -> FastMap<MarkedContentId, u32> {
let tree = doc.structure_tree().expect("tree");
let page = doc.page(0).unwrap();
block_on(tree.ranks_with(&Immediate(doc), &page, ids))
}
#[test]
fn no_struct_tree_root_means_no_tree() {
let mut b = PdfBuilder::new();
b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
let doc = Document::load(b.build(1)).unwrap();
assert!(doc.structure_tree().is_none());
}
#[test]
fn ranks_follow_the_tree_not_the_ids() {
let doc = two_paragraphs("<< /Nums [0 [13 0 R 14 0 R 13 0 R 14 0 R]] >>");
let ranks = ranks(&doc, &[id(0, 2), id(0, 3), id(0, 0), id(0, 1)]);
assert_eq!(ranks[&id(0, 0)], 0);
assert_eq!(ranks[&id(0, 2)], 1);
assert_eq!(ranks[&id(0, 1)], 2);
assert_eq!(ranks[&id(0, 3)], 3);
}
#[test]
fn parent_tree_kids_and_limits_are_descended() {
let doc = tagged_doc(
"/StructParents 7",
&[
(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
),
(
11,
"<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R] >>",
),
(12, "<< /Kids [15 0 R 16 0 R] >>"),
(
13,
"<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0] >>",
),
(15, "<< /Limits [0 3] /Nums [0 [] 3 []] >>"),
(16, "<< /Limits [7 9] /Nums [7 [13 0 R] 9 []] >>"),
],
);
let ranks = ranks(&doc, &[id(7, 0)]);
assert_eq!(ranks[&id(7, 0)], 0);
}
#[test]
fn a_page_without_a_parent_tree_entry_has_no_ranks() {
let doc = two_paragraphs("<< /Nums [5 [13 0 R]] >>");
assert!(ranks(&doc, &[id(0, 0), id(0, 1)]).is_empty());
}
#[test]
fn untagged_and_out_of_range_ids_are_absent() {
let doc = two_paragraphs("<< /Nums [0 [13 0 R 14 0 R 13 0 R 14 0 R]] >>");
let ranks = ranks(&doc, &[id(0, 0), id(0, 9), id(4, 0)]);
assert_eq!(ranks.len(), 1);
assert_eq!(ranks[&id(0, 0)], 0);
}
#[test]
fn a_broken_ancestry_leaves_only_that_element_unranked() {
let doc = tagged_doc(
"/StructParents 0",
&[
(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
),
(
11,
"<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R] >>",
),
(12, "<< /Nums [0 [13 0 R 14 0 R]] >>"),
(
13,
"<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0] >>",
),
(
14,
"<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [1] >>",
),
],
);
let ranks = ranks(&doc, &[id(0, 0), id(0, 1)]);
assert_eq!(ranks.len(), 1);
assert_eq!(ranks[&id(0, 0)], 0);
}
#[test]
fn marked_content_references_name_their_page() {
let doc = tagged_doc(
"/StructParents 0",
&[
(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
),
(
11,
"<< /Type /StructElem /S /P /P 10 0 R \
/K [<< /Type /MCR /Pg 99 0 R /MCID 0 >> 15 0 R] >>",
),
(12, "<< /Nums [0 [11 0 R]] >>"),
(15, "<< /Type /MCR /Pg 3 0 R /MCID 0 >>"),
],
);
let ranks = ranks(&doc, &[id(0, 0)]);
assert_eq!(ranks[&id(0, 0)], 0);
let tree = doc.structure_tree().unwrap();
let page = doc.page(0).unwrap();
let mut walk = Walk {
src: &Immediate(&doc),
page_ref: page.object_ref(),
root_ref: Some(ObjRef { num: 10, gen: 0 }),
dicts: FastMap::default(),
paths: FastMap::default(),
parents: FastMap::default(),
};
let element = block_on(walk.dict(ObjRef { num: 11, gen: 0 })).unwrap();
assert_eq!(block_on(walk.mcid_index(&element, 0)), Some(1));
drop(tree);
}
#[test]
fn a_bare_integer_on_another_page_is_not_this_page() {
let doc = tagged_doc(
"/StructParents 0",
&[
(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
),
(
11,
"<< /Type /StructElem /S /P /P 10 0 R /Pg 99 0 R /K [0] >>",
),
(12, "<< /Nums [0 [11 0 R]] >>"),
],
);
assert!(ranks(&doc, &[id(0, 0)]).is_empty());
}
#[test]
fn a_form_key_ranks_alongside_the_page() {
let doc = tagged_doc(
"/StructParents 0",
&[
(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
),
(
11,
"<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R 14 0 R] >>",
),
(12, "<< /Nums [0 [13 0 R] 1 [14 0 R]] >>"),
(
13,
"<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0] >>",
),
(
14,
"<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0] >>",
),
],
);
let ranks = ranks(&doc, &[id(1, 0), id(0, 0)]);
assert_eq!(ranks[&id(0, 0)], 0);
assert_eq!(ranks[&id(1, 0)], 1);
}
#[test]
fn a_single_kid_needs_no_array() {
let doc = tagged_doc(
"/StructParents 0",
&[
(
10,
"<< /Type /StructTreeRoot /K 11 0 R /ParentTree 12 0 R >>",
),
(11, "<< /Type /StructElem /S /P /P 10 0 R /Pg 3 0 R /K 0 >>"),
(12, "<< /Nums [0 [11 0 R]] >>"),
],
);
assert_eq!(ranks(&doc, &[id(0, 0)])[&id(0, 0)], 0);
}
}