Skip to main content

rpdfium_parser/
object.rs

1// Derived from PDFium's cpdf_object.h / cpdf_object.cpp
2// Original: Copyright 2014 The PDFium Authors
3// Licensed under BSD-3-Clause / Apache-2.0
4// See pdfium-upstream/LICENSE for the original license.
5
6//! PDF object model — ObjectId, StreamData, and the Object enum.
7
8use std::collections::HashMap;
9
10pub use rpdfium_core::error::ObjectId;
11use rpdfium_core::{Name, PdfString};
12
13/// Represents the raw (undecoded) data of a PDF stream.
14///
15/// Stream data is not read or decoded at parse time. Only the byte range
16/// within the source data is recorded for later on-demand decoding.
17#[derive(Debug, Clone)]
18pub enum StreamData {
19    /// Undecoded: byte range within source data.
20    Raw { offset: u64, length: u64 },
21    /// Already decoded/generated data (used by editing operations).
22    Decoded { data: Vec<u8> },
23}
24
25/// A PDF object value.
26///
27/// `Object::Reference` holds an unresolved indirect reference — it is **not**
28/// recursively resolved at parse time to avoid `OnceLock` deadlocks.
29#[derive(Debug, Clone)]
30pub enum Object {
31    /// The PDF null object.
32    Null,
33    /// A boolean value.
34    Boolean(bool),
35    /// An integer number.
36    Integer(i64),
37    /// A real (floating-point) number.
38    Real(f64),
39    /// A PDF string (encoding-aware).
40    String(PdfString),
41    /// A PDF name.
42    Name(Name),
43    /// An array of objects.
44    Array(Vec<Object>),
45    /// A dictionary mapping names to objects.
46    Dictionary(HashMap<Name, Object>),
47    /// A stream object with dictionary metadata and raw data reference.
48    Stream {
49        dict: HashMap<Name, Object>,
50        data: StreamData,
51    },
52    /// An unresolved indirect reference to another object.
53    Reference(ObjectId),
54}
55
56impl Object {
57    /// Returns the boolean value if this is `Object::Boolean`.
58    pub fn as_bool(&self) -> Option<bool> {
59        match self {
60            Object::Boolean(b) => Some(*b),
61            _ => None,
62        }
63    }
64
65    /// Returns the integer value if this is `Object::Integer`.
66    pub fn as_i64(&self) -> Option<i64> {
67        match self {
68            Object::Integer(n) => Some(*n),
69            _ => None,
70        }
71    }
72
73    /// Returns the floating-point value if this is `Object::Real` or `Object::Integer`.
74    pub fn as_f64(&self) -> Option<f64> {
75        match self {
76            Object::Real(f) => Some(*f),
77            Object::Integer(n) => Some(*n as f64),
78            _ => None,
79        }
80    }
81
82    /// Returns a reference to the `PdfString` if this is `Object::String`.
83    pub fn as_string(&self) -> Option<&PdfString> {
84        match self {
85            Object::String(s) => Some(s),
86            _ => None,
87        }
88    }
89
90    /// Returns a reference to the `Name` if this is `Object::Name`.
91    pub fn as_name(&self) -> Option<&Name> {
92        match self {
93            Object::Name(n) => Some(n),
94            _ => None,
95        }
96    }
97
98    /// Returns a reference to the array if this is `Object::Array`.
99    pub fn as_array(&self) -> Option<&[Object]> {
100        match self {
101            Object::Array(a) => Some(a),
102            _ => None,
103        }
104    }
105
106    /// Returns a reference to the dictionary if this is `Object::Dictionary`.
107    pub fn as_dict(&self) -> Option<&HashMap<Name, Object>> {
108        match self {
109            Object::Dictionary(d) => Some(d),
110            _ => None,
111        }
112    }
113
114    /// Returns a reference to the stream dictionary if this is `Object::Stream`.
115    pub fn as_stream_dict(&self) -> Option<&HashMap<Name, Object>> {
116        match self {
117            Object::Stream { dict, .. } => Some(dict),
118            _ => None,
119        }
120    }
121
122    /// Returns the `ObjectId` if this is `Object::Reference`.
123    pub fn as_reference(&self) -> Option<ObjectId> {
124        match self {
125            Object::Reference(id) => Some(*id),
126            _ => None,
127        }
128    }
129
130    /// Returns a mutable reference to the dictionary if this is `Object::Dictionary`.
131    pub fn as_dict_mut(&mut self) -> Option<&mut HashMap<Name, Object>> {
132        match self {
133            Object::Dictionary(d) => Some(d),
134            _ => None,
135        }
136    }
137
138    /// Returns a mutable reference to the array if this is `Object::Array`.
139    pub fn as_array_mut(&mut self) -> Option<&mut Vec<Object>> {
140        match self {
141            Object::Array(a) => Some(a),
142            _ => None,
143        }
144    }
145
146    /// Returns `true` if this is `Object::Null`.
147    pub fn is_null(&self) -> bool {
148        matches!(self, Object::Null)
149    }
150
151    /// Returns `true` if this is `Object::Reference`.
152    pub fn is_reference(&self) -> bool {
153        matches!(self, Object::Reference(_))
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn object_id_display_format() {
163        let id = ObjectId::new(42, 0);
164        assert_eq!(format!("{}", id), "42 0 R");
165    }
166
167    #[test]
168    fn object_id_equality() {
169        let a = ObjectId::new(1, 0);
170        let b = ObjectId::new(1, 0);
171        let c = ObjectId::new(1, 1);
172        assert_eq!(a, b);
173        assert_ne!(a, c);
174    }
175
176    #[test]
177    fn object_null() {
178        let obj = Object::Null;
179        assert!(obj.is_null());
180        assert_eq!(obj.as_bool(), None);
181        assert_eq!(obj.as_i64(), None);
182    }
183
184    #[test]
185    fn object_boolean() {
186        let obj = Object::Boolean(true);
187        assert_eq!(obj.as_bool(), Some(true));
188        assert_eq!(obj.as_i64(), None);
189    }
190
191    #[test]
192    fn object_integer() {
193        let obj = Object::Integer(42);
194        assert_eq!(obj.as_i64(), Some(42));
195        assert_eq!(obj.as_f64(), Some(42.0));
196        assert_eq!(obj.as_bool(), None);
197    }
198
199    #[test]
200    #[allow(clippy::approx_constant)]
201    fn object_real() {
202        let obj = Object::Real(3.14);
203        assert_eq!(obj.as_f64(), Some(3.14));
204        assert_eq!(obj.as_i64(), None);
205    }
206
207    #[test]
208    fn object_string() {
209        let obj = Object::String(PdfString::from_bytes(b"hello".to_vec()));
210        assert!(obj.as_string().is_some());
211        assert_eq!(obj.as_string().unwrap().as_bytes(), b"hello");
212    }
213
214    #[test]
215    fn object_name() {
216        let obj = Object::Name(Name::from_bytes(b"Type".to_vec()));
217        assert!(obj.as_name().is_some());
218    }
219
220    #[test]
221    fn object_array() {
222        let obj = Object::Array(vec![Object::Integer(1), Object::Integer(2)]);
223        assert_eq!(obj.as_array().unwrap().len(), 2);
224    }
225
226    #[test]
227    fn object_dictionary() {
228        let mut dict = HashMap::new();
229        dict.insert(
230            Name::r#type(),
231            Object::Name(Name::from_bytes(b"Catalog".to_vec())),
232        );
233        let obj = Object::Dictionary(dict);
234        assert!(obj.as_dict().is_some());
235    }
236
237    #[test]
238    fn object_stream() {
239        let dict = HashMap::new();
240        let data = StreamData::Raw {
241            offset: 0,
242            length: 100,
243        };
244        let obj = Object::Stream { dict, data };
245        assert!(obj.as_stream_dict().is_some());
246    }
247
248    #[test]
249    fn object_reference() {
250        let id = ObjectId::new(5, 0);
251        let obj = Object::Reference(id);
252        assert!(obj.is_reference());
253        assert_eq!(obj.as_reference(), Some(id));
254    }
255}