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    pub fn get(&self, key: &str) -> Option<&Object> {
38        self.0.get(key)
39    }
40
41    /// Inserts a key/value pair, returning the previous value if any.
42    pub fn insert(&mut self, key: Name, value: Object) -> Option<Object> {
43        self.0.insert(key, value)
44    }
45
46    /// Removes a key, returning its value if present.
47    pub fn remove(&mut self, key: &str) -> Option<Object> {
48        self.0.remove(key)
49    }
50
51    /// Iterates over all key/value pairs (unordered).
52    pub fn iter(&self) -> impl Iterator<Item = (&Name, &Object)> {
53        self.0.iter()
54    }
55
56    /// Mutable iteration over all values (used by in-place decryption).
57    pub(crate) fn values_mut(&mut self) -> impl Iterator<Item = &mut Object> {
58        self.0.values_mut()
59    }
60
61    /// Number of entries.
62    pub fn len(&self) -> usize {
63        self.0.len()
64    }
65
66    /// Whether the dictionary has no entries.
67    pub fn is_empty(&self) -> bool {
68        self.0.is_empty()
69    }
70
71    /// Typed lookup: integer value.
72    pub fn get_int(&self, key: &str) -> Option<i64> {
73        self.get(key)?.as_int()
74    }
75
76    /// Typed lookup: numeric value as `f64` (integers coerce).
77    pub fn get_f64(&self, key: &str) -> Option<f64> {
78        self.get(key)?.as_f64()
79    }
80
81    /// Typed lookup: name value.
82    pub fn get_name(&self, key: &str) -> Option<&Name> {
83        self.get(key)?.as_name()
84    }
85
86    /// Typed lookup: array value.
87    pub fn get_array(&self, key: &str) -> Option<&[Object]> {
88        self.get(key)?.as_array()
89    }
90
91    /// Typed lookup: dictionary value.
92    pub fn get_dict(&self, key: &str) -> Option<&Dict> {
93        self.get(key)?.as_dict()
94    }
95
96    /// Typed lookup: indirect reference value.
97    pub fn get_ref(&self, key: &str) -> Option<ObjRef> {
98        self.get(key)?.as_ref()
99    }
100}
101
102/// A stream object: its dictionary plus the raw, still-encoded data bytes.
103#[derive(Debug, Clone, PartialEq)]
104pub struct Stream {
105    pub dict: Dict,
106    /// Raw stream bytes as stored in the file (filters not yet applied).
107    pub data: Vec<u8>,
108}
109
110/// Any PDF object (ISO 32000 §7.3).
111#[derive(Debug, Clone, PartialEq)]
112pub enum Object {
113    Null,
114    Bool(bool),
115    Int(i64),
116    Real(f64),
117    /// A string object as raw bytes (may be text or binary data).
118    String(Vec<u8>),
119    Name(Name),
120    Array(Vec<Object>),
121    Dict(Dict),
122    Stream(Stream),
123    Ref(ObjRef),
124}
125
126impl Object {
127    /// Boolean value, if this is a `Bool`.
128    pub fn as_bool(&self) -> Option<bool> {
129        match self {
130            Object::Bool(b) => Some(*b),
131            _ => None,
132        }
133    }
134
135    /// Integer value, if this is an `Int`.
136    pub fn as_int(&self) -> Option<i64> {
137        match self {
138            Object::Int(i) => Some(*i),
139            _ => None,
140        }
141    }
142
143    /// Numeric value as `f64`; `Int` coerces.
144    pub fn as_f64(&self) -> Option<f64> {
145        match self {
146            Object::Int(i) => Some(*i as f64),
147            Object::Real(r) => Some(*r),
148            _ => None,
149        }
150    }
151
152    /// Raw string bytes, if this is a `String`.
153    pub fn as_str_bytes(&self) -> Option<&[u8]> {
154        match self {
155            Object::String(bytes) => Some(bytes),
156            _ => None,
157        }
158    }
159
160    /// Name value, if this is a `Name`.
161    pub fn as_name(&self) -> Option<&Name> {
162        match self {
163            Object::Name(n) => Some(n),
164            _ => None,
165        }
166    }
167
168    /// Array contents, if this is an `Array`.
169    pub fn as_array(&self) -> Option<&[Object]> {
170        match self {
171            Object::Array(items) => Some(items),
172            _ => None,
173        }
174    }
175
176    /// Dictionary, if this is a `Dict` (or the dictionary of a `Stream`).
177    pub fn as_dict(&self) -> Option<&Dict> {
178        match self {
179            Object::Dict(d) => Some(d),
180            Object::Stream(s) => Some(&s.dict),
181            _ => None,
182        }
183    }
184
185    /// Stream, if this is a `Stream`.
186    pub fn as_stream(&self) -> Option<&Stream> {
187        match self {
188            Object::Stream(s) => Some(s),
189            _ => None,
190        }
191    }
192
193    /// Indirect reference, if this is a `Ref`.
194    #[allow(clippy::should_implement_trait)] // accessor family named per spec
195    pub fn as_ref(&self) -> Option<ObjRef> {
196        match self {
197            Object::Ref(r) => Some(*r),
198            _ => None,
199        }
200    }
201
202    /// Whether this object is `Null`.
203    pub fn is_null(&self) -> bool {
204        matches!(self, Object::Null)
205    }
206}
207
208/// Decodes a PDF text string: UTF-16BE with BOM, UTF-8 with BOM (PDF 2.0),
209/// otherwise byte-per-char fallback in the spirit of PDFDocEncoding
210/// (approximately Latin-1).
211pub fn decode_text_string(bytes: &[u8]) -> String {
212    if let Some(rest) = bytes.strip_prefix(&[0xFE, 0xFF]) {
213        let units = rest
214            .as_chunks::<2>()
215            .0
216            .iter()
217            .map(|pair| u16::from_be_bytes([pair[0], pair[1]]));
218        let mut out: String = std::char::decode_utf16(units)
219            .map(|r| r.unwrap_or(char::REPLACEMENT_CHARACTER))
220            .collect();
221        if rest.len() % 2 == 1 {
222            // A dangling trailing byte cannot form a UTF-16 code unit.
223            out.push(char::REPLACEMENT_CHARACTER);
224        }
225        out
226    } else if let Some(rest) = bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]) {
227        String::from_utf8_lossy(rest).into_owned()
228    } else {
229        // PDFDocEncoding-flavored fallback: each byte maps to the Unicode
230        // scalar of the same value (Latin-1).
231        bytes.iter().map(|&b| char::from(b)).collect()
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    fn name(s: &str) -> Name {
240        Name(s.to_string())
241    }
242
243    #[test]
244    fn accessors_return_some_for_matching_variant() {
245        assert_eq!(Object::Bool(true).as_bool(), Some(true));
246        assert_eq!(Object::Bool(false).as_bool(), Some(false));
247        assert_eq!(Object::Int(-7).as_int(), Some(-7));
248        assert_eq!(Object::Real(1.5).as_f64(), Some(1.5));
249        assert_eq!(
250            Object::String(b"abc".to_vec()).as_str_bytes(),
251            Some(b"abc".as_slice())
252        );
253        assert_eq!(Object::Name(name("Type")).as_name(), Some(&name("Type")));
254        let arr = Object::Array(vec![Object::Int(1), Object::Null]);
255        assert_eq!(arr.as_array().map(<[Object]>::len), Some(2));
256        let mut d = Dict::new();
257        d.insert(name("K"), Object::Int(9));
258        assert_eq!(Object::Dict(d.clone()).as_dict(), Some(&d));
259        let r = ObjRef { num: 12, gen: 3 };
260        assert_eq!(Object::Ref(r).as_ref(), Some(r));
261        assert!(Object::Null.is_null());
262        assert!(!Object::Int(0).is_null());
263    }
264
265    #[test]
266    fn accessors_return_none_for_other_variants() {
267        let o = Object::Int(1);
268        assert_eq!(o.as_bool(), None);
269        assert_eq!(o.as_str_bytes(), None);
270        assert_eq!(o.as_name(), None);
271        assert_eq!(o.as_array(), None);
272        assert_eq!(o.as_dict(), None);
273        assert_eq!(o.as_stream(), None);
274        assert_eq!(o.as_ref(), None);
275        assert_eq!(Object::Real(2.0).as_int(), None);
276        assert_eq!(Object::Bool(true).as_f64(), None);
277        assert_eq!(Object::Null.as_f64(), None);
278    }
279
280    #[test]
281    fn as_f64_coerces_int() {
282        assert_eq!(Object::Int(42).as_f64(), Some(42.0));
283        assert_eq!(Object::Int(-3).as_f64(), Some(-3.0));
284        assert_eq!(Object::Real(0.25).as_f64(), Some(0.25));
285    }
286
287    #[test]
288    fn as_dict_sees_stream_dict() {
289        let mut d = Dict::new();
290        d.insert(name("Length"), Object::Int(5));
291        let s = Stream {
292            dict: d.clone(),
293            data: b"hello".to_vec(),
294        };
295        let o = Object::Stream(s.clone());
296        assert_eq!(o.as_dict(), Some(&d));
297        assert_eq!(o.as_stream(), Some(&s));
298    }
299
300    #[test]
301    fn dict_basic_operations() {
302        let mut d = Dict::new();
303        assert!(d.is_empty());
304        assert_eq!(d.len(), 0);
305        assert_eq!(d.insert(name("A"), Object::Int(1)), None);
306        assert_eq!(
307            d.insert(name("A"), Object::Int(2)),
308            Some(Object::Int(1)),
309            "insert returns the previous value"
310        );
311        d.insert(name("B"), Object::Bool(true));
312        assert_eq!(d.len(), 2);
313        assert!(!d.is_empty());
314        assert_eq!(d.get("A"), Some(&Object::Int(2)));
315        assert_eq!(d.get("Missing"), None);
316        let mut keys: Vec<&str> = d.iter().map(|(k, _)| k.0.as_str()).collect();
317        keys.sort_unstable();
318        assert_eq!(keys, vec!["A", "B"]);
319        assert_eq!(d.remove("A"), Some(Object::Int(2)));
320        assert_eq!(d.remove("A"), None);
321        assert_eq!(d.len(), 1);
322    }
323
324    #[test]
325    fn dict_typed_helpers() {
326        let mut inner = Dict::new();
327        inner.insert(name("X"), Object::Null);
328        let mut d = Dict::new();
329        d.insert(name("Int"), Object::Int(7));
330        d.insert(name("Real"), Object::Real(2.5));
331        d.insert(name("Name"), Object::Name(name("Page")));
332        d.insert(name("Arr"), Object::Array(vec![Object::Int(1)]));
333        d.insert(name("Dict"), Object::Dict(inner.clone()));
334        d.insert(name("Ref"), Object::Ref(ObjRef { num: 4, gen: 0 }));
335
336        assert_eq!(d.get_int("Int"), Some(7));
337        assert_eq!(d.get_int("Real"), None, "reals do not coerce to int");
338        assert_eq!(d.get_f64("Int"), Some(7.0), "ints coerce to f64");
339        assert_eq!(d.get_f64("Real"), Some(2.5));
340        assert_eq!(d.get_name("Name"), Some(&name("Page")));
341        assert_eq!(d.get_array("Arr"), Some([Object::Int(1)].as_slice()));
342        assert_eq!(d.get_dict("Dict"), Some(&inner));
343        assert_eq!(d.get_ref("Ref"), Some(ObjRef { num: 4, gen: 0 }));
344
345        // Missing key and wrong type both yield None.
346        assert_eq!(d.get_int("Nope"), None);
347        assert_eq!(d.get_name("Int"), None);
348        assert_eq!(d.get_array("Dict"), None);
349        assert_eq!(d.get_dict("Arr"), None);
350        assert_eq!(d.get_ref("Int"), None);
351    }
352
353    #[test]
354    fn decode_utf16be_with_bom() {
355        assert_eq!(
356            decode_text_string(&[0xFE, 0xFF, 0x00, 0x48, 0x00, 0x69]),
357            "Hi"
358        );
359        // Surrogate pair: U+1D11E.
360        assert_eq!(
361            decode_text_string(&[0xFE, 0xFF, 0xD8, 0x34, 0xDD, 0x1E]),
362            "\u{1D11E}"
363        );
364        // BOM only decodes to the empty string.
365        assert_eq!(decode_text_string(&[0xFE, 0xFF]), "");
366        // A lone surrogate becomes U+FFFD.
367        assert_eq!(decode_text_string(&[0xFE, 0xFF, 0xD8, 0x34]), "\u{FFFD}");
368        // An odd trailing byte becomes U+FFFD.
369        assert_eq!(
370            decode_text_string(&[0xFE, 0xFF, 0x00, 0x48, 0x12]),
371            "H\u{FFFD}"
372        );
373    }
374
375    #[test]
376    fn decode_utf8_with_bom() {
377        let mut bytes = vec![0xEF, 0xBB, 0xBF];
378        bytes.extend_from_slice("héllo €".as_bytes());
379        assert_eq!(decode_text_string(&bytes), "héllo €");
380        assert_eq!(decode_text_string(&[0xEF, 0xBB, 0xBF]), "");
381        // Invalid UTF-8 after the BOM is replaced, not an error.
382        assert_eq!(decode_text_string(&[0xEF, 0xBB, 0xBF, 0xFF]), "\u{FFFD}");
383    }
384
385    #[test]
386    fn decode_latin1_fallback() {
387        assert_eq!(decode_text_string(b"Hello"), "Hello");
388        assert_eq!(decode_text_string(&[0x48, 0xE9]), "Hé");
389        assert_eq!(decode_text_string(&[0xFF]), "ÿ");
390        assert_eq!(decode_text_string(&[]), "");
391        // A lone 0xFE (no full UTF-16 BOM) falls back to Latin-1.
392        assert_eq!(decode_text_string(&[0xFE]), "\u{FE}");
393    }
394}