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