Skip to main content

pdfrum_object/
dict.rs

1//! Dictionary objects (ISO 32000-1 §7.3.7) and the typed accessors every
2//! other crate reads PDF structure through. Keys keep document order,
3//! duplicates and all; the **last** entry with a key wins on lookup.
4//!
5//! Whether an accessor follows an indirect reference is deliberate, not an
6//! optimization. [`Dict::get`], [`Dict::int`], [`Dict::number`],
7//! [`Dict::dict`], [`Dict::array`], [`Dict::stream`], [`Dict::text`],
8//! [`Dict::rect`], [`Dict::matrix`] and [`Dict::byte_string`] chase one
9//! reference; [`Dict::raw`], [`Dict::direct_int`], [`Dict::name`],
10//! [`Dict::bool`], [`Dict::number_obj`] and [`Dict::string`] read one as
11//! absence.
12
13// # Why a `Vec`, and why insertion order
14//
15// PDF dictionaries are small — a handful of keys, a few dozen at the extreme
16// — so a linear scan beats a hash map on every real document, and the
17// storage doubles as the writer's key order. PDFium uses a sorted map and
18// therefore writes keys sorted; we keep document order in storage *and* in
19// serialization. That difference is invisible to every behavior under test:
20// lookup semantics are identical, and round-trip fidelity is judged by
21// reparsing, not by byte-diffing the output.
22//
23// Duplicate keys are kept as parsed and the **last** one wins on lookup,
24// which is what PDFium's overwrite-on-insert produces for a document read
25// front to back.
26//
27// # Which accessors resolve
28//
29// Whether an accessor follows an indirect reference is not a detail — it is
30// load-bearing recovery behavior. An indirect `/Prev` is *ignored* by the
31// cross-reference reader while an indirect `/Length` *is* chased, and files
32// in the wild depend on both. So this module offers each accessor in two
33// flavours and callers pick deliberately. The non-resolving ones are not an
34// optimization; they are the accessors whose C++ counterparts type-check
35// *before* resolution, so a reference there reads as absence.
36
37use pdfrum_common::kurbo::{Affine, Rect};
38
39use crate::{Array, Name, Object, PdfString, Resolve, Resolved, Stream};
40
41/// A PDF dictionary: key-value pairs in document order.
42///
43/// # Streams as values
44///
45/// ISO 32000-1 §7.3.8.1 forbids a *file* from writing a stream as a direct
46/// dictionary value, and the reader drops one found inline while parsing.
47/// That is a **file-format** constraint, not an in-memory invariant, and
48/// this type does not police it:
49/// [`Object::clone_direct`](crate::Object::clone_direct) flattens
50/// references, so a `/Resources` whose `/XObject` entries are indirect
51/// streams clones into a dictionary holding those streams directly, and
52/// [`Dict::stream`] reads such a value back. Enforcing §7.3.8.1 is the
53/// **writer's** job: `pdfrum-edit` hoists a direct stream to an indirect
54/// object at serialization time.
55///
56/// ```
57/// use pdfrum_object::{Dict, NoResolve, Object, names};
58///
59/// let dict = Dict::from_pairs([
60///     (names::TYPE.clone(), Object::Name(names::PAGE.clone())),
61///     (names::COUNT.clone(), Object::Int(3)),
62/// ]);
63/// assert_eq!(dict.name(names::TYPE), Some(names::PAGE));
64/// assert_eq!(dict.int(names::COUNT, &NoResolve), Some(3));
65/// assert_eq!(dict.len(), 2);
66/// ```
67// The inline-stream drop while parsing is `cpdf_syntax_parser.cpp:645-649`.
68// `CPDF_Dictionary::CloneNonCyclic` produces the same flattened shape, since
69// its loop writes straight into `map_` and bypasses the `CHECK(!IsStream())`
70// that guards the ordinary setters; `Dict::stream` mirrors
71// `CPDF_Dictionary::GetStreamFor` in reading it back.
72#[derive(Debug, Clone, Default, PartialEq)]
73pub struct Dict(Vec<(Name, Object)>);
74
75impl Dict {
76    /// An empty dictionary.
77    #[must_use]
78    pub fn new() -> Self {
79        Self(Vec::new())
80    }
81
82    /// A dictionary from key-value pairs, keeping their order.
83    #[must_use]
84    pub fn from_pairs(pairs: impl IntoIterator<Item = (Name, Object)>) -> Self {
85        pairs.into_iter().collect()
86    }
87
88    /// Append a pair, keeping any earlier entry with the same key.
89    ///
90    /// The later entry wins on lookup, so appending is how a reader records a
91    /// duplicate key without losing what the file actually said.
92    ///
93    /// Any object, a stream included — see the type-level note on §7.3.8.1.
94    pub fn push(&mut self, key: Name, value: Object) {
95        self.0.push((key, value));
96    }
97
98    /// Sets `key` to `value`: replaces the existing entry in place, keeping
99    /// its position, or appends.
100    pub fn insert(&mut self, key: Name, value: Object) {
101        if let Some(entry) = self.0.iter_mut().find(|entry| entry.0 == key) {
102            entry.1 = value;
103        } else {
104            self.0.push((key, value));
105        }
106    }
107
108    /// Removes `key`, returning its value; `None` when absent.
109    pub fn remove(&mut self, key: &Name) -> Option<Object> {
110        self.0
111            .iter()
112            .position(|(k, _)| k == key)
113            .map(|index| self.0.remove(index).1)
114    }
115
116    /// Number of stored pairs, duplicates included.
117    #[must_use]
118    pub fn len(&self) -> usize {
119        self.0.len()
120    }
121
122    /// Whether the dictionary has no pairs.
123    #[must_use]
124    pub fn is_empty(&self) -> bool {
125        self.0.is_empty()
126    }
127
128    /// The pairs, in document order.
129    pub fn iter(&self) -> impl Iterator<Item = &(Name, Object)> {
130        self.0.iter()
131    }
132
133    /// The keys, in document order, duplicates included.
134    pub fn keys(&self) -> impl Iterator<Item = &Name> {
135        self.0.iter().map(|(k, _)| k)
136    }
137
138    /// Whether any entry carries this key.
139    #[must_use]
140    pub fn contains_key(&self, key: &Name) -> bool {
141        self.raw(key).is_some()
142    }
143
144    // ---- non-resolving accessors ----
145
146    /// The stored value, whatever its type, without resolving references.
147    ///
148    /// The last entry with this key wins.
149    #[must_use]
150    pub fn raw(&self, key: &Name) -> Option<&Object> {
151        self.0.iter().rev().find(|(k, _)| k == key).map(|(_, v)| v)
152    }
153
154    /// The value of a `Number`-typed entry in the C-integer view, without
155    /// resolving.
156    ///
157    /// This is how the cross-reference reader reads `/Size`, `/Prev` and
158    /// `/XRefStm`: an *indirect* value there is ignored rather than chased,
159    /// which is deliberate recovery behavior in files whose trailer points at
160    /// objects the table cannot yet describe.
161    #[must_use]
162    pub fn direct_int(&self, key: &Name) -> Option<i64> {
163        self.raw(key)?.as_number()?.as_int()
164    }
165
166    /// The name a `Name`-typed entry holds, without resolving.
167    ///
168    /// A reference here reads as absent: the type check happens before any
169    /// resolution, so `/Type 5 0 R` never names a type.
170    #[must_use]
171    pub fn name(&self, key: &Name) -> Option<&Name> {
172        self.raw(key)?.as_name()
173    }
174
175    /// The value of a `Boolean`-typed entry, without resolving.
176    ///
177    /// An `Int(1)` is not a boolean and reads as absent.
178    #[must_use]
179    pub fn bool(&self, key: &Name) -> Option<bool> {
180        self.raw(key)?.as_bool()
181    }
182
183    /// A `Number`-typed entry as an object, without resolving. Used where the
184    /// distinction between "not a number" and "zero" matters, such as
185    /// validating a cross-reference stream's `/Index`.
186    #[must_use]
187    pub fn number_obj(&self, key: &Name) -> Option<&Object> {
188        self.raw(key)?.as_number()
189    }
190
191    /// The string a `String`-typed entry holds, without resolving.
192    #[must_use]
193    pub fn string(&self, key: &Name) -> Option<&PdfString> {
194        self.raw(key)?.as_string()
195    }
196
197    // ---- resolving accessors ----
198
199    /// The value, following one level of indirection.
200    ///
201    /// Returns `None` for a missing key *and* for a reference the store
202    /// cannot produce — both are absence — and the store records the
203    /// underlying failure in its diagnostics.
204    #[must_use]
205    pub fn get<'a>(&'a self, key: &Name, r: &impl Resolve) -> Option<Resolved<'a>> {
206        self.raw(key)?.resolve(r).ok()
207    }
208
209    /// The integer value of an entry of any type, in the C-integer view.
210    ///
211    /// Coerces: booleans read as 0 and 1, reals truncate. A reference is
212    /// followed one level, and a reference *to* a reference reads as absent.
213    #[must_use]
214    pub fn int(&self, key: &Name, r: &impl Resolve) -> Option<i64> {
215        self.get(key, r)?.as_direct()?.as_int()
216    }
217
218    /// The numeric value of an entry, coercing integers to `f32`.
219    #[must_use]
220    pub fn number(&self, key: &Name, r: &impl Resolve) -> Option<f32> {
221        self.get(key, r)?.as_direct()?.number()
222    }
223
224    /// The byte-string spelling of an entry of any type — see
225    /// [`Object::to_byte_string`].
226    #[must_use]
227    pub fn byte_string(&self, key: &Name, r: &impl Resolve) -> Option<Vec<u8>> {
228        Some(self.get(key, r)?.as_direct()?.to_byte_string())
229    }
230
231    /// An entry read as text — see [`Object::to_text`].
232    #[must_use]
233    pub fn text(&self, key: &Name, r: &impl Resolve) -> Option<String> {
234        Some(self.get(key, r)?.to_text())
235    }
236
237    /// The dictionary an entry holds, following one level of indirection.
238    ///
239    /// A stream answers with its own dictionary, so `/Pages` pointing at
240    /// either a dictionary or a stream reads the same way.
241    ///
242    /// Returns an owned clone because the dictionary may live inside an
243    /// `Arc` the store owns; dictionaries are small and this keeps the
244    /// borrow story simple for callers.
245    #[must_use]
246    pub fn dict(&self, key: &Name, r: &impl Resolve) -> Option<Dict> {
247        self.get(key, r)?.as_direct()?.as_dict().cloned()
248    }
249
250    /// The array an entry holds, following one level of indirection.
251    #[must_use]
252    pub fn array(&self, key: &Name, r: &impl Resolve) -> Option<Array> {
253        self.get(key, r)?.as_direct()?.as_array().cloned()
254    }
255
256    /// The stream an entry holds, following one level of indirection.
257    #[must_use]
258    pub fn stream(&self, key: &Name, r: &impl Resolve) -> Option<Stream> {
259        self.get(key, r)?.as_direct()?.as_stream().cloned()
260    }
261
262    /// The reference an entry holds, without resolving it.
263    #[must_use]
264    pub fn reference(&self, key: &Name) -> Option<crate::ObjRef> {
265        self.raw(key)?.as_ref_id()
266    }
267
268    /// A rectangle read from a four-element array — see [`Array::as_rect`].
269    ///
270    /// Missing or malformed yields the zero rectangle, never `None`: PDF
271    /// consumers of `/MediaBox` and friends all want a rectangle.
272    #[must_use]
273    pub fn rect(&self, key: &Name, r: &impl Resolve) -> Rect {
274        self.array(key, r)
275            .map_or_else(|| Rect::new(0.0, 0.0, 0.0, 0.0), |a| a.as_rect())
276    }
277
278    /// A transformation matrix read from a six-element array — see
279    /// [`Array::as_matrix`]. Missing or malformed yields the identity.
280    #[must_use]
281    pub fn matrix(&self, key: &Name, r: &impl Resolve) -> Affine {
282        self.array(key, r)
283            .map_or(Affine::IDENTITY, |a| a.as_matrix())
284    }
285}
286
287impl FromIterator<(Name, Object)> for Dict {
288    fn from_iter<I: IntoIterator<Item = (Name, Object)>>(iter: I) -> Self {
289        let mut dict = Self::new();
290        for (k, v) in iter {
291            dict.push(k, v);
292        }
293        dict
294    }
295}
296
297impl<'a> IntoIterator for &'a Dict {
298    type Item = &'a (Name, Object);
299    type IntoIter = std::slice::Iter<'a, (Name, Object)>;
300
301    fn into_iter(self) -> Self::IntoIter {
302        self.0.iter()
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use pdfrum_common::kurbo::{Affine, Rect};
309
310    use crate::test_resolve::TestStore;
311    use crate::{Array, Dict, Name, NoResolve, ObjRef, Object, PdfString, Stream, names};
312
313    fn key(s: &str) -> Name {
314        Name::from(s)
315    }
316
317    // From cpdf_dictionary_unittest.cpp:13-37, restated: PDFium's sorted map
318    // iterates alphabetically, we iterate in document order — a stated
319    // divergence, because document order is what a round-trip must preserve.
320    #[test]
321    fn iteration_follows_document_order_not_sort_order() {
322        let dict = Dict::from_pairs([
323            (key("the-dictionary"), Object::Dict(Dict::new())),
324            (key("the-array"), Object::Array(Array::new())),
325            (key("the-number"), Object::Int(42)),
326        ]);
327        let order: Vec<_> = dict.keys().filter_map(Name::as_str).collect();
328        assert_eq!(order, ["the-dictionary", "the-array", "the-number"]);
329    }
330
331    #[test]
332    fn last_duplicate_wins_on_lookup_and_both_are_kept() {
333        let dict = Dict::from_pairs([
334            (key("K"), Object::Int(1)),
335            (key("K"), Object::Int(2)),
336            (key("K"), Object::Int(3)),
337        ]);
338        assert_eq!(dict.raw(&key("K")), Some(&Object::Int(3)));
339        assert_eq!(dict.direct_int(&key("K")), Some(3));
340        assert_eq!(dict.len(), 3, "the file said it three times");
341    }
342
343    // From cpdf_object_unittest.cpp:289-311 (GetNameFor / GetByteStringFor).
344    #[test]
345    fn name_accessor_is_type_filtered_but_byte_string_coerces() {
346        let dict = Dict::from_pairs([
347            (key("bool"), Object::Bool(false)),
348            (key("num"), Object::Real(0.23)),
349            (key("string"), Object::Str(PdfString::literal(b"ium"))),
350            (key("name"), Object::Name(key("Pdf"))),
351        ]);
352
353        assert_eq!(dict.name(&key("invalid")), None);
354        assert_eq!(dict.name(&key("bool")), None);
355        assert_eq!(dict.name(&key("num")), None);
356        assert_eq!(dict.name(&key("string")), None);
357        assert_eq!(dict.name(&key("name")), Some(&key("Pdf")));
358
359        assert_eq!(dict.byte_string(&key("invalid"), &NoResolve), None);
360        assert_eq!(
361            dict.byte_string(&key("bool"), &NoResolve).as_deref(),
362            Some(&b"false"[..])
363        );
364        assert_eq!(
365            dict.byte_string(&key("num"), &NoResolve).as_deref(),
366            Some(&b".23"[..])
367        );
368        assert_eq!(
369            dict.byte_string(&key("string"), &NoResolve).as_deref(),
370            Some(&b"ium"[..])
371        );
372        assert_eq!(
373            dict.byte_string(&key("name"), &NoResolve).as_deref(),
374            Some(&b"Pdf"[..])
375        );
376    }
377
378    #[test]
379    fn boolean_accessor_rejects_integers() {
380        let dict = Dict::from_pairs([
381            (key("flag"), Object::Bool(true)),
382            (key("one"), Object::Int(1)),
383        ]);
384        assert_eq!(dict.bool(&key("flag")), Some(true));
385        assert_eq!(dict.bool(&key("one")), None, "an Int(1) is not a boolean");
386    }
387
388    #[test]
389    fn direct_int_ignores_indirection_while_int_follows_it() {
390        let store = TestStore::from_pairs([(3, Object::Int(99))]);
391        let dict = Dict::from_pairs([
392            (names::PREV.clone(), Object::Ref(ObjRef::new(3, 0))),
393            (names::LENGTH.clone(), Object::Ref(ObjRef::new(3, 0))),
394        ]);
395        // How the cross-reference reader reads /Prev: an indirect value is
396        // ignored, not chased.
397        assert_eq!(dict.direct_int(names::PREV), None);
398        // How the stream reader reads /Length: chased.
399        assert_eq!(dict.int(names::LENGTH, &store), Some(99));
400    }
401
402    #[test]
403    fn resolution_stops_after_one_level() {
404        let store =
405            TestStore::from_pairs([(1, Object::Ref(ObjRef::new(2, 0))), (2, Object::Int(7))]);
406        let dict = Dict::from_pairs([(key("K"), Object::Ref(ObjRef::new(1, 0)))]);
407        // Object 1's body is itself a reference; the value reads as absent.
408        assert_eq!(dict.int(&key("K"), &store), None);
409        assert_eq!(dict.number(&key("K"), &store), None);
410        // ...but the resolution itself succeeded and produced that reference.
411        assert_eq!(
412            dict.get(&key("K"), &store).as_deref(),
413            Some(&Object::Ref(ObjRef::new(2, 0)))
414        );
415    }
416
417    #[test]
418    fn dangling_references_read_as_absent() {
419        let store = TestStore::default();
420        let dict = Dict::from_pairs([(key("K"), Object::Ref(ObjRef::new(9, 0)))]);
421        assert!(dict.get(&key("K"), &store).is_none());
422        assert_eq!(dict.int(&key("K"), &store), None);
423        assert_eq!(dict.dict(&key("K"), &store), None);
424    }
425
426    // From cpdf_object_unittest.cpp:271-288 (GetDict): a stream answers with
427    // its own dictionary, directly or through a reference.
428    #[test]
429    fn dict_accessor_accepts_a_stream() {
430        let inner = Dict::from_pairs([(names::LENGTH.clone(), Object::Int(3))]);
431        let stream = Stream::new(inner.clone(), b"abc".to_vec().into());
432        let store = TestStore::from_pairs([(5, Object::Stream(Box::new(stream)))]);
433        let dict = Dict::from_pairs([(key("S"), Object::Ref(ObjRef::new(5, 0)))]);
434
435        assert_eq!(dict.dict(&key("S"), &store), Some(inner));
436        assert!(dict.stream(&key("S"), &store).is_some());
437        assert_eq!(dict.array(&key("S"), &store), None);
438    }
439
440    // From cpdf_object_unittest.cpp:471-507.
441    #[test]
442    fn rect_and_matrix_need_exactly_the_right_element_count() {
443        let four = Object::Array(Array::of([
444            Object::Int(1),
445            Object::Int(2),
446            Object::Int(3),
447            Object::Int(4),
448        ]));
449        let three = Object::Array(Array::of([Object::Int(1), Object::Int(2), Object::Int(3)]));
450        let six = Object::Array(
451            (1..=6)
452                .map(|i| Object::Int(i64::from(i)))
453                .collect::<Array>(),
454        );
455
456        let dict = Dict::from_pairs([
457            (key("four"), four),
458            (key("three"), three),
459            (key("six"), six),
460        ]);
461
462        assert_eq!(
463            dict.rect(&key("four"), &NoResolve),
464            Rect::new(1.0, 2.0, 3.0, 4.0)
465        );
466        assert_eq!(
467            dict.rect(&key("three"), &NoResolve),
468            Rect::new(0.0, 0.0, 0.0, 0.0)
469        );
470        assert_eq!(
471            dict.rect(&key("missing"), &NoResolve),
472            Rect::new(0.0, 0.0, 0.0, 0.0)
473        );
474
475        assert_eq!(
476            dict.matrix(&key("six"), &NoResolve),
477            Affine::new([1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
478        );
479        assert_eq!(dict.matrix(&key("four"), &NoResolve), Affine::IDENTITY);
480        assert_eq!(dict.matrix(&key("missing"), &NoResolve), Affine::IDENTITY);
481    }
482
483    #[test]
484    fn missing_keys_read_as_their_fallbacks_everywhere() {
485        let dict = Dict::new();
486        let absent = key("nope");
487        assert!(dict.is_empty());
488        assert!(!dict.contains_key(&absent));
489        assert_eq!(dict.raw(&absent), None);
490        assert_eq!(dict.int(&absent, &NoResolve), None);
491        assert_eq!(dict.number(&absent, &NoResolve), None);
492        assert_eq!(dict.name(&absent), None);
493        assert_eq!(dict.bool(&absent), None);
494        assert_eq!(dict.string(&absent), None);
495        assert_eq!(dict.text(&absent, &NoResolve), None);
496        assert_eq!(dict.reference(&absent), None);
497    }
498
499    #[test]
500    fn parsed_nulls_are_stored_like_any_other_value() {
501        let dict = Dict::from_pairs([(key("K"), Object::Null)]);
502        assert!(dict.contains_key(&key("K")));
503        assert_eq!(dict.raw(&key("K")), Some(&Object::Null));
504        assert_eq!(dict.int(&key("K"), &NoResolve), None);
505    }
506
507    #[test]
508    fn insert_replaces_in_place_and_appends_when_new() {
509        let mut d = Dict::new();
510        d.push(Name::from("A"), Object::Int(1));
511        d.push(Name::from("B"), Object::Int(2));
512        d.insert(Name::from("B"), Object::Int(20));
513        assert_eq!(d.raw(&Name::from("B")), Some(&Object::Int(20)));
514        let keys: Vec<_> = d.iter().map(|(k, _)| k.clone()).collect();
515        assert_eq!(keys, vec![Name::from("A"), Name::from("B")]);
516        d.insert(Name::from("C"), Object::Int(3));
517        let keys: Vec<_> = d.iter().map(|(k, _)| k.clone()).collect();
518        assert_eq!(
519            keys,
520            vec![Name::from("A"), Name::from("B"), Name::from("C")]
521        );
522    }
523
524    #[test]
525    fn remove_returns_the_value_and_drops_the_key() {
526        let mut d = Dict::new();
527        d.push(Name::from("A"), Object::Int(1));
528        d.push(Name::from("B"), Object::Null);
529        assert_eq!(d.remove(&Name::from("A")), Some(Object::Int(1)));
530        assert_eq!(d.raw(&Name::from("A")), None);
531        assert_eq!(d.raw(&Name::from("B")), Some(&Object::Null));
532        assert_eq!(d.len(), 1);
533        assert_eq!(d.remove(&Name::from("A")), None);
534    }
535}