Skip to main content

zpdf_core/
object.rs

1use std::collections::BTreeMap;
2use std::fmt;
3use std::sync::Arc;
4
5use crate::{Error, Rect, Result};
6
7/// Indirect object identifier: (object number, generation number).
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
9pub struct ObjectId(pub u32, pub u16);
10
11impl fmt::Display for ObjectId {
12    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13        write!(f, "{} {} R", self.0, self.1)
14    }
15}
16
17/// PDF Name object (e.g., `/Type` stored as `"Type"`).
18#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
19pub struct PdfName(pub String);
20
21impl PdfName {
22    pub fn new(s: impl Into<String>) -> Self {
23        Self(s.into())
24    }
25
26    pub fn as_str(&self) -> &str {
27        &self.0
28    }
29}
30
31impl fmt::Display for PdfName {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        write!(f, "/{}", self.0)
34    }
35}
36
37/// PDF string (literal or hexadecimal).
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct PdfString(pub Vec<u8>);
40
41impl PdfString {
42    pub fn new(bytes: Vec<u8>) -> Self {
43        Self(bytes)
44    }
45
46    pub fn as_bytes(&self) -> &[u8] {
47        &self.0
48    }
49
50    pub fn to_string_lossy(&self) -> String {
51        String::from_utf8_lossy(&self.0).into_owned()
52    }
53}
54
55/// PDF dictionary: ordered map of Name → PdfObject.
56#[derive(Debug, Clone, PartialEq)]
57pub struct PdfDict(pub BTreeMap<PdfName, PdfObject>);
58
59impl PdfDict {
60    pub fn new() -> Self {
61        Self(BTreeMap::new())
62    }
63
64    pub fn get(&self, key: &str) -> Option<&PdfObject> {
65        self.0.get(&PdfName(key.to_string()))
66    }
67
68    pub fn insert(&mut self, key: PdfName, value: PdfObject) {
69        self.0.insert(key, value);
70    }
71
72    pub fn get_name(&self, key: &str) -> Result<&str> {
73        match self.get(key) {
74            Some(PdfObject::Name(n)) => Ok(n.as_str()),
75            Some(other) => Err(Error::TypeMismatch {
76                expected: "Name",
77                actual: other.type_name(),
78            }),
79            None => Err(Error::MissingKey(key.to_string())),
80        }
81    }
82
83    pub fn get_i64(&self, key: &str) -> Result<i64> {
84        match self.get(key) {
85            Some(PdfObject::Integer(n)) => Ok(*n),
86            Some(other) => Err(Error::TypeMismatch {
87                expected: "Integer",
88                actual: other.type_name(),
89            }),
90            None => Err(Error::MissingKey(key.to_string())),
91        }
92    }
93
94    pub fn get_f64(&self, key: &str) -> Result<f64> {
95        match self.get(key) {
96            Some(PdfObject::Real(n)) => Ok(*n),
97            Some(PdfObject::Integer(n)) => Ok(*n as f64),
98            Some(other) => Err(Error::TypeMismatch {
99                expected: "number",
100                actual: other.type_name(),
101            }),
102            None => Err(Error::MissingKey(key.to_string())),
103        }
104    }
105
106    pub fn get_array(&self, key: &str) -> Result<&[PdfObject]> {
107        match self.get(key) {
108            Some(PdfObject::Array(a)) => Ok(a),
109            Some(other) => Err(Error::TypeMismatch {
110                expected: "Array",
111                actual: other.type_name(),
112            }),
113            None => Err(Error::MissingKey(key.to_string())),
114        }
115    }
116
117    pub fn get_dict(&self, key: &str) -> Result<&PdfDict> {
118        match self.get(key) {
119            Some(PdfObject::Dict(d)) => Ok(d),
120            Some(other) => Err(Error::TypeMismatch {
121                expected: "Dict",
122                actual: other.type_name(),
123            }),
124            None => Err(Error::MissingKey(key.to_string())),
125        }
126    }
127
128    pub fn get_ref(&self, key: &str) -> Result<ObjectId> {
129        match self.get(key) {
130            Some(PdfObject::Ref(id)) => Ok(*id),
131            Some(other) => Err(Error::TypeMismatch {
132                expected: "Ref",
133                actual: other.type_name(),
134            }),
135            None => Err(Error::MissingKey(key.to_string())),
136        }
137    }
138
139    pub fn get_rect(&self, key: &str) -> Result<Rect> {
140        let arr = self.get_array(key)?;
141        if arr.len() != 4 {
142            return Err(Error::TypeMismatch {
143                expected: "4-element array (Rect)",
144                actual: "wrong-length array",
145            });
146        }
147        let to_f64 = |obj: &PdfObject| -> Result<f64> {
148            match obj {
149                PdfObject::Real(n) => Ok(*n),
150                PdfObject::Integer(n) => Ok(*n as f64),
151                _ => Err(Error::TypeMismatch {
152                    expected: "number",
153                    actual: obj.type_name(),
154                }),
155            }
156        };
157        Ok(Rect::new(
158            to_f64(&arr[0])?,
159            to_f64(&arr[1])?,
160            to_f64(&arr[2])?,
161            to_f64(&arr[3])?,
162        ))
163    }
164}
165
166impl Default for PdfDict {
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172/// PDF stream object: dictionary + raw byte range (lazy decode).
173#[derive(Debug, Clone, PartialEq)]
174pub struct PdfStream {
175    pub dict: PdfDict,
176    pub data: Arc<[u8]>,
177}
178
179impl PdfStream {
180    pub fn new(dict: PdfDict, data: Vec<u8>) -> Self {
181        Self {
182            dict,
183            data: data.into(),
184        }
185    }
186}
187
188/// All PDF object types.
189#[derive(Debug, Clone, PartialEq)]
190pub enum PdfObject {
191    Null,
192    Bool(bool),
193    Integer(i64),
194    Real(f64),
195    String(PdfString),
196    Name(PdfName),
197    Array(Vec<PdfObject>),
198    Dict(PdfDict),
199    Stream(PdfStream),
200    Ref(ObjectId),
201}
202
203impl PdfObject {
204    pub fn type_name(&self) -> &'static str {
205        match self {
206            PdfObject::Null => "Null",
207            PdfObject::Bool(_) => "Bool",
208            PdfObject::Integer(_) => "Integer",
209            PdfObject::Real(_) => "Real",
210            PdfObject::String(_) => "String",
211            PdfObject::Name(_) => "Name",
212            PdfObject::Array(_) => "Array",
213            PdfObject::Dict(_) => "Dict",
214            PdfObject::Stream(_) => "Stream",
215            PdfObject::Ref(_) => "Ref",
216        }
217    }
218
219    pub fn as_i64(&self) -> Result<i64> {
220        match self {
221            PdfObject::Integer(n) => Ok(*n),
222            _ => Err(Error::TypeMismatch {
223                expected: "Integer",
224                actual: self.type_name(),
225            }),
226        }
227    }
228
229    pub fn as_f64(&self) -> Result<f64> {
230        match self {
231            PdfObject::Real(n) => Ok(*n),
232            PdfObject::Integer(n) => Ok(*n as f64),
233            _ => Err(Error::TypeMismatch {
234                expected: "number",
235                actual: self.type_name(),
236            }),
237        }
238    }
239
240    pub fn as_name(&self) -> Result<&str> {
241        match self {
242            PdfObject::Name(n) => Ok(n.as_str()),
243            _ => Err(Error::TypeMismatch {
244                expected: "Name",
245                actual: self.type_name(),
246            }),
247        }
248    }
249
250    pub fn as_str(&self) -> Result<&PdfString> {
251        match self {
252            PdfObject::String(s) => Ok(s),
253            _ => Err(Error::TypeMismatch {
254                expected: "String",
255                actual: self.type_name(),
256            }),
257        }
258    }
259
260    pub fn as_array(&self) -> Result<&[PdfObject]> {
261        match self {
262            PdfObject::Array(a) => Ok(a),
263            _ => Err(Error::TypeMismatch {
264                expected: "Array",
265                actual: self.type_name(),
266            }),
267        }
268    }
269
270    pub fn as_dict(&self) -> Result<&PdfDict> {
271        match self {
272            PdfObject::Dict(d) => Ok(d),
273            _ => Err(Error::TypeMismatch {
274                expected: "Dict",
275                actual: self.type_name(),
276            }),
277        }
278    }
279
280    pub fn as_stream(&self) -> Result<&PdfStream> {
281        match self {
282            PdfObject::Stream(s) => Ok(s),
283            _ => Err(Error::TypeMismatch {
284                expected: "Stream",
285                actual: self.type_name(),
286            }),
287        }
288    }
289
290    pub fn as_ref(&self) -> Result<ObjectId> {
291        match self {
292            PdfObject::Ref(id) => Ok(*id),
293            _ => Err(Error::TypeMismatch {
294                expected: "Ref",
295                actual: self.type_name(),
296            }),
297        }
298    }
299
300    pub fn is_null(&self) -> bool {
301        matches!(self, PdfObject::Null)
302    }
303}
304
305impl fmt::Display for PdfObject {
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307        match self {
308            PdfObject::Null => write!(f, "null"),
309            PdfObject::Bool(b) => write!(f, "{}", if *b { "true" } else { "false" }),
310            PdfObject::Integer(n) => write!(f, "{n}"),
311            PdfObject::Real(n) => write!(f, "{n}"),
312            PdfObject::String(s) => write!(f, "({})", s.to_string_lossy()),
313            PdfObject::Name(n) => write!(f, "{n}"),
314            PdfObject::Array(a) => {
315                write!(f, "[")?;
316                for (i, obj) in a.iter().enumerate() {
317                    if i > 0 {
318                        write!(f, " ")?;
319                    }
320                    write!(f, "{obj}")?;
321                }
322                write!(f, "]")
323            }
324            PdfObject::Dict(d) => {
325                write!(f, "<< ")?;
326                for (k, v) in &d.0 {
327                    write!(f, "{k} {v} ")?;
328                }
329                write!(f, ">>")
330            }
331            PdfObject::Stream(s) => {
332                write!(f, "<< ")?;
333                for (k, v) in &s.dict.0 {
334                    write!(f, "{k} {v} ")?;
335                }
336                write!(f, ">> stream({} bytes)", s.data.len())
337            }
338            PdfObject::Ref(id) => write!(f, "{id}"),
339        }
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346
347    #[test]
348    fn dict_accessors() {
349        let mut d = PdfDict::new();
350        d.insert(PdfName::new("Type"), PdfObject::Name(PdfName::new("Page")));
351        d.insert(PdfName::new("Count"), PdfObject::Integer(5));
352
353        assert_eq!(d.get_name("Type").unwrap(), "Page");
354        assert_eq!(d.get_i64("Count").unwrap(), 5);
355        assert!(d.get_name("Missing").is_err());
356    }
357
358    #[test]
359    fn dict_get_rect() {
360        let mut d = PdfDict::new();
361        d.insert(
362            PdfName::new("MediaBox"),
363            PdfObject::Array(vec![
364                PdfObject::Integer(0),
365                PdfObject::Integer(0),
366                PdfObject::Real(612.0),
367                PdfObject::Real(792.0),
368            ]),
369        );
370        let r = d.get_rect("MediaBox").unwrap();
371        assert!((r.x1 - 612.0).abs() < 1e-10);
372        assert!((r.y1 - 792.0).abs() < 1e-10);
373    }
374
375    #[test]
376    fn object_display() {
377        let obj = PdfObject::Dict(PdfDict::new());
378        assert_eq!(format!("{obj}"), "<< >>");
379
380        let obj = PdfObject::Ref(ObjectId(12, 0));
381        assert_eq!(format!("{obj}"), "12 0 R");
382    }
383}