Skip to main content

pdfboss_core/
object.rs

1//! The PDF object model: the nine basic object types plus indirect
2//! references (ISO 32000 §7.3).
3
4use std::borrow::Borrow;
5
6use crate::hash::FastMap;
7
8/// An indirect object reference: object number and generation number.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct ObjRef {
11    pub num: u32,
12    pub gen: u16,
13}
14
15/// A name object, stored decoded (any `#xx` escapes already resolved) and
16/// without the leading solidus.
17#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub struct Name(pub String);
19
20impl Borrow<str> for Name {
21    fn borrow(&self) -> &str {
22        &self.0
23    }
24}
25
26/// A dictionary object mapping names to objects.
27#[derive(Debug, Clone, Default, PartialEq)]
28pub struct Dict(FastMap<Name, Object>);
29
30impl Dict {
31    /// Creates an empty dictionary.
32    pub fn new() -> Dict {
33        Dict::default()
34    }
35
36    /// Looks up a value by key (without the leading solidus).
37    ///
38    /// Covers ISO 32000-1 §7.3.7.
39    pub fn get(&self, key: &str) -> Option<&Object> {
40        self.0.get(key)
41    }
42
43    /// Inserts a key/value pair, returning the previous value if any.
44    pub fn insert(&mut self, key: Name, value: Object) -> Option<Object> {
45        self.0.insert(key, value)
46    }
47
48    /// Removes a key, returning its value if present.
49    pub fn remove(&mut self, key: &str) -> Option<Object> {
50        self.0.remove(key)
51    }
52
53    /// Iterates over all key/value pairs (unordered).
54    pub fn iter(&self) -> impl Iterator<Item = (&Name, &Object)> {
55        self.0.iter()
56    }
57
58    /// Mutable iteration over all values (used by in-place decryption).
59    pub(crate) fn values_mut(&mut self) -> impl Iterator<Item = &mut Object> {
60        self.0.values_mut()
61    }
62
63    /// Number of entries.
64    pub fn len(&self) -> usize {
65        self.0.len()
66    }
67
68    /// Whether the dictionary has no entries.
69    pub fn is_empty(&self) -> bool {
70        self.0.is_empty()
71    }
72
73    /// Typed lookup: integer value.
74    pub fn get_int(&self, key: &str) -> Option<i64> {
75        self.get(key)?.as_int()
76    }
77
78    /// Typed lookup: numeric value as `f64` (integers coerce).
79    pub fn get_f64(&self, key: &str) -> Option<f64> {
80        self.get(key)?.as_f64()
81    }
82
83    /// Typed lookup: name value.
84    pub fn get_name(&self, key: &str) -> Option<&Name> {
85        self.get(key)?.as_name()
86    }
87
88    /// Typed lookup: array value.
89    pub fn get_array(&self, key: &str) -> Option<&[Object]> {
90        self.get(key)?.as_array()
91    }
92
93    /// Typed lookup: dictionary value.
94    pub fn get_dict(&self, key: &str) -> Option<&Dict> {
95        self.get(key)?.as_dict()
96    }
97
98    /// Typed lookup: indirect reference value.
99    pub fn get_ref(&self, key: &str) -> Option<ObjRef> {
100        self.get(key)?.as_ref()
101    }
102}
103
104/// A stream object: its dictionary plus the raw, still-encoded data bytes.
105#[derive(Debug, Clone, PartialEq)]
106pub struct Stream {
107    pub dict: Dict,
108    /// Raw stream bytes as stored in the file (filters not yet applied).
109    pub data: Vec<u8>,
110}
111
112/// Any PDF object (ISO 32000 §7.3).
113#[derive(Debug, Clone, PartialEq)]
114pub enum Object {
115    Null,
116    Bool(bool),
117    Int(i64),
118    Real(f64),
119    /// A string object as raw bytes (may be text or binary data).
120    String(Vec<u8>),
121    Name(Name),
122    Array(Vec<Object>),
123    Dict(Dict),
124    Stream(Stream),
125    Ref(ObjRef),
126}
127
128impl Object {
129    /// Boolean value, if this is a `Bool`.
130    pub fn as_bool(&self) -> Option<bool> {
131        match self {
132            Object::Bool(b) => Some(*b),
133            _ => None,
134        }
135    }
136
137    /// Integer value, if this is an `Int`.
138    pub fn as_int(&self) -> Option<i64> {
139        match self {
140            Object::Int(i) => Some(*i),
141            _ => None,
142        }
143    }
144
145    /// Numeric value as `f64`; `Int` coerces.
146    pub fn as_f64(&self) -> Option<f64> {
147        match self {
148            Object::Int(i) => Some(*i as f64),
149            Object::Real(r) => Some(*r),
150            _ => None,
151        }
152    }
153
154    /// Raw string bytes, if this is a `String`.
155    ///
156    /// Covers ISO 32000-1 §7.9.2 and §7.9.2.4.
157    pub fn as_str_bytes(&self) -> Option<&[u8]> {
158        match self {
159            Object::String(bytes) => Some(bytes),
160            _ => None,
161        }
162    }
163
164    /// Name value, if this is a `Name`.
165    pub fn as_name(&self) -> Option<&Name> {
166        match self {
167            Object::Name(n) => Some(n),
168            _ => None,
169        }
170    }
171
172    /// Array contents, if this is an `Array`.
173    pub fn as_array(&self) -> Option<&[Object]> {
174        match self {
175            Object::Array(items) => Some(items),
176            _ => None,
177        }
178    }
179
180    /// Dictionary, if this is a `Dict` (or the dictionary of a `Stream`).
181    pub fn as_dict(&self) -> Option<&Dict> {
182        match self {
183            Object::Dict(d) => Some(d),
184            Object::Stream(s) => Some(&s.dict),
185            _ => None,
186        }
187    }
188
189    /// Stream, if this is a `Stream`.
190    pub fn as_stream(&self) -> Option<&Stream> {
191        match self {
192            Object::Stream(s) => Some(s),
193            _ => None,
194        }
195    }
196
197    /// Indirect reference, if this is a `Ref`.
198    #[allow(clippy::should_implement_trait)] // accessor family named per spec
199    pub fn as_ref(&self) -> Option<ObjRef> {
200        match self {
201            Object::Ref(r) => Some(*r),
202            _ => None,
203        }
204    }
205
206    /// Whether this object is `Null`.
207    ///
208    /// Covers ISO 32000-1 §7.3.9.
209    pub fn is_null(&self) -> bool {
210        matches!(self, Object::Null)
211    }
212}
213
214/// The character a PDFDocEncoding byte stands for, or `None` for the codes
215/// the encoding leaves undefined (0x00 to 0x17 except tab, line feed and
216/// carriage return, 0x7F, 0x9F and 0xAD). The encoding agrees with Latin-1
217/// except for the eight accents at 0x18 to 0x1F, the punctuation and
218/// ligatures at 0x80 to 0x9E and the Euro sign at 0xA0.
219///
220/// Covers ISO 32000-1 §7.9.2.3 and Annex D.3.
221pub fn pdf_doc_char(code: u8) -> Option<char> {
222    let c = match code {
223        0x09 | 0x0A | 0x0D | 0x20..=0x7E | 0xA1..=0xAC | 0xAE..=0xFF => char::from(code),
224        0x18 => '\u{2D8}',
225        0x19 => '\u{2C7}',
226        0x1A => '\u{2C6}',
227        0x1B => '\u{2D9}',
228        0x1C => '\u{2DD}',
229        0x1D => '\u{2DB}',
230        0x1E => '\u{2DA}',
231        0x1F => '\u{2DC}',
232        0x80 => '\u{2022}',
233        0x81 => '\u{2020}',
234        0x82 => '\u{2021}',
235        0x83 => '\u{2026}',
236        0x84 => '\u{2014}',
237        0x85 => '\u{2013}',
238        0x86 => '\u{192}',
239        0x87 => '\u{2044}',
240        0x88 => '\u{2039}',
241        0x89 => '\u{203A}',
242        0x8A => '\u{2212}',
243        0x8B => '\u{2030}',
244        0x8C => '\u{201E}',
245        0x8D => '\u{201C}',
246        0x8E => '\u{201D}',
247        0x8F => '\u{2018}',
248        0x90 => '\u{2019}',
249        0x91 => '\u{201A}',
250        0x92 => '\u{2122}',
251        0x93 => '\u{FB01}',
252        0x94 => '\u{FB02}',
253        0x95 => '\u{141}',
254        0x96 => '\u{152}',
255        0x97 => '\u{160}',
256        0x98 => '\u{178}',
257        0x99 => '\u{17D}',
258        0x9A => '\u{131}',
259        0x9B => '\u{142}',
260        0x9C => '\u{153}',
261        0x9D => '\u{161}',
262        0x9E => '\u{17E}',
263        0xA0 => '\u{20AC}',
264        _ => return None,
265    };
266    Some(c)
267}
268
269/// Decodes a PDF text string: UTF-16BE with BOM, UTF-8 with BOM (PDF 2.0),
270/// otherwise PDFDocEncoding, with the replacement character for the codes
271/// that encoding leaves undefined.
272///
273/// Covers ISO 32000-1 §7.9.2, §7.9.2.2, §7.9.2.3 and Annex D.3.
274pub fn decode_text_string(bytes: &[u8]) -> String {
275    if let Some(rest) = bytes.strip_prefix(&[0xFE, 0xFF]) {
276        let units = rest
277            .as_chunks::<2>()
278            .0
279            .iter()
280            .map(|pair| u16::from_be_bytes([pair[0], pair[1]]));
281        let mut out: String = std::char::decode_utf16(units)
282            .map(|r| r.unwrap_or(char::REPLACEMENT_CHARACTER))
283            .collect();
284        if rest.len() % 2 == 1 {
285            // A dangling trailing byte cannot form a UTF-16 code unit.
286            out.push(char::REPLACEMENT_CHARACTER);
287        }
288        out
289    } else if let Some(rest) = bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]) {
290        String::from_utf8_lossy(rest).into_owned()
291    } else {
292        bytes
293            .iter()
294            .map(|&b| pdf_doc_char(b).unwrap_or(char::REPLACEMENT_CHARACTER))
295            .collect()
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    fn name(s: &str) -> Name {
304        Name(s.to_string())
305    }
306
307    #[test]
308    fn accessors_return_some_for_matching_variant() {
309        assert_eq!(Object::Bool(true).as_bool(), Some(true));
310        assert_eq!(Object::Bool(false).as_bool(), Some(false));
311        assert_eq!(Object::Int(-7).as_int(), Some(-7));
312        assert_eq!(Object::Real(1.5).as_f64(), Some(1.5));
313        assert_eq!(
314            Object::String(b"abc".to_vec()).as_str_bytes(),
315            Some(b"abc".as_slice())
316        );
317        assert_eq!(Object::Name(name("Type")).as_name(), Some(&name("Type")));
318        let arr = Object::Array(vec![Object::Int(1), Object::Null]);
319        assert_eq!(arr.as_array().map(<[Object]>::len), Some(2));
320        let mut d = Dict::new();
321        d.insert(name("K"), Object::Int(9));
322        assert_eq!(Object::Dict(d.clone()).as_dict(), Some(&d));
323        let r = ObjRef { num: 12, gen: 3 };
324        assert_eq!(Object::Ref(r).as_ref(), Some(r));
325        assert!(Object::Null.is_null());
326        assert!(!Object::Int(0).is_null());
327    }
328
329    #[test]
330    fn accessors_return_none_for_other_variants() {
331        let o = Object::Int(1);
332        assert_eq!(o.as_bool(), None);
333        assert_eq!(o.as_str_bytes(), None);
334        assert_eq!(o.as_name(), None);
335        assert_eq!(o.as_array(), None);
336        assert_eq!(o.as_dict(), None);
337        assert_eq!(o.as_stream(), None);
338        assert_eq!(o.as_ref(), None);
339        assert_eq!(Object::Real(2.0).as_int(), None);
340        assert_eq!(Object::Bool(true).as_f64(), None);
341        assert_eq!(Object::Null.as_f64(), None);
342    }
343
344    #[test]
345    fn as_f64_coerces_int() {
346        assert_eq!(Object::Int(42).as_f64(), Some(42.0));
347        assert_eq!(Object::Int(-3).as_f64(), Some(-3.0));
348        assert_eq!(Object::Real(0.25).as_f64(), Some(0.25));
349    }
350
351    #[test]
352    fn as_dict_sees_stream_dict() {
353        let mut d = Dict::new();
354        d.insert(name("Length"), Object::Int(5));
355        let s = Stream {
356            dict: d.clone(),
357            data: b"hello".to_vec(),
358        };
359        let o = Object::Stream(s.clone());
360        assert_eq!(o.as_dict(), Some(&d));
361        assert_eq!(o.as_stream(), Some(&s));
362    }
363
364    #[test]
365    fn dict_basic_operations() {
366        let mut d = Dict::new();
367        assert!(d.is_empty());
368        assert_eq!(d.len(), 0);
369        assert_eq!(d.insert(name("A"), Object::Int(1)), None);
370        assert_eq!(
371            d.insert(name("A"), Object::Int(2)),
372            Some(Object::Int(1)),
373            "insert returns the previous value"
374        );
375        d.insert(name("B"), Object::Bool(true));
376        assert_eq!(d.len(), 2);
377        assert!(!d.is_empty());
378        assert_eq!(d.get("A"), Some(&Object::Int(2)));
379        assert_eq!(d.get("Missing"), None);
380        let mut keys: Vec<&str> = d.iter().map(|(k, _)| k.0.as_str()).collect();
381        keys.sort_unstable();
382        assert_eq!(keys, vec!["A", "B"]);
383        assert_eq!(d.remove("A"), Some(Object::Int(2)));
384        assert_eq!(d.remove("A"), None);
385        assert_eq!(d.len(), 1);
386    }
387
388    #[test]
389    fn dict_typed_helpers() {
390        let mut inner = Dict::new();
391        inner.insert(name("X"), Object::Null);
392        let mut d = Dict::new();
393        d.insert(name("Int"), Object::Int(7));
394        d.insert(name("Real"), Object::Real(2.5));
395        d.insert(name("Name"), Object::Name(name("Page")));
396        d.insert(name("Arr"), Object::Array(vec![Object::Int(1)]));
397        d.insert(name("Dict"), Object::Dict(inner.clone()));
398        d.insert(name("Ref"), Object::Ref(ObjRef { num: 4, gen: 0 }));
399
400        assert_eq!(d.get_int("Int"), Some(7));
401        assert_eq!(d.get_int("Real"), None, "reals do not coerce to int");
402        assert_eq!(d.get_f64("Int"), Some(7.0), "ints coerce to f64");
403        assert_eq!(d.get_f64("Real"), Some(2.5));
404        assert_eq!(d.get_name("Name"), Some(&name("Page")));
405        assert_eq!(d.get_array("Arr"), Some([Object::Int(1)].as_slice()));
406        assert_eq!(d.get_dict("Dict"), Some(&inner));
407        assert_eq!(d.get_ref("Ref"), Some(ObjRef { num: 4, gen: 0 }));
408
409        // Missing key and wrong type both yield None.
410        assert_eq!(d.get_int("Nope"), None);
411        assert_eq!(d.get_name("Int"), None);
412        assert_eq!(d.get_array("Dict"), None);
413        assert_eq!(d.get_dict("Arr"), None);
414        assert_eq!(d.get_ref("Int"), None);
415    }
416
417    // Covers ISO 32000-1 §7.9.2 and §7.9.2.2.
418    #[test]
419    fn decode_utf16be_with_bom() {
420        assert_eq!(
421            decode_text_string(&[0xFE, 0xFF, 0x00, 0x48, 0x00, 0x69]),
422            "Hi"
423        );
424        // Surrogate pair: U+1D11E.
425        assert_eq!(
426            decode_text_string(&[0xFE, 0xFF, 0xD8, 0x34, 0xDD, 0x1E]),
427            "\u{1D11E}"
428        );
429        // BOM only decodes to the empty string.
430        assert_eq!(decode_text_string(&[0xFE, 0xFF]), "");
431        // A lone surrogate becomes U+FFFD.
432        assert_eq!(decode_text_string(&[0xFE, 0xFF, 0xD8, 0x34]), "\u{FFFD}");
433        // An odd trailing byte becomes U+FFFD.
434        assert_eq!(
435            decode_text_string(&[0xFE, 0xFF, 0x00, 0x48, 0x12]),
436            "H\u{FFFD}"
437        );
438    }
439
440    // Covers ISO 32000-1 §7.9.2.2.
441    #[test]
442    fn decode_utf8_with_bom() {
443        let mut bytes = vec![0xEF, 0xBB, 0xBF];
444        bytes.extend_from_slice("héllo €".as_bytes());
445        assert_eq!(decode_text_string(&bytes), "héllo €");
446        assert_eq!(decode_text_string(&[0xEF, 0xBB, 0xBF]), "");
447        // Invalid UTF-8 after the BOM is replaced, not an error.
448        assert_eq!(decode_text_string(&[0xEF, 0xBB, 0xBF, 0xFF]), "\u{FFFD}");
449    }
450
451    // Covers ISO 32000-1 §7.9.2, §7.9.2.3 and Annex D.3.
452    #[test]
453    fn decode_without_bom_uses_pdf_doc_encoding() {
454        assert_eq!(decode_text_string(b"Hello"), "Hello");
455        assert_eq!(decode_text_string(&[0x48, 0xE9]), "Hé");
456        assert_eq!(decode_text_string(&[0xFF]), "ÿ");
457        assert_eq!(decode_text_string(&[]), "");
458        // A lone 0xFE (no full UTF-16 BOM) is PDFDocEncoding's thorn.
459        assert_eq!(decode_text_string(&[0xFE]), "\u{FE}");
460    }
461
462    // Covers ISO 32000-1 §7.9.2.3 and Annex D.3: the codes where
463    // PDFDocEncoding departs from Latin-1.
464    #[test]
465    fn decode_pdf_doc_encoding_accents_at_0x18() {
466        assert_eq!(
467            decode_text_string(&[0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F]),
468            "\u{2D8}\u{2C7}\u{2C6}\u{2D9}\u{2DD}\u{2DB}\u{2DA}\u{2DC}"
469        );
470    }
471
472    // Covers ISO 32000-1 §7.9.2.3 and Annex D.3.
473    #[test]
474    fn decode_pdf_doc_encoding_punctuation_at_0x80() {
475        assert_eq!(
476            decode_text_string(&[0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87]),
477            "•†‡…—–ƒ⁄"
478        );
479        assert_eq!(
480            decode_text_string(&[0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F]),
481            "‹›−‰„“”‘"
482        );
483        assert_eq!(
484            decode_text_string(&[0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97]),
485            "’‚™fiflŁŒŠ"
486        );
487        assert_eq!(
488            decode_text_string(&[0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E]),
489            "ŸŽıłœšž"
490        );
491        assert_eq!(decode_text_string(&[0xA0]), "€");
492    }
493
494    // Covers ISO 32000-1 §7.9.2.3 and Annex D.3: everywhere else the two
495    // agree, so Latin-1 text keeps decoding as before.
496    #[test]
497    fn decode_pdf_doc_encoding_agrees_with_latin1_elsewhere() {
498        assert_eq!(
499            decode_text_string(&[0x09, 0x0A, 0x0D, 0x20, 0x7E, 0xA1, 0xE9, 0xFE, 0xFF]),
500            "\t\n\r ~¡éþÿ"
501        );
502    }
503
504    // Covers ISO 32000-1 §7.9.2.3 and Annex D.3: codes the encoding leaves
505    // undefined decode to the replacement character instead of a guess.
506    #[test]
507    fn decode_pdf_doc_encoding_undefined_codes_are_replaced() {
508        assert_eq!(decode_text_string(&[0x9F, 0xAD]), "\u{FFFD}\u{FFFD}");
509        assert_eq!(decode_text_string(&[0x00, 0x7F]), "\u{FFFD}\u{FFFD}");
510    }
511}