use std::sync::Arc;
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 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, Clone, PartialEq)]
pub struct StructureTree {
root: Dict,
root_ref: Option<ObjRef>,
role_map: FastMap<String, String>,
class_map: FastMap<String, Object>,
}
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(),
};
Some(StructureTree {
root,
root_ref,
role_map,
class_map,
})
}
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 mut placed: FastMap<MarkedContentId, Placement> = FastMap::default();
let Some(parent_tree) = self.root.get("ParentTree") else {
return placed;
};
let Some(parent_tree) = resolved_dict(src, parent_tree).await else {
return placed;
};
let mut walk = Walk {
src,
page_ref: page.object_ref(),
root_ref: self.root_ref,
class_map: &self.class_map,
dicts: FastMap::default(),
paths: FastMap::default(),
parents: FastMap::default(),
};
let mut keyed: Vec<(MarkedContentId, Vec<u32>, Ancestry)> = Vec::new();
let mut seen: FastSet<MarkedContentId> = FastSet::default();
for id in ids {
if !seen.insert(*id) {
continue;
}
let Some((key, ancestry)) = walk.key_of(&parent_tree, *id).await else {
continue;
};
keyed.push((*id, 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| self.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(&self.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
.into_iter()
.flat_map(|ancestor| ancestor.attributes)
.collect();
placed.insert(
id,
Placement {
rank: rank as u32,
structure_type,
mapped_type,
standard_type,
path,
alt,
lang,
expansion,
attributes,
},
);
}
placed
}
}
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,
}
}
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>,
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>, Ancestry)> {
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, self.ancestry(element).await))
}
async fn ancestry(&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, parent_tree: &Dict, key: u32) -> Option<Arc<Vec<Object>>> {
if let Some(cached) = self.parents.get(&key) {
return cached.clone();
}
let found = match crate::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()
}
#[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,
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);
}
}