Skip to main content

rust_pdf_box/cos/
object.rs

1use crate::error::{PdfError, PdfResult};
2use flate2::Compression;
3use flate2::read::ZlibDecoder;
4use flate2::write::ZlibEncoder;
5use std::collections::BTreeMap;
6use std::fmt;
7use std::io::{Read, Write};
8
9/// An indirect object identifier in PDF: object number and generation number.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct ObjectId {
12    pub num: u32,
13    pub generation: u16,
14}
15
16impl ObjectId {
17    pub const fn new(num: u32, generation: u16) -> Self {
18        Self { num, generation }
19    }
20}
21
22impl fmt::Display for ObjectId {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        write!(f, "{} {} R", self.num, self.generation)
25    }
26}
27
28/// Representation format for PDF strings.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum StringFormat {
31    Literal,
32    Hex,
33}
34
35/// A PDF string object (can be literal or hex formatted).
36#[derive(Clone, PartialEq, Eq)]
37pub struct COSString {
38    pub data: Vec<u8>,
39    pub format: StringFormat,
40}
41
42impl COSString {
43    pub fn literal(data: impl Into<Vec<u8>>) -> Self {
44        Self {
45            data: data.into(),
46            format: StringFormat::Literal,
47        }
48    }
49
50    pub fn hex(data: impl Into<Vec<u8>>) -> Self {
51        Self {
52            data: data.into(),
53            format: StringFormat::Hex,
54        }
55    }
56
57    pub fn as_str(&self) -> Result<&str, std::str::Utf8Error> {
58        std::str::from_utf8(&self.data)
59    }
60
61    pub fn to_string_lossy(&self) -> String {
62        // PDF strings may be PDFDocEncoding or UTF-16BE with BOM (0xFE 0xFF)
63        if self.data.len() >= 2 && self.data[0] == 0xFE && self.data[1] == 0xFF {
64            let u16s: Vec<u16> = self.data[2..]
65                .chunks_exact(2)
66                .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
67                .collect();
68            String::from_utf16_lossy(&u16s)
69        } else {
70            String::from_utf8_lossy(&self.data).into_owned()
71        }
72    }
73}
74
75impl fmt::Debug for COSString {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        match self.format {
78            StringFormat::Literal => write!(f, "COSString(\"{}\")", self.to_string_lossy()),
79            StringFormat::Hex => {
80                let hex_str: String = self.data.iter().map(|b| format!("{:02X}", b)).collect();
81                write!(f, "COSString(<{}>)", hex_str)
82            }
83        }
84    }
85}
86
87/// A PDF Name object (e.g. /Type, /Catalog, /Page).
88#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
89pub struct COSName(pub String);
90
91impl COSName {
92    pub fn new(name: impl Into<String>) -> Self {
93        let mut s = name.into();
94        if s.starts_with('/') {
95            s.remove(0);
96        }
97        Self(s)
98    }
99
100    pub fn as_str(&self) -> &str {
101        &self.0
102    }
103}
104
105impl fmt::Debug for COSName {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        write!(f, "/{}", self.0)
108    }
109}
110
111impl fmt::Display for COSName {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        write!(f, "/{}", self.0)
114    }
115}
116
117impl<T: Into<String>> From<T> for COSName {
118    fn from(s: T) -> Self {
119        Self::new(s)
120    }
121}
122
123/// A PDF Array object.
124#[derive(Debug, Clone, PartialEq, Default)]
125pub struct COSArray(pub Vec<COSObject>);
126
127impl COSArray {
128    pub fn new() -> Self {
129        Self(Vec::new())
130    }
131
132    pub fn with_capacity(capacity: usize) -> Self {
133        Self(Vec::with_capacity(capacity))
134    }
135
136    pub fn push<T: Into<COSObject>>(&mut self, obj: T) {
137        self.0.push(obj.into());
138    }
139
140    pub fn len(&self) -> usize {
141        self.0.len()
142    }
143
144    pub fn is_empty(&self) -> bool {
145        self.0.is_empty()
146    }
147
148    pub fn get(&self, index: usize) -> Option<&COSObject> {
149        self.0.get(index)
150    }
151
152    pub fn iter(&self) -> std::slice::Iter<'_, COSObject> {
153        self.0.iter()
154    }
155}
156
157/// A PDF Dictionary object (`/Key Value`).
158#[derive(Debug, Clone, PartialEq, Default)]
159pub struct COSDictionary {
160    pub entries: BTreeMap<COSName, COSObject>,
161}
162
163impl COSDictionary {
164    pub fn new() -> Self {
165        Self {
166            entries: BTreeMap::new(),
167        }
168    }
169
170    pub fn get(&self, key: &str) -> Option<&COSObject> {
171        let name = COSName::new(key);
172        self.entries.get(&name)
173    }
174
175    pub fn get_mut(&mut self, key: &str) -> Option<&mut COSObject> {
176        let name = COSName::new(key);
177        self.entries.get_mut(&name)
178    }
179
180    pub fn set<K: Into<COSName>, V: Into<COSObject>>(&mut self, key: K, val: V) {
181        self.entries.insert(key.into(), val.into());
182    }
183
184    pub fn remove(&mut self, key: &str) -> Option<COSObject> {
185        let name = COSName::new(key);
186        self.entries.remove(&name)
187    }
188
189    pub fn contains_key(&self, key: &str) -> bool {
190        self.get(key).is_some()
191    }
192
193    pub fn get_name(&self, key: &str) -> Option<&str> {
194        match self.get(key) {
195            Some(COSObject::Name(n)) => Some(n.as_str()),
196            _ => None,
197        }
198    }
199
200    pub fn get_integer(&self, key: &str) -> Option<i64> {
201        match self.get(key) {
202            Some(COSObject::Integer(i)) => Some(*i),
203            _ => None,
204        }
205    }
206
207    pub fn get_float(&self, key: &str) -> Option<f64> {
208        match self.get(key) {
209            Some(COSObject::Real(f)) => Some(*f),
210            Some(COSObject::Integer(i)) => Some(*i as f64),
211            _ => None,
212        }
213    }
214
215    pub fn get_bool(&self, key: &str) -> Option<bool> {
216        match self.get(key) {
217            Some(COSObject::Boolean(b)) => Some(*b),
218            _ => None,
219        }
220    }
221
222    pub fn get_string(&self, key: &str) -> Option<&COSString> {
223        match self.get(key) {
224            Some(COSObject::String(s)) => Some(s),
225            _ => None,
226        }
227    }
228
229    pub fn get_dict(&self, key: &str) -> Option<&COSDictionary> {
230        match self.get(key) {
231            Some(COSObject::Dictionary(d)) => Some(d),
232            _ => None,
233        }
234    }
235
236    pub fn get_dict_mut(&mut self, key: &str) -> Option<&mut COSDictionary> {
237        let name = COSName::new(key);
238        match self.entries.get_mut(&name) {
239            Some(COSObject::Dictionary(d)) => Some(d),
240            _ => None,
241        }
242    }
243
244    pub fn get_array(&self, key: &str) -> Option<&COSArray> {
245        match self.get(key) {
246            Some(COSObject::Array(a)) => Some(a),
247            _ => None,
248        }
249    }
250
251    pub fn get_reference(&self, key: &str) -> Option<ObjectId> {
252        match self.get(key) {
253            Some(COSObject::Reference(id)) => Some(*id),
254            _ => None,
255        }
256    }
257}
258
259/// A PDF Stream object (Dictionary + raw data).
260#[derive(Debug, Clone, PartialEq)]
261pub struct COSStream {
262    pub dict: COSDictionary,
263    pub data: Vec<u8>,
264}
265
266impl COSStream {
267    pub fn new(dict: COSDictionary, data: Vec<u8>) -> Self {
268        let mut s = Self { dict, data };
269        s.update_length();
270        s
271    }
272
273    pub fn update_length(&mut self) {
274        self.dict.set(
275            COSName::new("Length"),
276            COSObject::Integer(self.data.len() as i64),
277        );
278    }
279
280    /// Decodes stream contents based on /Filter entries.
281    pub fn decode(&self) -> PdfResult<Vec<u8>> {
282        let filter = self.dict.get_name("Filter");
283        match filter {
284            None => Ok(self.data.clone()),
285            Some("FlateDecode") => {
286                let mut decoder = ZlibDecoder::new(&self.data[..]);
287                let mut decoded = Vec::new();
288                decoder.read_to_end(&mut decoded).map_err(|e| {
289                    PdfError::StreamCompression(format!("Flate decompression error: {e}"))
290                })?;
291                Ok(decoded)
292            }
293            Some(other) => Err(PdfError::Unsupported(format!(
294                "Unsupported stream filter: {other}"
295            ))),
296        }
297    }
298
299    /// Compresses uncompressed stream contents with FlateDecode.
300    pub fn encode_flate(&mut self) -> PdfResult<()> {
301        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
302        encoder
303            .write_all(&self.data)
304            .map_err(|e| PdfError::StreamCompression(format!("Flate compression error: {e}")))?;
305        let compressed = encoder.finish().map_err(|e| {
306            PdfError::StreamCompression(format!("Flate compression finish error: {e}"))
307        })?;
308
309        self.data = compressed;
310        self.dict
311            .set(COSName::new("Filter"), COSName::new("FlateDecode"));
312        self.update_length();
313        Ok(())
314    }
315}
316
317/// The fundamental COS Object enum in PDF.
318#[derive(Debug, Clone, PartialEq)]
319pub enum COSObject {
320    Null,
321    Boolean(bool),
322    Integer(i64),
323    Real(f64),
324    String(COSString),
325    Name(COSName),
326    Array(COSArray),
327    Dictionary(COSDictionary),
328    Stream(COSStream),
329    Reference(ObjectId),
330}
331
332impl COSObject {
333    pub const fn null() -> Self {
334        Self::Null
335    }
336
337    pub fn type_name(&self) -> &'static str {
338        match self {
339            Self::Null => "null",
340            Self::Boolean(_) => "boolean",
341            Self::Integer(_) => "integer",
342            Self::Real(_) => "real",
343            Self::String(_) => "string",
344            Self::Name(_) => "name",
345            Self::Array(_) => "array",
346            Self::Dictionary(_) => "dictionary",
347            Self::Stream(_) => "stream",
348            Self::Reference(_) => "reference",
349        }
350    }
351
352    pub fn as_integer(&self) -> Option<i64> {
353        match self {
354            Self::Integer(i) => Some(*i),
355            _ => None,
356        }
357    }
358
359    pub fn as_float(&self) -> Option<f64> {
360        match self {
361            Self::Real(f) => Some(*f),
362            Self::Integer(i) => Some(*i as f64),
363            _ => None,
364        }
365    }
366
367    pub fn as_bool(&self) -> Option<bool> {
368        match self {
369            Self::Boolean(b) => Some(*b),
370            _ => None,
371        }
372    }
373
374    pub fn as_name(&self) -> Option<&str> {
375        match self {
376            Self::Name(n) => Some(n.as_str()),
377            _ => None,
378        }
379    }
380
381    pub fn as_string(&self) -> Option<&COSString> {
382        match self {
383            Self::String(s) => Some(s),
384            _ => None,
385        }
386    }
387
388    pub fn as_dict(&self) -> Option<&COSDictionary> {
389        match self {
390            Self::Dictionary(d) => Some(d),
391            _ => None,
392        }
393    }
394
395    pub fn as_dict_mut(&mut self) -> Option<&mut COSDictionary> {
396        match self {
397            Self::Dictionary(d) => Some(d),
398            _ => None,
399        }
400    }
401
402    pub fn as_array(&self) -> Option<&COSArray> {
403        match self {
404            Self::Array(a) => Some(a),
405            _ => None,
406        }
407    }
408
409    pub fn as_stream(&self) -> Option<&COSStream> {
410        match self {
411            Self::Stream(s) => Some(s),
412            _ => None,
413        }
414    }
415
416    pub fn as_reference(&self) -> Option<ObjectId> {
417        match self {
418            Self::Reference(id) => Some(*id),
419            _ => None,
420        }
421    }
422}
423
424// Ergonomic From implementations
425impl From<bool> for COSObject {
426    fn from(b: bool) -> Self {
427        Self::Boolean(b)
428    }
429}
430
431impl From<i64> for COSObject {
432    fn from(i: i64) -> Self {
433        Self::Integer(i)
434    }
435}
436
437impl From<i32> for COSObject {
438    fn from(i: i32) -> Self {
439        Self::Integer(i as i64)
440    }
441}
442
443impl From<u32> for COSObject {
444    fn from(i: u32) -> Self {
445        Self::Integer(i as i64)
446    }
447}
448
449impl From<usize> for COSObject {
450    fn from(i: usize) -> Self {
451        Self::Integer(i as i64)
452    }
453}
454
455impl From<f64> for COSObject {
456    fn from(f: f64) -> Self {
457        Self::Real(f)
458    }
459}
460
461impl From<f32> for COSObject {
462    fn from(f: f32) -> Self {
463        Self::Real(f as f64)
464    }
465}
466
467impl From<COSName> for COSObject {
468    fn from(n: COSName) -> Self {
469        Self::Name(n)
470    }
471}
472
473impl From<COSString> for COSObject {
474    fn from(s: COSString) -> Self {
475        Self::String(s)
476    }
477}
478
479impl From<COSDictionary> for COSObject {
480    fn from(d: COSDictionary) -> Self {
481        Self::Dictionary(d)
482    }
483}
484
485impl From<COSArray> for COSObject {
486    fn from(a: COSArray) -> Self {
487        Self::Array(a)
488    }
489}
490
491impl From<COSStream> for COSObject {
492    fn from(s: COSStream) -> Self {
493        Self::Stream(s)
494    }
495}
496
497impl From<ObjectId> for COSObject {
498    fn from(id: ObjectId) -> Self {
499        Self::Reference(id)
500    }
501}
502
503impl From<&str> for COSObject {
504    fn from(s: &str) -> Self {
505        Self::String(COSString::literal(s))
506    }
507}
508
509impl From<String> for COSObject {
510    fn from(s: String) -> Self {
511        Self::String(COSString::literal(s))
512    }
513}