use pdfrum_object::{Dict, Name, Object, Resolve, names};
const MAX_ANCESTORS: usize = 64;
#[must_use]
pub(crate) fn inheritable(page: &Dict, key: &Name, r: &impl Resolve) -> Option<Object> {
if !page.contains_key(names::PARENT) || !page.contains_key(names::TYPE) {
return None;
}
if page.name(names::TYPE)?.as_bytes() != b"Page" {
return None;
}
page.dict(names::PARENT, r)?;
if let Some(own) = page.get(key, r) {
return Some(own.get().clone());
}
let mut seen: Vec<Dict> = vec![page.clone()];
let mut node = page.dict(names::PARENT, r)?;
for _ in 0..MAX_ANCESTORS {
if seen.contains(&node) {
return None;
}
if let Some(value) = node.get(key, r) {
return Some(value.get().clone());
}
seen.push(node.clone());
node = node.dict(names::PARENT, r)?;
}
None
}
#[cfg(test)]
pub(crate) fn copy_inheritable(out: &mut Dict, page: &Dict, key: &Name, r: &impl Resolve) -> bool {
if out.contains_key(key) {
return true;
}
match inheritable(page, key, r) {
Some(value) => {
out.push(key.clone(), value);
true
}
None => false,
}
}
#[cfg(test)]
mod tests {
use super::{copy_inheritable, inheritable};
use pdfrum_object::{Array, Dict, Name, ObjRef, Object, Resolve, names};
use std::collections::BTreeMap;
use std::sync::Arc;
struct Store(BTreeMap<u32, Object>);
impl Resolve for Store {
fn fetch(&self, r: ObjRef) -> Result<Arc<Object>, pdfrum_object::Error> {
Ok(Arc::new(
self.0.get(&r.num).cloned().unwrap_or(Object::Null),
))
}
}
fn page(pairs: impl IntoIterator<Item = (&'static str, Object)>) -> Dict {
let mut d = Dict::from_pairs([(names::TYPE.clone(), Object::Name(names::PAGE.clone()))]);
for (k, v) in pairs {
d.push(Name::from(k), v);
}
d
}
fn media_box(width: f32) -> Object {
Object::Array(Array::of([
Object::Int(0),
Object::Int(0),
Object::Real(width),
Object::Int(792),
]))
}
fn r(num: u32) -> Object {
Object::Ref(ObjRef::new(num, 0))
}
#[test]
fn a_page_inherits_from_its_grandparent() {
let store = Store(BTreeMap::from([
(
2,
Object::Dict(Dict::from_pairs([(names::PARENT.clone(), r(1))])),
),
(
1,
Object::Dict(Dict::from_pairs([(
names::MEDIA_BOX.clone(),
media_box(612.0),
)])),
),
]));
let p = page([("Parent", r(2))]);
let found = inheritable(&p, names::MEDIA_BOX, &store).expect("inherits");
assert_eq!(found.as_array().map(Array::len), Some(4));
}
#[test]
fn a_pages_own_value_wins() {
let store = Store(BTreeMap::from([(
1,
Object::Dict(Dict::from_pairs([(
names::MEDIA_BOX.clone(),
media_box(612.0),
)])),
)]));
let p = page([("Parent", r(1)), ("MediaBox", media_box(200.0))]);
let found = inheritable(&p, names::MEDIA_BOX, &store).expect("its own");
assert_eq!(found.as_array().and_then(|a| a.number_at(2)), Some(200.0));
}
#[test]
fn no_parent_means_nothing_is_inheritable() {
let store = Store(BTreeMap::new());
let p = page([("MediaBox", media_box(200.0))]);
assert!(inheritable(&p, names::MEDIA_BOX, &store).is_none());
}
#[test]
fn the_type_test_is_case_sensitive() {
let store = Store(BTreeMap::from([(
1,
Object::Dict(Dict::from_pairs([(
names::MEDIA_BOX.clone(),
media_box(612.0),
)])),
)]));
let mut p = Dict::from_pairs([
(names::TYPE.clone(), Object::Name(Name::from("page"))),
(names::PARENT.clone(), r(1)),
]);
assert!(inheritable(&p, names::MEDIA_BOX, &store).is_none());
p = Dict::from_pairs([
(names::TYPE.clone(), Object::Name(names::PAGE.clone())),
(names::PARENT.clone(), r(1)),
]);
assert!(inheritable(&p, names::MEDIA_BOX, &store).is_some());
}
#[test]
fn a_missing_type_stops_the_walk() {
let store = Store(BTreeMap::from([(
1,
Object::Dict(Dict::from_pairs([(
names::MEDIA_BOX.clone(),
media_box(612.0),
)])),
)]));
let p = Dict::from_pairs([(names::PARENT.clone(), r(1))]);
assert!(inheritable(&p, names::MEDIA_BOX, &store).is_none());
}
#[test]
fn a_non_dictionary_parent_stops_the_walk() {
let store = Store(BTreeMap::from([(1, Object::Int(5))]));
let p = page([("Parent", r(1)), ("MediaBox", media_box(200.0))]);
assert!(inheritable(&p, names::MEDIA_BOX, &store).is_none());
}
#[test]
fn a_parent_cycle_discards_everything() {
let store = Store(BTreeMap::from([
(
1,
Object::Dict(Dict::from_pairs([(names::PARENT.clone(), r(2))])),
),
(
2,
Object::Dict(Dict::from_pairs([(names::PARENT.clone(), r(1))])),
),
]));
let p = page([("Parent", r(1))]);
assert!(inheritable(&p, names::ROTATE, &store).is_none());
}
#[test]
fn an_ancestors_type_is_not_checked() {
let store = Store(BTreeMap::from([(
1,
Object::Dict(Dict::from_pairs([
(names::TYPE.clone(), Object::Name(Name::from("Whatever"))),
(names::ROTATE.clone(), Object::Int(90)),
])),
)]));
let p = page([("Parent", r(1))]);
assert_eq!(
inheritable(&p, names::ROTATE, &store).and_then(|o| o.as_int()),
Some(90)
);
}
#[test]
fn copying_never_overwrites_what_is_already_there() {
let store = Store(BTreeMap::from([(
1,
Object::Dict(Dict::from_pairs([(
names::MEDIA_BOX.clone(),
media_box(612.0),
)])),
)]));
let p = page([("Parent", r(1))]);
let mut out = Dict::from_pairs([(names::MEDIA_BOX.clone(), media_box(100.0))]);
assert!(copy_inheritable(&mut out, &p, names::MEDIA_BOX, &store));
assert_eq!(
out.array(names::MEDIA_BOX, &store)
.and_then(|a| a.number_at(2)),
Some(100.0),
"the existing value is untouched"
);
}
#[test]
fn copying_reports_whether_the_key_ended_up_present() {
let store = Store(BTreeMap::new());
let p = page([]);
let mut out = Dict::new();
assert!(!copy_inheritable(&mut out, &p, names::MEDIA_BOX, &store));
assert!(!out.contains_key(names::MEDIA_BOX));
}
}