use std::sync::{Arc, Mutex};
use crate::document::Page;
use crate::hash::{FastMap, FastSet};
use crate::object::{Dict, ObjRef, Object};
use crate::source::AsyncObjectSource;
use crate::tree::resolved_dict;
const MAX_ELEMENT_DEPTH: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MarkedContentId {
pub parents: u32,
pub mcid: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ContentItem {
Sequence(MarkedContentId),
Object(ObjRef),
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlacedItem {
pub item: ContentItem,
pub placement: Placement,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StandardKind {
Grouping,
BlockLevel,
InlineLevel,
Illustration,
}
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StandardType {
Document,
Part,
Art,
Sect,
Div,
BlockQuote,
Caption,
TOC,
TOCI,
Index,
NonStruct,
Private,
P,
H,
H1,
H2,
H3,
H4,
H5,
H6,
L,
LI,
Lbl,
LBody,
Table,
TR,
TH,
TD,
THead,
TBody,
TFoot,
Span,
Quote,
Note,
Reference,
BibEntry,
Code,
Link,
Annot,
Ruby,
RB,
RT,
RP,
Warichu,
WT,
WP,
Figure,
Formula,
Form,
}
impl StandardType {
pub const ALL: [StandardType; 49] = [
StandardType::Document,
StandardType::Part,
StandardType::Art,
StandardType::Sect,
StandardType::Div,
StandardType::BlockQuote,
StandardType::Caption,
StandardType::TOC,
StandardType::TOCI,
StandardType::Index,
StandardType::NonStruct,
StandardType::Private,
StandardType::P,
StandardType::H,
StandardType::H1,
StandardType::H2,
StandardType::H3,
StandardType::H4,
StandardType::H5,
StandardType::H6,
StandardType::L,
StandardType::LI,
StandardType::Lbl,
StandardType::LBody,
StandardType::Table,
StandardType::TR,
StandardType::TH,
StandardType::TD,
StandardType::THead,
StandardType::TBody,
StandardType::TFoot,
StandardType::Span,
StandardType::Quote,
StandardType::Note,
StandardType::Reference,
StandardType::BibEntry,
StandardType::Code,
StandardType::Link,
StandardType::Annot,
StandardType::Ruby,
StandardType::RB,
StandardType::RT,
StandardType::RP,
StandardType::Warichu,
StandardType::WT,
StandardType::WP,
StandardType::Figure,
StandardType::Formula,
StandardType::Form,
];
pub fn from_name(name: &str) -> Option<StandardType> {
StandardType::ALL.into_iter().find(|t| t.name() == name)
}
pub fn name(self) -> &'static str {
match self {
StandardType::Document => "Document",
StandardType::Part => "Part",
StandardType::Art => "Art",
StandardType::Sect => "Sect",
StandardType::Div => "Div",
StandardType::BlockQuote => "BlockQuote",
StandardType::Caption => "Caption",
StandardType::TOC => "TOC",
StandardType::TOCI => "TOCI",
StandardType::Index => "Index",
StandardType::NonStruct => "NonStruct",
StandardType::Private => "Private",
StandardType::P => "P",
StandardType::H => "H",
StandardType::H1 => "H1",
StandardType::H2 => "H2",
StandardType::H3 => "H3",
StandardType::H4 => "H4",
StandardType::H5 => "H5",
StandardType::H6 => "H6",
StandardType::L => "L",
StandardType::LI => "LI",
StandardType::Lbl => "Lbl",
StandardType::LBody => "LBody",
StandardType::Table => "Table",
StandardType::TR => "TR",
StandardType::TH => "TH",
StandardType::TD => "TD",
StandardType::THead => "THead",
StandardType::TBody => "TBody",
StandardType::TFoot => "TFoot",
StandardType::Span => "Span",
StandardType::Quote => "Quote",
StandardType::Note => "Note",
StandardType::Reference => "Reference",
StandardType::BibEntry => "BibEntry",
StandardType::Code => "Code",
StandardType::Link => "Link",
StandardType::Annot => "Annot",
StandardType::Ruby => "Ruby",
StandardType::RB => "RB",
StandardType::RT => "RT",
StandardType::RP => "RP",
StandardType::Warichu => "Warichu",
StandardType::WT => "WT",
StandardType::WP => "WP",
StandardType::Figure => "Figure",
StandardType::Formula => "Formula",
StandardType::Form => "Form",
}
}
pub fn kind(self) -> StandardKind {
match self {
StandardType::Document
| StandardType::Part
| StandardType::Art
| StandardType::Sect
| StandardType::Div
| StandardType::BlockQuote
| StandardType::Caption
| StandardType::TOC
| StandardType::TOCI
| StandardType::Index
| StandardType::NonStruct
| StandardType::Private => StandardKind::Grouping,
StandardType::P
| StandardType::H
| StandardType::H1
| StandardType::H2
| StandardType::H3
| StandardType::H4
| StandardType::H5
| StandardType::H6
| StandardType::L
| StandardType::LI
| StandardType::Lbl
| StandardType::LBody
| StandardType::Table
| StandardType::TR
| StandardType::TH
| StandardType::TD
| StandardType::THead
| StandardType::TBody
| StandardType::TFoot => StandardKind::BlockLevel,
StandardType::Span
| StandardType::Quote
| StandardType::Note
| StandardType::Reference
| StandardType::BibEntry
| StandardType::Code
| StandardType::Link
| StandardType::Annot
| StandardType::Ruby
| StandardType::RB
| StandardType::RT
| StandardType::RP
| StandardType::Warichu
| StandardType::WT
| StandardType::WP => StandardKind::InlineLevel,
StandardType::Figure | StandardType::Formula | StandardType::Form => {
StandardKind::Illustration
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StructureElement {
pub standard_type: StandardType,
pub object: ObjRef,
pub revision: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StandardOwner {
Layout,
List,
PrintField,
Table,
Xml100,
Html32,
Html401,
Oeb10,
Rtf105,
Css100,
Css200,
}
impl StandardOwner {
pub const ALL: [StandardOwner; 11] = [
StandardOwner::Layout,
StandardOwner::List,
StandardOwner::PrintField,
StandardOwner::Table,
StandardOwner::Xml100,
StandardOwner::Html32,
StandardOwner::Html401,
StandardOwner::Oeb10,
StandardOwner::Rtf105,
StandardOwner::Css100,
StandardOwner::Css200,
];
pub fn from_name(name: &str) -> Option<StandardOwner> {
StandardOwner::ALL
.into_iter()
.find(|owner| owner.name() == name)
}
pub fn name(self) -> &'static str {
match self {
StandardOwner::Layout => "Layout",
StandardOwner::List => "List",
StandardOwner::PrintField => "PrintField",
StandardOwner::Table => "Table",
StandardOwner::Xml100 => "XML-1.00",
StandardOwner::Html32 => "HTML-3.2",
StandardOwner::Html401 => "HTML-4.01",
StandardOwner::Oeb10 => "OEB-1.0",
StandardOwner::Rtf105 => "RTF-1.05",
StandardOwner::Css100 => "CSS-1.00",
StandardOwner::Css200 => "CSS-2.00",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AttributeObject {
pub element: ObjRef,
pub owner: String,
pub revision: i64,
pub entries: Dict,
}
impl AttributeObject {
pub fn standard_owner(&self) -> Option<StandardOwner> {
StandardOwner::from_name(&self.owner)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Placement {
pub rank: u32,
pub structure_type: Option<String>,
pub mapped_type: Option<String>,
pub standard_type: Option<StandardType>,
pub path: Vec<StructureElement>,
pub alt: Option<String>,
pub lang: Option<String>,
pub expansion: Option<String>,
pub attributes: Vec<AttributeObject>,
}
impl Placement {
pub fn attribute(&self, element: ObjRef, owner: &str, key: &str) -> Option<&Object> {
self.attributes
.iter()
.rev()
.filter(|a| a.element == element && a.owner == owner)
.find_map(|a| a.entries.get(key))
}
}
#[derive(Debug)]
pub struct StructureTree {
root: Arc<Dict>,
root_ref: Option<ObjRef>,
caches: Mutex<Caches>,
role_map: FastMap<String, String>,
class_map: FastMap<String, Object>,
parent_entries: Arc<FastMap<i64, Object>>,
}
impl Clone for StructureTree {
fn clone(&self) -> StructureTree {
StructureTree {
root: Arc::clone(&self.root),
root_ref: self.root_ref,
caches: Mutex::new(Caches::default()),
role_map: self.role_map.clone(),
class_map: self.class_map.clone(),
parent_entries: Arc::clone(&self.parent_entries),
}
}
}
impl PartialEq for StructureTree {
fn eq(&self, other: &StructureTree) -> bool {
self.root == other.root
&& self.root_ref == other.root_ref
&& self.role_map == other.role_map
&& self.class_map == other.class_map
&& self.parent_entries == other.parent_entries
}
}
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();
let role_map = match root.get("RoleMap") {
Some(entry) => role_map_of(resolved_dict(src, entry).await),
None => FastMap::default(),
};
let class_map = match root.get("ClassMap") {
Some(entry) => class_map_of(resolved_dict(src, entry).await),
None => FastMap::default(),
};
let parent_entries = match root.get("ParentTree") {
Some(entry) => parent_entries_of(src, resolved_dict(src, entry).await).await,
None => FastMap::default(),
};
Some(StructureTree {
root: Arc::new(root),
root_ref,
caches: Mutex::new(Caches::default()),
role_map,
class_map,
parent_entries: Arc::new(parent_entries),
})
}
pub fn mapped_type(&self, name: &str) -> String {
let mut seen: FastSet<&str> = FastSet::default();
let mut current = name;
while let Some(next) = self.role_map.get(current) {
if !seen.insert(current) {
return name.to_string();
}
current = next;
}
current.to_string()
}
pub async fn ranks_with<S: AsyncObjectSource>(
&self,
src: &S,
page: &Page,
ids: &[MarkedContentId],
) -> FastMap<MarkedContentId, u32> {
self.place_with(src, page, ids)
.await
.into_iter()
.map(|(id, placement)| (id, placement.rank))
.collect()
}
pub async fn place_with<S: AsyncObjectSource>(
&self,
src: &S,
page: &Page,
ids: &[MarkedContentId],
) -> FastMap<MarkedContentId, Placement> {
let items: Vec<ContentItem> = ids.iter().copied().map(ContentItem::Sequence).collect();
self.place_items_with(src, page, &items)
.await
.into_iter()
.filter_map(|(item, placement)| match item {
ContentItem::Sequence(id) => Some((id, placement)),
ContentItem::Object(_) => None,
})
.collect()
}
pub async fn content_items_with<S: AsyncObjectSource>(
&self,
src: &S,
page: &Page,
) -> Vec<PlacedItem> {
let mut walk = self.walk(src, page);
let mut items: Vec<ContentItem> = Vec::new();
let page_dict = page.dict();
let parents = page_dict
.get_int("StructParents")
.and_then(|n| u32::try_from(n).ok());
if let Some(parents) = parents {
if let Some(entries) = walk.parent_array(parents).await {
items.extend(
entries
.iter()
.enumerate()
.filter(|(_, entry)| entry.as_ref().is_some())
.map(|(mcid, _)| {
ContentItem::Sequence(MarkedContentId {
parents,
mcid: mcid as u32,
})
}),
);
}
}
for r in annotation_refs(src, page_dict).await {
let Some(dict) = walk.dict(r).await else {
continue;
};
if dict.get("StructParent").is_some() {
items.push(ContentItem::Object(r));
}
}
let placed_items = walk.place(self, &items).await;
self.store(walk);
let mut placed: Vec<PlacedItem> = placed_items
.into_iter()
.map(|(item, placement)| PlacedItem { item, placement })
.collect();
placed.sort_by_key(|placed| placed.placement.rank);
placed
}
pub async fn content_item_counts_with<S: AsyncObjectSource>(
&self,
src: &S,
page: &Page,
) -> (usize, usize) {
let page_dict = page.dict();
let mut sequences = 0;
let parents = page_dict
.get_int("StructParents")
.and_then(|key| self.parent_entries.get(&key));
if let Some(entry) = parents {
if let Ok(Object::Array(entries)) = src.resolve(entry).await {
sequences = entries.iter().filter(|e| e.as_ref().is_some()).count();
}
}
let mut objects = 0;
for r in annotation_refs(src, page_dict).await {
let Ok(Object::Dict(dict)) = src.get(r).await else {
continue;
};
let filed = dict
.get_int("StructParent")
.is_some_and(|key| self.parent_entries.contains_key(&key));
objects += usize::from(filed);
}
(sequences, objects)
}
pub async fn place_items_with<S: AsyncObjectSource>(
&self,
src: &S,
page: &Page,
items: &[ContentItem],
) -> FastMap<ContentItem, Placement> {
let mut walk = self.walk(src, page);
let placed = walk.place(self, items).await;
self.store(walk);
placed
}
fn walk<'a, S: AsyncObjectSource>(&'a self, src: &'a S, page: &Page) -> Walk<'a, S> {
let caches = std::mem::take(&mut *self.caches.lock().unwrap_or_else(|e| e.into_inner()));
Walk {
src,
page_ref: page.object_ref(),
root_ref: self.root_ref,
class_map: &self.class_map,
parent_entries: &self.parent_entries,
caches,
}
}
fn store<S>(&self, walk: Walk<'_, S>) {
let mut caches = self.caches.lock().unwrap_or_else(|e| e.into_inner());
if caches.dicts.is_empty() {
*caches = walk.caches;
return;
}
caches.dicts.extend(walk.caches.dicts);
caches.paths.extend(walk.caches.paths);
caches.parents.extend(walk.caches.parents);
caches.ancestries.extend(walk.caches.ancestries);
}
}
impl<S: AsyncObjectSource> Walk<'_, S> {
async fn place(
&mut self,
tree: &StructureTree,
items: &[ContentItem],
) -> FastMap<ContentItem, Placement> {
let mut placed: FastMap<ContentItem, Placement> = FastMap::default();
let mut keyed: Vec<(ContentItem, Vec<u32>, Arc<Ancestry>)> = Vec::new();
let mut seen: FastSet<ContentItem> = FastSet::default();
for item in items {
if !seen.insert(*item) {
continue;
}
let found = match item {
ContentItem::Sequence(id) => self.key_of(*id).await,
ContentItem::Object(r) => self.object_key_of(*r).await,
};
let Some((key, ancestry)) = found else {
continue;
};
keyed.push((*item, key, ancestry));
}
keyed.sort_by(|a, b| a.1.cmp(&b.1));
for (rank, (id, _, ancestry)) in keyed.into_iter().enumerate() {
let structure_type = ancestry.last().and_then(|a| a.structure_type.clone());
let mapped_type = structure_type.as_deref().map(|s| tree.mapped_type(s));
let standard_type = mapped_type.as_deref().and_then(StandardType::from_name);
let path = ancestry
.iter()
.filter_map(|ancestor| {
let name = ancestor.structure_type.as_deref()?;
let standard_type = StandardType::from_name(&tree.mapped_type(name))?;
Some(StructureElement {
standard_type,
object: ancestor.object,
revision: ancestor.revision,
})
})
.collect();
let alt = ancestry.iter().rev().find_map(|a| a.alt.clone());
let lang = ancestry.iter().rev().find_map(|a| a.lang.clone());
let expansion = ancestry.iter().rev().find_map(|a| a.expansion.clone());
let attributes = ancestry
.iter()
.flat_map(|ancestor| ancestor.attributes.iter().cloned())
.collect();
placed.insert(
id,
Placement {
rank: rank as u32,
structure_type,
mapped_type,
standard_type,
path,
alt,
lang,
expansion,
attributes,
},
);
}
placed
}
}
#[derive(Debug, Default)]
struct Caches {
dicts: FastMap<ObjRef, Option<Arc<Dict>>>,
paths: FastMap<ObjRef, Option<Arc<Vec<u32>>>>,
parents: FastMap<u32, Option<Arc<Vec<Object>>>>,
ancestries: FastMap<ObjRef, Arc<Ancestry>>,
}
#[derive(Debug)]
struct Ancestor {
object: ObjRef,
structure_type: Option<String>,
alt: Option<String>,
lang: Option<String>,
expansion: Option<String>,
revision: i64,
attributes: Vec<AttributeObject>,
}
type Ancestry = Vec<Ancestor>;
fn class_map_of(dict: Option<Dict>) -> FastMap<String, Object> {
let Some(dict) = dict else {
return FastMap::default();
};
dict.iter()
.map(|(key, value)| (key.0.clone(), value.clone()))
.collect()
}
fn attribute_object(element: ObjRef, entries: Dict) -> AttributeObject {
AttributeObject {
element,
owner: entries
.get_name("O")
.map(|n| n.0.clone())
.unwrap_or_default(),
revision: 0,
entries,
}
}
async fn parent_entries_of<S: AsyncObjectSource>(
src: &S,
dict: Option<Dict>,
) -> FastMap<i64, Object> {
let Some(dict) = dict else {
return FastMap::default();
};
let mut entries: FastMap<i64, Object> = FastMap::default();
for (key, value) in crate::tree::entries::<i64, S>(src, &dict).await {
entries.entry(key).or_insert(value);
}
entries
}
fn role_map_of(dict: Option<Dict>) -> FastMap<String, String> {
let Some(dict) = dict else {
return FastMap::default();
};
dict.iter()
.filter_map(|(key, value)| Some((key.0.clone(), value.as_name()?.0.clone())))
.collect()
}
struct Walk<'a, S> {
src: &'a S,
page_ref: Option<ObjRef>,
root_ref: Option<ObjRef>,
class_map: &'a FastMap<String, Object>,
parent_entries: &'a FastMap<i64, Object>,
caches: Caches,
}
impl<S: AsyncObjectSource> Walk<'_, S> {
async fn key_of(&mut self, id: MarkedContentId) -> Option<(Vec<u32>, Arc<Ancestry>)> {
let elements = self.parent_array(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).await?;
let mut key = Vec::with_capacity(path.len() + 1);
key.extend_from_slice(&path);
key.push(index);
Some((key, self.ancestry(element).await))
}
async fn object_key_of(&mut self, object: ObjRef) -> Option<(Vec<u32>, Arc<Ancestry>)> {
let dict = self.dict(object).await?;
let key = u32::try_from(dict.get_int("StructParent")?).ok()?;
let element = self.parent_element(key)?;
let path = self.path_of(element).await?;
let dict = self.dict(element).await?;
let index = self.object_index(&dict, object).await?;
let mut key = Vec::with_capacity(path.len() + 1);
key.extend_from_slice(&path);
key.push(index);
Some((key, self.ancestry(element).await))
}
fn parent_element(&self, key: u32) -> Option<ObjRef> {
self.parent_entries
.get(&i64::from(key))
.and_then(Object::as_ref)
}
async fn object_index(&mut self, element: &Dict, object: ObjRef) -> Option<u32> {
let kids = kids_of(element);
let page_ref = self.page_ref;
let element_page = element.get_ref("Pg");
let refers = |d: &Dict| {
d.get_ref("Obj") == Some(object) && on_page(d.get_ref("Pg").or(element_page), page_ref)
};
let direct = kids.iter().position(|kid| match kid {
Object::Dict(d) => refers(d),
_ => 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 refers(&dict) {
return u32::try_from(index).ok();
}
}
None
}
async fn is_reference_for(
&mut self,
dict: &Dict,
id: MarkedContentId,
element_page: Option<ObjRef>,
) -> bool {
if !is_mcr(dict, id.mcid) || !on_page(dict.get_ref("Pg").or(element_page), self.page_ref) {
return false;
}
let Some(stream) = dict.get_ref("Stm") else {
return true;
};
let Some(stream) = self.dict(stream).await else {
return true;
};
match stream.get_int("StructParents") {
Some(parents) => u32::try_from(parents).is_ok_and(|p| p == id.parents),
None => true,
}
}
async fn ancestry(&mut self, element: ObjRef) -> Arc<Ancestry> {
if let Some(cached) = self.caches.ancestries.get(&element) {
return cached.clone();
}
let chain = Arc::new(self.climb(element).await);
self.caches.ancestries.insert(element, chain.clone());
chain
}
async fn climb(&mut self, element: ObjRef) -> Ancestry {
let mut chain: Ancestry = Vec::new();
let mut current = element;
for _ in 0..MAX_ELEMENT_DEPTH {
let Some(dict) = self.dict(current).await else {
break;
};
let alt = match dict.get("Alt") {
Some(alt) => self.text_string(alt).await,
None => None,
};
let lang = match dict.get("Lang") {
Some(lang) => self.text_string(lang).await,
None => None,
};
let expansion = match dict.get("E") {
Some(expansion) => self.text_string(expansion).await,
None => None,
};
let attributes = self.attribute_objects(current, &dict).await;
chain.push(Ancestor {
object: current,
structure_type: dict.get_name("S").map(|n| n.0.clone()),
alt,
lang,
expansion,
revision: dict.get_int("R").unwrap_or(0),
attributes,
});
let Some(parent) = dict.get("P").and_then(Object::as_ref) else {
break;
};
if self.is_root(parent).await {
break;
}
current = parent;
}
chain.reverse();
chain
}
async fn attribute_objects(&mut self, element: ObjRef, dict: &Dict) -> Vec<AttributeObject> {
let mut out = Vec::new();
if let Some(classes) = dict.get("C") {
self.class_attributes(element, classes, &mut out).await;
}
if let Some(direct) = dict.get("A") {
self.direct_attributes(element, direct, &mut out).await;
}
out
}
async fn direct_attributes(
&mut self,
element: ObjRef,
value: &Object,
out: &mut Vec<AttributeObject>,
) {
let Ok(resolved) = self.src.resolve(value).await else {
return;
};
let items = match resolved {
Object::Dict(dict) => {
out.push(attribute_object(element, dict));
return;
}
Object::Array(items) => items,
_ => return,
};
let start = out.len();
for item in items {
if let Some(revision) = item.as_int() {
if out.len() > start {
if let Some(last) = out.last_mut() {
last.revision = revision;
}
}
continue;
}
let Ok(resolved) = self.src.resolve(&item).await else {
continue;
};
if let Object::Dict(dict) = resolved {
out.push(attribute_object(element, dict));
}
}
}
async fn class_attributes(
&mut self,
element: ObjRef,
value: &Object,
out: &mut Vec<AttributeObject>,
) {
let Ok(resolved) = self.src.resolve(value).await else {
return;
};
let names = match resolved {
Object::Name(_) => vec![resolved],
Object::Array(items) => items,
_ => return,
};
let mut added = 0..0;
for item in names {
if let Some(revision) = item.as_int() {
for attribute in &mut out[added.clone()] {
attribute.revision = revision;
}
continue;
}
let Some(name) = item.as_name() else {
continue;
};
let Some(class) = self.class_map.get(&name.0).cloned() else {
continue;
};
let start = out.len();
self.direct_attributes(element, &class, out).await;
added = start..out.len();
}
}
async fn text_string(&mut self, value: &Object) -> Option<String> {
let resolved = self.src.resolve(value).await.ok()?;
Some(crate::object::decode_text_string(resolved.as_str_bytes()?))
}
async fn parent_array(&mut self, key: u32) -> Option<Arc<Vec<Object>>> {
if let Some(cached) = self.caches.parents.get(&key) {
return cached.clone();
}
let found = match self.parent_entries.get(&i64::from(key)) {
Some(entry) => match self.src.resolve(entry).await.ok()? {
Object::Array(items) => Some(Arc::new(items)),
_ => None,
},
None => None,
};
self.caches.parents.insert(key, found.clone());
found
}
async fn dict(&mut self, r: ObjRef) -> Option<Arc<Dict>> {
if let Some(cached) = self.caches.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.caches.dicts.insert(r, loaded.clone());
loaded
}
async fn path_of(&mut self, element: ObjRef) -> Option<Arc<Vec<u32>>> {
if let Some(cached) = self.caches.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.caches.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.caches.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.caches.paths.insert(child, None);
return None;
};
path.push(index);
let shared = Arc::new(path.clone());
self.caches.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, id: MarkedContentId) -> Option<u32> {
let kids = kids_of(element);
let page_ref = self.page_ref;
let element_page = element.get_ref("Pg");
for (index, kid) in kids.iter().enumerate() {
let found = match kid {
Object::Int(n) => {
u32::try_from(*n).is_ok_and(|n| n == id.mcid) && on_page(element_page, page_ref)
}
Object::Dict(d) => self.is_reference_for(d, id, element_page).await,
_ => false,
};
if found {
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 self.is_reference_for(&dict, id, element_page).await {
return u32::try_from(index).ok();
}
}
None
}
}
async fn annotation_refs<S: AsyncObjectSource>(src: &S, page: &Dict) -> Vec<ObjRef> {
let Some(annots) = page.get("Annots") else {
return Vec::new();
};
let Ok(Object::Array(items)) = src.resolve(annots).await else {
return Vec::new();
};
items.iter().filter_map(Object::as_ref).collect()
}
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) -> &[Object] {
match element.get("K") {
Some(Object::Array(items)) => items,
Some(single) => std::slice::from_ref(single),
None => &[],
}
}
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()
}
#[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))
}
fn placements(doc: &Document, ids: &[MarkedContentId]) -> FastMap<MarkedContentId, Placement> {
let tree = doc.structure_tree().expect("tree");
let page = doc.page(0).unwrap();
block_on(tree.place_with(&Immediate(doc), &page, ids))
}
#[test]
fn structure_types_are_read_and_mapped_through_the_role_map() {
let doc = tagged_doc(
"/StructParents 0",
&[
(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R /RoleMap 20 0 R >>",
),
(
11,
"<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R 14 0 R 15 0 R 16 0 R] >>",
),
(12, "<< /Nums [0 [13 0 R 14 0 R 15 0 R 16 0 R]] >>"),
(
13,
"<< /Type /StructElem /S /Para /P 11 0 R /Pg 3 0 R /K [0] >>",
),
(
14,
"<< /Type /StructElem /S /Sub /P 11 0 R /Pg 3 0 R /K [1] >>",
),
(
15,
"<< /Type /StructElem /S /Loop1 /P 11 0 R /Pg 3 0 R /K [2] >>",
),
(
16,
"<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [3] >>",
),
(
20,
"<< /Para /P /Sub /Head /Head /H1 /Loop1 /Loop2 /Loop2 /Loop1 >>",
),
],
);
let placed = placements(&doc, &[id(0, 3), id(0, 2), id(0, 1), id(0, 0)]);
let typed = |n: u32| {
let p = &placed[&id(0, n)];
(p.structure_type.as_deref(), p.mapped_type.as_deref())
};
assert_eq!(typed(0), (Some("Para"), Some("P")));
assert_eq!(typed(1), (Some("Sub"), Some("H1")));
assert_eq!(typed(2), (Some("Loop1"), Some("Loop1")));
assert_eq!(typed(3), (Some("P"), Some("P")));
assert_eq!(placed[&id(0, 3)].rank, 3);
assert_eq!(placed[&id(0, 0)].standard_type, Some(StandardType::P));
assert_eq!(placed[&id(0, 1)].standard_type, Some(StandardType::H1));
assert_eq!(placed[&id(0, 2)].standard_type, None);
let tree = doc.structure_tree().unwrap();
assert_eq!(tree.mapped_type("Head"), "H1");
assert_eq!(tree.mapped_type("Span"), "Span");
}
#[test]
fn an_element_without_a_type_places_with_none() {
let doc = tagged_doc(
"/StructParents 0",
&[
(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
),
(11, "<< /Type /StructElem /P 10 0 R /Pg 3 0 R /K [0] >>"),
(12, "<< /Nums [0 [11 0 R]] >>"),
],
);
let placed = placements(&doc, &[id(0, 0)]);
assert_eq!(placed[&id(0, 0)].rank, 0);
assert_eq!(placed[&id(0, 0)].structure_type, None);
assert_eq!(placed[&id(0, 0)].mapped_type, None);
assert_eq!(placed[&id(0, 0)].standard_type, None);
}
#[test]
fn placements_carry_the_standard_typed_ancestry() {
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 15 0 R 20 0 R] >>",
),
(12, "<< /Nums [0 [14 0 R 17 0 R 19 0 R 21 0 R]] >>"),
(13, "<< /Type /StructElem /S /Sect /P 11 0 R /K [14 0 R] >>"),
(
14,
"<< /Type /StructElem /S /H /P 13 0 R /Pg 3 0 R /K [0] >>",
),
(15, "<< /Type /StructElem /S /L /P 11 0 R /K [16 0 R] >>"),
(
16,
"<< /Type /StructElem /S /LI /P 15 0 R /K [17 0 R 18 0 R] >>",
),
(
17,
"<< /Type /StructElem /S /Lbl /P 16 0 R /Pg 3 0 R /K [1] >>",
),
(
18,
"<< /Type /StructElem /S /LBody /P 16 0 R /K [19 0 R] >>",
),
(
19,
"<< /Type /StructElem /S /P /P 18 0 R /Pg 3 0 R /K [2] >>",
),
(
20,
"<< /Type /StructElem /S /Sidebar /P 11 0 R /K [21 0 R] >>",
),
(
21,
"<< /Type /StructElem /S /P /P 20 0 R /Pg 3 0 R /K [3] >>",
),
],
);
let placed = placements(&doc, &[id(0, 0), id(0, 1), id(0, 2), id(0, 3)]);
let kinds = |n: u32| -> Vec<StandardType> {
placed[&id(0, n)]
.path
.iter()
.map(|e| e.standard_type)
.collect()
};
let objects = |n: u32| -> Vec<u32> {
placed[&id(0, n)]
.path
.iter()
.map(|e| e.object.num)
.collect()
};
assert_eq!(
kinds(0),
[StandardType::Document, StandardType::Sect, StandardType::H]
);
assert_eq!(objects(0), [11, 13, 14]);
assert_eq!(
kinds(1),
[
StandardType::Document,
StandardType::L,
StandardType::LI,
StandardType::Lbl
]
);
assert_eq!(
kinds(2),
[
StandardType::Document,
StandardType::L,
StandardType::LI,
StandardType::LBody,
StandardType::P
]
);
assert_eq!(kinds(3), [StandardType::Document, StandardType::P]);
assert_eq!(objects(3), [11, 21]);
}
#[test]
fn placements_carry_the_nearest_alternate_description() {
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 15 0 R 16 0 R] >>",
),
(12, "<< /Nums [0 [14 0 R 15 0 R 16 0 R]] >>"),
(
13,
"<< /Type /StructElem /S /Figure /P 11 0 R /Alt (A chart) /K [14 0 R] >>",
),
(
14,
"<< /Type /StructElem /S /P /P 13 0 R /Pg 3 0 R /K [0] >>",
),
(
15,
"<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [1] >>",
),
(
16,
"<< /Type /StructElem /S /Span /P 11 0 R /Pg 3 0 R /K [2] /Alt <FEFF00E9> >>",
),
],
);
let placed = placements(&doc, &[id(0, 0), id(0, 1), id(0, 2)]);
assert_eq!(placed[&id(0, 0)].alt.as_deref(), Some("A chart"));
assert_eq!(placed[&id(0, 1)].alt, None);
assert_eq!(placed[&id(0, 2)].alt.as_deref(), Some("\u{e9}"));
}
#[test]
fn placements_carry_the_nearest_expansion() {
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 15 0 R 16 0 R] >>",
),
(12, "<< /Nums [0 [14 0 R 15 0 R 16 0 R]] >>"),
(
13,
"<< /Type /StructElem /S /Span /P 11 0 R /E (Portable Document Format) /K [14 0 R] >>",
),
(
14,
"<< /Type /StructElem /S /Span /P 13 0 R /Pg 3 0 R /K [0] >>",
),
(
15,
"<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [1] >>",
),
(
16,
"<< /Type /StructElem /S /Span /P 11 0 R /Pg 3 0 R /K [2] /E <FEFF00C9> >>",
),
],
);
let placed = placements(&doc, &[id(0, 0), id(0, 1), id(0, 2)]);
assert_eq!(
placed[&id(0, 0)].expansion.as_deref(),
Some("Portable Document Format")
);
assert_eq!(placed[&id(0, 1)].expansion, None);
assert_eq!(placed[&id(0, 2)].expansion.as_deref(), Some("\u{c9}"));
}
#[test]
fn placements_carry_the_nearest_language() {
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 /Lang (en) /K [13 0 R 14 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 /Lang (de) /K [1] >>",
),
],
);
let placed = placements(&doc, &[id(0, 0), id(0, 1)]);
assert_eq!(placed[&id(0, 0)].lang.as_deref(), Some("en"));
assert_eq!(placed[&id(0, 1)].lang.as_deref(), Some("de"));
let untagged = two_paragraphs("<< /Nums [0 [13 0 R 14 0 R 13 0 R 14 0 R]] >>");
assert_eq!(placements(&untagged, &[id(0, 0)])[&id(0, 0)].lang, None);
}
#[test]
fn placements_carry_the_elements_attribute_objects() {
let doc = tagged_doc(
"/StructParents 0",
&[
(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R \
/ClassMap << /Wide << /O /Table /ColSpan 3 /RowSpan 2 >> >> >>",
),
(
11,
"<< /Type /StructElem /S /Table /P 10 0 R /K [13 0 R 17 0 R] >>",
),
(12, "<< /Nums [0 [14 0 R 15 0 R 16 0 R 17 0 R]] >>"),
(
13,
"<< /Type /StructElem /S /TR /P 11 0 R /K [14 0 R 15 0 R 16 0 R] >>",
),
(
14,
"<< /Type /StructElem /S /TD /P 13 0 R /Pg 3 0 R /K [0] /R 1 \
/A << /O /Table /ColSpan 2 >> >>",
),
(
15,
"<< /Type /StructElem /S /TD /P 13 0 R /Pg 3 0 R /K [1] \
/C [/Wide 4] /A << /O /Table /ColSpan 1 >> >>",
),
(
16,
"<< /Type /StructElem /S /TD /P 13 0 R /Pg 3 0 R /K [2] \
/A [<< /O /Layout /Placement /Block >> 2 20 0 R] >>",
),
(
17,
"<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [3] >>",
),
(20, "<< /O /Table /RowSpan 3 >>"),
],
);
let placed = placements(&doc, &[id(0, 0), id(0, 1), id(0, 2), id(0, 3)]);
let cell = |num: u32| ObjRef { num, gen: 0 };
let int = |value: Option<&Object>| value.and_then(Object::as_int);
let objects = |p: &Placement, element: ObjRef| -> Vec<(String, i64)> {
p.attributes
.iter()
.filter(|a| a.element == element)
.map(|a| (a.owner.clone(), a.revision))
.collect()
};
let p0 = &placed[&id(0, 0)];
assert_eq!(int(p0.attribute(cell(14), "Table", "ColSpan")), Some(2));
assert_eq!(p0.attribute(cell(14), "Table", "RowSpan"), None);
assert_eq!(p0.attributes.len(), 1, "the row and table carry none");
assert_eq!(p0.path.last().unwrap().revision, 1);
let p1 = &placed[&id(0, 1)];
assert_eq!(
objects(p1, cell(15)),
[("Table".to_string(), 4), ("Table".to_string(), 0)]
);
assert_eq!(int(p1.attribute(cell(15), "Table", "ColSpan")), Some(1));
assert_eq!(int(p1.attribute(cell(15), "Table", "RowSpan")), Some(2));
assert_eq!(p1.path.last().unwrap().revision, 0);
let p2 = &placed[&id(0, 2)];
assert_eq!(
objects(p2, cell(16)),
[("Layout".to_string(), 2), ("Table".to_string(), 0)]
);
assert_eq!(int(p2.attribute(cell(16), "Table", "RowSpan")), Some(3));
assert_eq!(
p2.attribute(cell(16), "Layout", "Placement")
.and_then(Object::as_name)
.map(|n| n.0.as_str()),
Some("Block")
);
assert_eq!(p2.attribute(cell(16), "Table", "ColSpan"), None);
assert!(placed[&id(0, 3)].attributes.is_empty());
}
#[test]
fn standard_attribute_owners_are_recognized_by_name() {
assert_eq!(
StandardOwner::from_name("Table"),
Some(StandardOwner::Table)
);
assert_eq!(
StandardOwner::from_name("XML-1.00"),
Some(StandardOwner::Xml100)
);
assert_eq!(StandardOwner::from_name("Acme"), None);
assert_eq!(StandardOwner::from_name("table"), None);
assert_eq!(StandardOwner::ALL.len(), 11);
for owner in StandardOwner::ALL {
assert_eq!(
StandardOwner::from_name(owner.name()),
Some(owner),
"{}",
owner.name()
);
}
let object = AttributeObject {
element: ObjRef { num: 1, gen: 0 },
owner: "Layout".to_string(),
revision: 0,
entries: Dict::default(),
};
assert_eq!(object.standard_owner(), Some(StandardOwner::Layout));
let own = AttributeObject {
owner: "Acme".to_string(),
..object
};
assert_eq!(own.standard_owner(), None);
}
#[test]
fn standard_types_are_recognized_by_name_and_grouped_by_clause() {
assert_eq!(StandardType::from_name("H1"), Some(StandardType::H1));
assert_eq!(StandardType::H1.kind(), StandardKind::BlockLevel);
assert_eq!(
StandardType::from_name("TOCI").map(StandardType::kind),
Some(StandardKind::Grouping)
);
assert_eq!(
StandardType::from_name("LBody").map(StandardType::kind),
Some(StandardKind::BlockLevel)
);
assert_eq!(
StandardType::from_name("TFoot").map(StandardType::kind),
Some(StandardKind::BlockLevel)
);
assert_eq!(
StandardType::from_name("Ruby").map(StandardType::kind),
Some(StandardKind::InlineLevel)
);
assert_eq!(
StandardType::from_name("Formula").map(StandardType::kind),
Some(StandardKind::Illustration)
);
assert_eq!(StandardType::from_name("h1"), None);
assert_eq!(StandardType::from_name("Para"), None);
assert_eq!(StandardType::ALL.len(), 49);
for t in StandardType::ALL {
assert_eq!(StandardType::from_name(t.name()), Some(t), "{}", t.name());
}
let grouping = StandardType::ALL
.iter()
.filter(|t| t.kind() == StandardKind::Grouping)
.count();
assert_eq!(grouping, 12);
}
#[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 }),
class_map: &tree.class_map,
parent_entries: &tree.parent_entries,
caches: Caches::default(),
};
let element = block_on(walk.dict(ObjRef { num: 11, gen: 0 })).unwrap();
assert_eq!(block_on(walk.mcid_index(&element, id(0, 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);
}
fn r(num: u32) -> ObjRef {
ObjRef { num, gen: 0 }
}
fn content_items(doc: &Document) -> Vec<PlacedItem> {
let tree = doc.structure_tree().expect("tree");
let page = doc.page(0).unwrap();
block_on(tree.content_items_with(&Immediate(doc), &page))
}
fn items_of(placed: &[PlacedItem]) -> Vec<(ContentItem, u32)> {
placed.iter().map(|p| (p.item, p.placement.rank)).collect()
}
fn linked_doc(objr: &str, extra: &[(u32, &str)]) -> Document {
let mut objects = vec![
(
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 15 0 R] >>",
),
(12, "<< /Nums [0 [13 0 R 15 0 R] 1 14 0 R] >>"),
(
13,
"<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0] >>",
),
(
15,
"<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [1] >>",
),
(
20,
"<< /Type /Annot /Subtype /Link /Rect [0 0 10 10] /StructParent 1 >>",
),
];
let link = format!("<< /Type /StructElem /S /Link /P 11 0 R /Pg 3 0 R /K {objr} >>");
objects.push((14, link.as_str()));
objects.extend_from_slice(extra);
tagged_doc("/StructParents 0 /Annots [20 0 R]", &objects)
}
#[test]
fn an_annotation_takes_its_place_between_the_sequences() {
let doc = linked_doc("[<< /Type /OBJR /Obj 20 0 R >>]", &[]);
let placed = content_items(&doc);
assert_eq!(
items_of(&placed),
vec![
(ContentItem::Sequence(id(0, 0)), 0),
(ContentItem::Object(r(20)), 1),
(ContentItem::Sequence(id(0, 1)), 2),
]
);
let link = &placed[1].placement;
assert_eq!(link.structure_type.as_deref(), Some("Link"));
assert_eq!(link.standard_type, Some(StandardType::Link));
assert_eq!(
link.path
.iter()
.map(|e| e.standard_type)
.collect::<Vec<_>>(),
vec![StandardType::Document, StandardType::Link]
);
}
#[test]
fn an_indirect_object_reference_is_read_after_the_direct_kids() {
let doc = linked_doc(
"[21 0 R]",
&[(21, "<< /Type /OBJR /Obj 20 0 R /Pg 3 0 R >>")],
);
let placed = content_items(&doc);
assert_eq!(items_of(&placed)[1], (ContentItem::Object(r(20)), 1));
}
#[test]
fn the_object_reference_page_overrides_the_elements() {
let doc = tagged_doc(
"/StructParents 0 /Annots [20 0 R]",
&[
(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
),
(
11,
"<< /Type /StructElem /S /Link /P 10 0 R /Pg 99 0 R \
/K [<< /Type /OBJR /Obj 20 0 R /Pg 3 0 R >>] >>",
),
(12, "<< /Nums [1 11 0 R] >>"),
(20, "<< /Type /Annot /Subtype /Link /StructParent 1 >>"),
],
);
assert_eq!(
items_of(&content_items(&doc)),
vec![(ContentItem::Object(r(20)), 0)]
);
let doc = tagged_doc(
"/StructParents 0 /Annots [20 0 R]",
&[
(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
),
(
11,
"<< /Type /StructElem /S /Link /P 10 0 R /Pg 99 0 R \
/K [<< /Type /OBJR /Obj 20 0 R >>] >>",
),
(12, "<< /Nums [1 11 0 R] >>"),
(20, "<< /Type /Annot /Subtype /Link /StructParent 1 >>"),
],
);
assert!(content_items(&doc).is_empty());
}
#[test]
fn an_annotation_the_tree_does_not_hold_is_no_content_item() {
let doc = tagged_doc(
"/StructParents 0 /Annots [20 0 R 21 0 R 22 0 R 23 0 R << /Subtype /Link /StructParent 1 >>]",
&[
(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
),
(
11,
"<< /Type /StructElem /S /Link /P 10 0 R /Pg 3 0 R \
/K [<< /Type /OBJR /Obj 20 0 R >>] >>",
),
(12, "<< /Nums [1 11 0 R 3 11 0 R] >>"),
(20, "<< /Type /Annot /Subtype /Link /StructParent 1 >>"),
(21, "<< /Type /Annot /Subtype /Link >>"),
(22, "<< /Type /Annot /Subtype /Link /StructParent 2 >>"),
(23, "<< /Type /Annot /Subtype /Link /StructParent 3 >>"),
],
);
assert_eq!(
items_of(&content_items(&doc)),
vec![(ContentItem::Object(r(20)), 0)]
);
}
#[test]
fn a_link_element_orders_its_sequence_and_annotation_by_kid_index() {
let doc = tagged_doc(
"/StructParents 0 /Annots [20 0 R]",
&[
(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
),
(
11,
"<< /Type /StructElem /S /Link /P 10 0 R /Pg 3 0 R \
/K [<< /Type /OBJR /Obj 20 0 R >> << /Type /MCR /MCID 0 >>] >>",
),
(12, "<< /Nums [0 [11 0 R] 1 11 0 R] >>"),
(20, "<< /Type /Annot /Subtype /Link /StructParent 1 >>"),
],
);
assert_eq!(
items_of(&content_items(&doc)),
vec![
(ContentItem::Object(r(20)), 0),
(ContentItem::Sequence(id(0, 0)), 1),
]
);
}
#[test]
fn a_form_xobject_is_placed_when_handed_in() {
let doc = tagged_doc(
"/StructParents 0",
&[
(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
),
(
11,
"<< /Type /StructElem /S /Figure /P 10 0 R /Pg 3 0 R \
/K [<< /Type /OBJR /Obj 30 0 R >>] >>",
),
(12, "<< /Nums [5 11 0 R] >>"),
(
30,
"<< /Type /XObject /Subtype /Form /BBox [0 0 1 1] /StructParent 5 /Length 0 >>\nstream\n\nendstream",
),
],
);
let tree = doc.structure_tree().unwrap();
let page = doc.page(0).unwrap();
let placed = block_on(tree.place_items_with(
&Immediate(&doc),
&page,
&[ContentItem::Object(r(30)), ContentItem::Object(r(30))],
));
assert_eq!(placed.len(), 1);
assert_eq!(
placed[&ContentItem::Object(r(30))].standard_type,
Some(StandardType::Figure)
);
assert!(content_items(&doc).is_empty());
}
#[test]
fn a_reference_naming_a_stream_matches_only_that_streams_sequence() {
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 [<< /Type /MCR /Stm 30 0 R /MCID 0 >> 0] >>",
),
(12, "<< /Nums [0 [11 0 R] 1 [11 0 R]] >>"),
(
30,
"<< /Type /XObject /Subtype /Form /BBox [0 0 1 1] /StructParents 1 /Length 0 >>\nstream\n\nendstream",
),
],
);
let ranks = ranks(&doc, &[id(0, 0), id(1, 0)]);
assert_eq!(ranks[&id(1, 0)], 0);
assert_eq!(ranks[&id(0, 0)], 1);
}
#[test]
fn the_page_enumeration_skips_null_entries_and_needs_a_parent_tree() {
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 2] >>",
),
(12, "<< /Nums [0 [11 0 R null 11 0 R]] >>"),
],
);
assert_eq!(
items_of(&content_items(&doc)),
vec![
(ContentItem::Sequence(id(0, 0)), 0),
(ContentItem::Sequence(id(0, 2)), 1),
]
);
let doc = tagged_doc(
"/StructParents 0",
&[(10, "<< /Type /StructTreeRoot /K [] >>")],
);
assert!(content_items(&doc).is_empty());
}
fn counts(doc: &Document) -> (usize, usize) {
let tree = doc.structure_tree().expect("tree");
let page = doc.page(0).unwrap();
block_on(tree.content_item_counts_with(&Immediate(doc), &page))
}
#[test]
fn the_counts_follow_the_parent_tree_without_reading_the_elements() {
let doc = linked_doc("[<< /Type /OBJR /Obj 20 0 R >>]", &[]);
assert_eq!(counts(&doc), (2, 1));
assert_eq!(content_items(&doc).len(), 3);
let doc = tagged_doc(
"/StructParents 0 /Annots [20 0 R 21 0 R 22 0 R 23 0 R]",
&[
(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
),
(
11,
"<< /Type /StructElem /S /Link /P 10 0 R /Pg 3 0 R \
/K [<< /Type /OBJR /Obj 20 0 R >>] >>",
),
(12, "<< /Nums [0 [11 0 R null] 1 11 0 R 3 11 0 R] >>"),
(20, "<< /Type /Annot /Subtype /Link /StructParent 1 >>"),
(21, "<< /Type /Annot /Subtype /Link >>"),
(22, "<< /Type /Annot /Subtype /Link /StructParent 2 >>"),
(23, "<< /Type /Annot /Subtype /Link /StructParent 3 >>"),
],
);
assert_eq!(counts(&doc), (1, 2));
assert_eq!(content_items(&doc).len(), 1);
}
#[test]
fn a_second_page_reuses_what_the_first_walk_read() {
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 4 0 R] /Count 2 >>");
b.object(
3,
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /StructParents 0 >>",
);
b.object(
4,
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /StructParents 1 >>",
);
b.object(
10,
"<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
);
b.object(
11,
"<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R 14 0 R] >>",
);
b.object(12, "<< /Nums [0 [13 0 R] 1 [14 0 R]] >>");
b.object(
13,
"<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0] >>",
);
b.object(
14,
"<< /Type /StructElem /S /P /P 11 0 R /Pg 4 0 R /K [0] >>",
);
let doc = Document::load(b.build(1)).expect("load");
let tree = doc.structure_tree().expect("tree");
let first = block_on(tree.content_items_with(&Immediate(&doc), &doc.page(0).unwrap()));
assert_eq!(first.len(), 1);
assert!(tree.caches.lock().unwrap().dicts.contains_key(&r(11)));
let second = block_on(tree.content_items_with(&Immediate(&doc), &doc.page(1).unwrap()));
assert_eq!(second.len(), 1);
assert_eq!(second[0].placement.path.len(), 2);
assert_eq!(second[0].placement.path[0].object, r(11));
assert_eq!(second[0].placement.path[1].object, r(14));
assert_eq!(tree.clone(), tree);
}
}