#[cfg(test)]
use pdfrum_object::Array;
use pdfrum_object::{Dict, Object, Resolve, names};
#[must_use]
pub(crate) fn is_copyable(value: &Object) -> bool {
match value {
Object::Bool(_) | Object::Int(_) | Object::Real(_) | Object::Str(_) | Object::Name(_) => {
true
}
Object::Array(a) => a.iter().all(is_flat_element),
Object::Null | Object::Dict(_) | Object::Stream(_) | Object::Ref(_) => false,
}
}
fn is_flat_element(value: &Object) -> bool {
matches!(
value,
Object::Bool(_) | Object::Int(_) | Object::Real(_) | Object::Str(_) | Object::Name(_)
)
}
#[must_use]
pub(crate) fn filtered(src_catalog: &Dict, r: &impl Resolve) -> Option<Dict> {
let source = src_catalog.dict(names::VIEWER_PREFERENCES, r)?;
let mut out = Dict::new();
for (key, value) in source.iter() {
if is_copyable(value) {
out.push(key.clone(), value.clone());
}
}
Some(out)
}
#[must_use]
#[cfg(test)]
pub(crate) fn array_is_flat(a: &Array) -> bool {
a.iter().all(is_flat_element)
}
#[cfg(test)]
mod tests {
use super::{array_is_flat, filtered, is_copyable};
use pdfrum_object::{Array, Dict, Name, ObjRef, Object, PdfString, 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),
))
}
}
#[test]
fn the_six_accepted_kinds_survive() {
let prefs = Dict::from_pairs([
(Name::from("Bool"), Object::Bool(true)),
(Name::from("Num"), Object::Int(1)),
(Name::from("Str"), Object::Str(PdfString::literal(b"str"))),
(Name::from("Name"), Object::Name(Name::from("name"))),
(Name::from("EmptyArray"), Object::Array(Array::new())),
(
Name::from("GoodArray"),
Object::Array(Array::of([
Object::Int(1),
Object::Int(2),
Object::Int(3),
Object::Int(4),
])),
),
]);
let catalog = Dict::from_pairs([(names::VIEWER_PREFERENCES.clone(), Object::Dict(prefs))]);
let out = filtered(&catalog, &Store(BTreeMap::new())).expect("a dict");
assert_eq!(out.len(), 6);
assert_eq!(out.bool(&Name::from("Bool")), Some(true));
assert_eq!(out.direct_int(&Name::from("Num")), Some(1));
assert_eq!(
out.string(&Name::from("Str")).map(|s| s.bytes.to_vec()),
Some(b"str".to_vec())
);
assert_eq!(out.name(&Name::from("Name")), Some(&Name::from("name")));
assert_eq!(
out.array(&Name::from("GoodArray"), &Store(BTreeMap::new()))
.map(|a| a.len()),
Some(4)
);
}
#[test]
fn references_are_refused_at_every_level() {
let reference = Object::Ref(ObjRef::new(9, 0));
assert!(!is_copyable(&reference));
assert!(!is_copyable(&Object::Array(Array::of([
Object::Int(1),
reference
]))));
}
#[test]
fn dictionaries_nulls_and_streams_are_refused() {
assert!(!is_copyable(&Object::Dict(Dict::new())));
assert!(!is_copyable(&Object::Null));
assert!(!is_copyable(&Object::Stream(Box::new(
pdfrum_object::Stream::new(Dict::new(), pdfrum_object::ByteSpan::empty())
))));
}
#[test]
fn a_nested_array_is_refused() {
let nested = Object::Array(Array::of([
Object::Int(1),
Object::Array(Array::of([Object::Int(2)])),
]));
assert!(!is_copyable(&nested));
}
#[test]
fn an_empty_array_is_flat() {
assert!(array_is_flat(&Array::new()));
}
#[test]
fn a_circular_graph_is_filtered_out_rather_than_walked() {
let prefs = Dict::from_pairs([
(Name::from("Loop"), Object::Ref(ObjRef::new(1, 0))),
(Name::from("Fine"), Object::Bool(true)),
]);
let catalog = Dict::from_pairs([(
names::VIEWER_PREFERENCES.clone(),
Object::Dict(prefs.clone()),
)]);
let store = Store(BTreeMap::from([(1, Object::Dict(prefs))]));
let out = filtered(&catalog, &store).expect("a dict");
assert_eq!(out.len(), 1);
assert!(out.contains_key(&Name::from("Fine")));
}
#[test]
fn dropping_every_entry_still_reports_success() {
let prefs = Dict::from_pairs([(Name::from("Loop"), Object::Ref(ObjRef::new(1, 0)))]);
let catalog = Dict::from_pairs([(names::VIEWER_PREFERENCES.clone(), Object::Dict(prefs))]);
let out = filtered(&catalog, &Store(BTreeMap::new())).expect("still a dict");
assert!(out.is_empty());
}
#[test]
fn a_catalog_without_preferences_copies_nothing() {
let catalog =
Dict::from_pairs([(names::TYPE.clone(), Object::Name(Name::from("Catalog")))]);
assert!(filtered(&catalog, &Store(BTreeMap::new())).is_none());
}
#[test]
fn a_non_dictionary_value_reads_as_absent() {
let catalog = Dict::from_pairs([(names::VIEWER_PREFERENCES.clone(), Object::Int(5))]);
assert!(filtered(&catalog, &Store(BTreeMap::new())).is_none());
}
}