use pdfrum_common::kurbo::{Affine, Rect};
use crate::{Array, Name, Object, PdfString, Resolve, Resolved, Stream};
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Dict(Vec<(Name, Object)>);
impl Dict {
#[must_use]
pub fn new() -> Self {
Self(Vec::new())
}
#[must_use]
pub fn from_pairs(pairs: impl IntoIterator<Item = (Name, Object)>) -> Self {
pairs.into_iter().collect()
}
pub fn push(&mut self, key: Name, value: Object) {
self.0.push((key, value));
}
pub fn insert(&mut self, key: Name, value: Object) {
if let Some(entry) = self.0.iter_mut().find(|entry| entry.0 == key) {
entry.1 = value;
} else {
self.0.push((key, value));
}
}
pub fn remove(&mut self, key: &Name) -> Option<Object> {
self.0
.iter()
.position(|(k, _)| k == key)
.map(|index| self.0.remove(index).1)
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &(Name, Object)> {
self.0.iter()
}
pub fn keys(&self) -> impl Iterator<Item = &Name> {
self.0.iter().map(|(k, _)| k)
}
#[must_use]
pub fn contains_key(&self, key: &Name) -> bool {
self.raw(key).is_some()
}
#[must_use]
pub fn raw(&self, key: &Name) -> Option<&Object> {
self.0.iter().rev().find(|(k, _)| k == key).map(|(_, v)| v)
}
#[must_use]
pub fn direct_int(&self, key: &Name) -> Option<i64> {
self.raw(key)?.as_number()?.as_int()
}
#[must_use]
pub fn name(&self, key: &Name) -> Option<&Name> {
self.raw(key)?.as_name()
}
#[must_use]
pub fn bool(&self, key: &Name) -> Option<bool> {
self.raw(key)?.as_bool()
}
#[must_use]
pub fn number_obj(&self, key: &Name) -> Option<&Object> {
self.raw(key)?.as_number()
}
#[must_use]
pub fn string(&self, key: &Name) -> Option<&PdfString> {
self.raw(key)?.as_string()
}
#[must_use]
pub fn get<'a>(&'a self, key: &Name, r: &impl Resolve) -> Option<Resolved<'a>> {
self.raw(key)?.resolve(r).ok()
}
#[must_use]
pub fn int(&self, key: &Name, r: &impl Resolve) -> Option<i64> {
self.get(key, r)?.as_direct()?.as_int()
}
#[must_use]
pub fn number(&self, key: &Name, r: &impl Resolve) -> Option<f32> {
self.get(key, r)?.as_direct()?.number()
}
#[must_use]
pub fn byte_string(&self, key: &Name, r: &impl Resolve) -> Option<Vec<u8>> {
Some(self.get(key, r)?.as_direct()?.to_byte_string())
}
#[must_use]
pub fn text(&self, key: &Name, r: &impl Resolve) -> Option<String> {
Some(self.get(key, r)?.to_text())
}
#[must_use]
pub fn dict(&self, key: &Name, r: &impl Resolve) -> Option<Dict> {
self.get(key, r)?.as_direct()?.as_dict().cloned()
}
#[must_use]
pub fn array(&self, key: &Name, r: &impl Resolve) -> Option<Array> {
self.get(key, r)?.as_direct()?.as_array().cloned()
}
#[must_use]
pub fn stream(&self, key: &Name, r: &impl Resolve) -> Option<Stream> {
self.get(key, r)?.as_direct()?.as_stream().cloned()
}
#[must_use]
pub fn reference(&self, key: &Name) -> Option<crate::ObjRef> {
self.raw(key)?.as_ref_id()
}
#[must_use]
pub fn rect(&self, key: &Name, r: &impl Resolve) -> Rect {
self.array(key, r)
.map_or_else(|| Rect::new(0.0, 0.0, 0.0, 0.0), |a| a.as_rect())
}
#[must_use]
pub fn matrix(&self, key: &Name, r: &impl Resolve) -> Affine {
self.array(key, r)
.map_or(Affine::IDENTITY, |a| a.as_matrix())
}
}
impl FromIterator<(Name, Object)> for Dict {
fn from_iter<I: IntoIterator<Item = (Name, Object)>>(iter: I) -> Self {
let mut dict = Self::new();
for (k, v) in iter {
dict.push(k, v);
}
dict
}
}
impl<'a> IntoIterator for &'a Dict {
type Item = &'a (Name, Object);
type IntoIter = std::slice::Iter<'a, (Name, Object)>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
#[cfg(test)]
mod tests {
use pdfrum_common::kurbo::{Affine, Rect};
use crate::test_resolve::TestStore;
use crate::{Array, Dict, Name, NoResolve, ObjRef, Object, PdfString, Stream, names};
fn key(s: &str) -> Name {
Name::from(s)
}
#[test]
fn iteration_follows_document_order_not_sort_order() {
let dict = Dict::from_pairs([
(key("the-dictionary"), Object::Dict(Dict::new())),
(key("the-array"), Object::Array(Array::new())),
(key("the-number"), Object::Int(42)),
]);
let order: Vec<_> = dict.keys().filter_map(Name::as_str).collect();
assert_eq!(order, ["the-dictionary", "the-array", "the-number"]);
}
#[test]
fn last_duplicate_wins_on_lookup_and_both_are_kept() {
let dict = Dict::from_pairs([
(key("K"), Object::Int(1)),
(key("K"), Object::Int(2)),
(key("K"), Object::Int(3)),
]);
assert_eq!(dict.raw(&key("K")), Some(&Object::Int(3)));
assert_eq!(dict.direct_int(&key("K")), Some(3));
assert_eq!(dict.len(), 3, "the file said it three times");
}
#[test]
fn name_accessor_is_type_filtered_but_byte_string_coerces() {
let dict = Dict::from_pairs([
(key("bool"), Object::Bool(false)),
(key("num"), Object::Real(0.23)),
(key("string"), Object::Str(PdfString::literal(b"ium"))),
(key("name"), Object::Name(key("Pdf"))),
]);
assert_eq!(dict.name(&key("invalid")), None);
assert_eq!(dict.name(&key("bool")), None);
assert_eq!(dict.name(&key("num")), None);
assert_eq!(dict.name(&key("string")), None);
assert_eq!(dict.name(&key("name")), Some(&key("Pdf")));
assert_eq!(dict.byte_string(&key("invalid"), &NoResolve), None);
assert_eq!(
dict.byte_string(&key("bool"), &NoResolve).as_deref(),
Some(&b"false"[..])
);
assert_eq!(
dict.byte_string(&key("num"), &NoResolve).as_deref(),
Some(&b".23"[..])
);
assert_eq!(
dict.byte_string(&key("string"), &NoResolve).as_deref(),
Some(&b"ium"[..])
);
assert_eq!(
dict.byte_string(&key("name"), &NoResolve).as_deref(),
Some(&b"Pdf"[..])
);
}
#[test]
fn boolean_accessor_rejects_integers() {
let dict = Dict::from_pairs([
(key("flag"), Object::Bool(true)),
(key("one"), Object::Int(1)),
]);
assert_eq!(dict.bool(&key("flag")), Some(true));
assert_eq!(dict.bool(&key("one")), None, "an Int(1) is not a boolean");
}
#[test]
fn direct_int_ignores_indirection_while_int_follows_it() {
let store = TestStore::from_pairs([(3, Object::Int(99))]);
let dict = Dict::from_pairs([
(names::PREV.clone(), Object::Ref(ObjRef::new(3, 0))),
(names::LENGTH.clone(), Object::Ref(ObjRef::new(3, 0))),
]);
assert_eq!(dict.direct_int(names::PREV), None);
assert_eq!(dict.int(names::LENGTH, &store), Some(99));
}
#[test]
fn resolution_stops_after_one_level() {
let store =
TestStore::from_pairs([(1, Object::Ref(ObjRef::new(2, 0))), (2, Object::Int(7))]);
let dict = Dict::from_pairs([(key("K"), Object::Ref(ObjRef::new(1, 0)))]);
assert_eq!(dict.int(&key("K"), &store), None);
assert_eq!(dict.number(&key("K"), &store), None);
assert_eq!(
dict.get(&key("K"), &store).as_deref(),
Some(&Object::Ref(ObjRef::new(2, 0)))
);
}
#[test]
fn dangling_references_read_as_absent() {
let store = TestStore::default();
let dict = Dict::from_pairs([(key("K"), Object::Ref(ObjRef::new(9, 0)))]);
assert!(dict.get(&key("K"), &store).is_none());
assert_eq!(dict.int(&key("K"), &store), None);
assert_eq!(dict.dict(&key("K"), &store), None);
}
#[test]
fn dict_accessor_accepts_a_stream() {
let inner = Dict::from_pairs([(names::LENGTH.clone(), Object::Int(3))]);
let stream = Stream::new(inner.clone(), b"abc".to_vec().into());
let store = TestStore::from_pairs([(5, Object::Stream(Box::new(stream)))]);
let dict = Dict::from_pairs([(key("S"), Object::Ref(ObjRef::new(5, 0)))]);
assert_eq!(dict.dict(&key("S"), &store), Some(inner));
assert!(dict.stream(&key("S"), &store).is_some());
assert_eq!(dict.array(&key("S"), &store), None);
}
#[test]
fn rect_and_matrix_need_exactly_the_right_element_count() {
let four = Object::Array(Array::of([
Object::Int(1),
Object::Int(2),
Object::Int(3),
Object::Int(4),
]));
let three = Object::Array(Array::of([Object::Int(1), Object::Int(2), Object::Int(3)]));
let six = Object::Array(
(1..=6)
.map(|i| Object::Int(i64::from(i)))
.collect::<Array>(),
);
let dict = Dict::from_pairs([
(key("four"), four),
(key("three"), three),
(key("six"), six),
]);
assert_eq!(
dict.rect(&key("four"), &NoResolve),
Rect::new(1.0, 2.0, 3.0, 4.0)
);
assert_eq!(
dict.rect(&key("three"), &NoResolve),
Rect::new(0.0, 0.0, 0.0, 0.0)
);
assert_eq!(
dict.rect(&key("missing"), &NoResolve),
Rect::new(0.0, 0.0, 0.0, 0.0)
);
assert_eq!(
dict.matrix(&key("six"), &NoResolve),
Affine::new([1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
);
assert_eq!(dict.matrix(&key("four"), &NoResolve), Affine::IDENTITY);
assert_eq!(dict.matrix(&key("missing"), &NoResolve), Affine::IDENTITY);
}
#[test]
fn missing_keys_read_as_their_fallbacks_everywhere() {
let dict = Dict::new();
let absent = key("nope");
assert!(dict.is_empty());
assert!(!dict.contains_key(&absent));
assert_eq!(dict.raw(&absent), None);
assert_eq!(dict.int(&absent, &NoResolve), None);
assert_eq!(dict.number(&absent, &NoResolve), None);
assert_eq!(dict.name(&absent), None);
assert_eq!(dict.bool(&absent), None);
assert_eq!(dict.string(&absent), None);
assert_eq!(dict.text(&absent, &NoResolve), None);
assert_eq!(dict.reference(&absent), None);
}
#[test]
fn parsed_nulls_are_stored_like_any_other_value() {
let dict = Dict::from_pairs([(key("K"), Object::Null)]);
assert!(dict.contains_key(&key("K")));
assert_eq!(dict.raw(&key("K")), Some(&Object::Null));
assert_eq!(dict.int(&key("K"), &NoResolve), None);
}
#[test]
fn insert_replaces_in_place_and_appends_when_new() {
let mut d = Dict::new();
d.push(Name::from("A"), Object::Int(1));
d.push(Name::from("B"), Object::Int(2));
d.insert(Name::from("B"), Object::Int(20));
assert_eq!(d.raw(&Name::from("B")), Some(&Object::Int(20)));
let keys: Vec<_> = d.iter().map(|(k, _)| k.clone()).collect();
assert_eq!(keys, vec![Name::from("A"), Name::from("B")]);
d.insert(Name::from("C"), Object::Int(3));
let keys: Vec<_> = d.iter().map(|(k, _)| k.clone()).collect();
assert_eq!(
keys,
vec![Name::from("A"), Name::from("B"), Name::from("C")]
);
}
#[test]
fn remove_returns_the_value_and_drops_the_key() {
let mut d = Dict::new();
d.push(Name::from("A"), Object::Int(1));
d.push(Name::from("B"), Object::Null);
assert_eq!(d.remove(&Name::from("A")), Some(Object::Int(1)));
assert_eq!(d.raw(&Name::from("A")), None);
assert_eq!(d.raw(&Name::from("B")), Some(&Object::Null));
assert_eq!(d.len(), 1);
assert_eq!(d.remove(&Name::from("A")), None);
}
}