Skip to main content

zpdf_core/
object.rs

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