Skip to main content

xberg_native_pdf/
object.rs

1//! PDF object types.
2
3use crate::error::{Error, Result};
4
5/// PDF object representation.
6#[derive(Debug, Clone, PartialEq)]
7pub enum Object {
8    /// Null object
9    Null,
10    /// Boolean value
11    Boolean(bool),
12    /// Integer value
13    Integer(i64),
14    /// Real (floating-point) value
15    Real(f64),
16    /// String (byte array)
17    String(Vec<u8>),
18    /// Name (starting with /)
19    Name(String),
20    /// Array of objects
21    Array(Vec<Object>),
22    /// Dictionary (key-value pairs)
23    Dictionary(std::collections::HashMap<String, Object>),
24    /// Stream (dictionary + data)
25    Stream {
26        /// Stream dictionary
27        dict: std::collections::HashMap<String, Object>,
28        /// Stream data
29        data: bytes::Bytes,
30    },
31    /// Indirect object reference
32    Reference(ObjectRef),
33}
34
35/// Reference to an indirect object.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
37pub struct ObjectRef {
38    /// Object number
39    pub id: u32,
40    /// Generation number
41    pub generation: u16,
42}
43
44impl ObjectRef {
45    /// Create a new object reference.
46    pub fn new(id: u32, generation: u16) -> Self {
47        Self { id, generation }
48    }
49}
50
51impl std::fmt::Display for ObjectRef {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        write!(f, "{} {} R", self.id, self.generation)
54    }
55}
56
57/// Encode a Rust `&str` as a PDF text string byte sequence.
58///
59/// Uses PDFDocEncoding (direct code-point byte) for strings whose characters
60/// all fall within U+0000–U+00FF.  Falls back to UTF-16BE with leading BOM
61/// (`0xFE 0xFF`) for any string that contains a code point above U+00FF.
62///
63/// This is the encoding required by ISO 32000-2 §7.9.2 for all PDF text
64/// strings (metadata fields, annotation text, bookmark titles, form field
65/// values, page-label prefixes, etc.).
66pub fn encode_pdf_text_string(s: &str) -> Vec<u8> {
67    if s.chars().all(|c| (c as u32) <= 0x00FF) {
68        s.chars().map(|c| c as u8).collect()
69    } else {
70        let mut buf = vec![0xFE_u8, 0xFF];
71        for unit in s.encode_utf16() {
72            buf.push((unit >> 8) as u8);
73            buf.push((unit & 0xFF) as u8);
74        }
75        buf
76    }
77}
78
79impl Object {
80    /// Create a PDF text string object from a Rust string.
81    ///
82    /// Accepts `&str`, `String`, or any type that implements `AsRef<str>`.
83    /// Encodes using PDFDocEncoding for characters within U+0000–U+00FF,
84    /// or UTF-16BE with BOM for strings that contain characters above U+00FF.
85    pub fn text_string(s: impl AsRef<str>) -> Self {
86        Object::String(encode_pdf_text_string(s.as_ref()))
87    }
88
89    /// Get the type name of this object (without data).
90    ///
91    /// Returns a human-readable type name like "String", "Array", "Dictionary", etc.
92    /// without including the actual data content.
93    pub fn type_name(&self) -> &'static str {
94        match self {
95            Object::Null => "Null",
96            Object::Boolean(_) => "Boolean",
97            Object::Integer(_) => "Integer",
98            Object::Real(_) => "Real",
99            Object::String(_) => "String",
100            Object::Name(_) => "Name",
101            Object::Array(_) => "Array",
102            Object::Dictionary(_) => "Dictionary",
103            Object::Stream { .. } => "Stream",
104            Object::Reference(_) => "Reference",
105        }
106    }
107
108    /// Try to cast to integer.
109    pub fn as_integer(&self) -> Option<i64> {
110        match self {
111            Object::Integer(i) => Some(*i),
112            _ => None,
113        }
114    }
115
116    /// Try to cast to name.
117    pub fn as_name(&self) -> Option<&str> {
118        match self {
119            Object::Name(s) => Some(s),
120            _ => None,
121        }
122    }
123
124    /// Try to cast to dictionary. Works for both Dictionary and Stream objects.
125    pub fn as_dict(&self) -> Option<&std::collections::HashMap<String, Object>> {
126        match self {
127            Object::Dictionary(d) => Some(d),
128            Object::Stream { dict, .. } => Some(dict),
129            _ => None,
130        }
131    }
132
133    /// Try to cast to array.
134    pub fn as_array(&self) -> Option<&Vec<Object>> {
135        match self {
136            Object::Array(arr) => Some(arr),
137            _ => None,
138        }
139    }
140
141    /// Try to cast to reference.
142    pub fn as_reference(&self) -> Option<ObjectRef> {
143        match self {
144            Object::Reference(r) => Some(*r),
145            _ => None,
146        }
147    }
148
149    /// Try to cast to boolean.
150    pub fn as_bool(&self) -> Option<bool> {
151        match self {
152            Object::Boolean(b) => Some(*b),
153            _ => None,
154        }
155    }
156
157    /// Try to cast to real number.
158    pub fn as_real(&self) -> Option<f64> {
159        match self {
160            Object::Real(r) => Some(*r),
161            _ => None,
162        }
163    }
164
165    /// Try to cast to string (bytes).
166    pub fn as_string(&self) -> Option<&[u8]> {
167        match self {
168            Object::String(s) => Some(s),
169            _ => None,
170        }
171    }
172
173    /// Check if object is null.
174    pub fn is_null(&self) -> bool {
175        matches!(self, Object::Null)
176    }
177
178    /// Decode stream data using filters specified in the stream dictionary.
179    ///
180    /// This is a convenience method that calls `decode_stream_data_with_decryption`
181    /// with no encryption parameters.
182    ///
183    /// # Returns
184    ///
185    /// The decoded stream data, or an error if this is not a stream object
186    /// or if decoding fails.
187    ///
188    /// # Examples
189    ///
190    /// ```rust,no_run
191    /// use xberg_native_pdf::object::Object;
192    ///
193    /// # fn example(stream_obj: Object) -> Result<(), Box<dyn std::error::Error>> {
194    /// let decoded_data = stream_obj.decode_stream_data()?;
195    /// # Ok(())
196    /// # }
197    /// ```
198    pub fn decode_stream_data(&self) -> Result<Vec<u8>> {
199        self.decode_stream_data_with_decryption(None, 0, 0)
200    }
201
202    pub(crate) fn decode_image_stream_data(&self, expected_filter_output_size: usize) -> Result<Vec<u8>> {
203        self.decode_stream_data_with_decryption_and_expected_size(None, 0, 0, Some(expected_filter_output_size))
204    }
205
206    /// Decode stream data with optional decryption.
207    ///
208    /// PDF Spec: Section 7.6.2 - General Encryption Algorithm states that streams
209    /// must be decrypted BEFORE applying filters (decompression).
210    ///
211    /// # Arguments
212    ///
213    /// * `decryption_fn` - Optional decryption function (from EncryptionHandler)
214    /// * `obj_num` - Object number (for encryption key derivation)
215    /// * `gen_num` - Generation number (for encryption key derivation)
216    ///
217    /// # Returns
218    ///
219    /// The decoded stream data, or an error if decoding/decryption fails.
220    pub fn decode_stream_data_with_decryption(
221        &self,
222        decryption_fn: Option<&dyn Fn(&[u8]) -> Result<Vec<u8>>>,
223        obj_num: u32,
224        gen_num: u32,
225    ) -> Result<Vec<u8>> {
226        self.decode_stream_data_with_decryption_and_expected_size(decryption_fn, obj_num, gen_num, None)
227    }
228
229    pub(crate) fn decode_stream_data_with_decryption_and_expected_size(
230        &self,
231        decryption_fn: Option<&dyn Fn(&[u8]) -> Result<Vec<u8>>>,
232        obj_num: u32,
233        gen_num: u32,
234        expected_filter_output_size: Option<usize>,
235    ) -> Result<Vec<u8>> {
236        match self {
237            Object::Stream { dict, data } => {
238                // Step 1: Decrypt stream data BEFORE applying filters
239                // PDF Spec: Section 7.6.2 - Encryption must be applied before compression ~keep
240                let decrypted_data = decrypt_stream_data(data, decryption_fn, obj_num, gen_num)?;
241
242                // Step 2: Apply filters (decompression)
243                // PDF Spec: ISO 32000-1:2008, Section 7.3.8.2 - Stream Objects ~keep
244                let filters = dict.get("Filter").map(extract_filter_names).unwrap_or_default();
245
246                if filters.is_empty() {
247                    Ok(decrypted_data)
248                } else {
249                    // Get decode parameters if present
250                    // PDF Spec: ISO 32000-1:2008, Section 7.4.2 - DecodeParms ~keep
251                    let decode_params = extract_decode_params(dict.get("DecodeParms"));
252
253                    if let Some(expected_size) = expected_filter_output_size {
254                        crate::decoders::decode_stream_with_params_and_expected_size(
255                            &decrypted_data,
256                            &filters,
257                            decode_params.as_ref(),
258                            expected_size,
259                        )
260                    } else {
261                        crate::decoders::decode_stream_with_params(&decrypted_data, &filters, decode_params.as_ref())
262                    }
263                }
264            }
265            Object::Dictionary(dict) => {
266                // Per ISO 32000, every stream is a dictionary. Some PDFs (e.g.,
267                // SafeDocs Dialect-StreamIsDict.pdf) store objects as plain
268                // dictionaries where a stream is expected. Treat as empty stream. ~keep
269                tracing::warn!("dictionary used where stream expected; treating as empty stream");
270                let filters = dict.get("Filter").map(extract_filter_names).unwrap_or_default();
271                if filters.is_empty() {
272                    Ok(Vec::new())
273                } else {
274                    let decode_params = extract_decode_params(dict.get("DecodeParms"));
275                    crate::decoders::decode_stream_with_params(&[], &filters, decode_params.as_ref())
276                }
277            }
278            _ => Err(Error::InvalidObjectType {
279                expected: "Stream".to_string(),
280                found: self.type_name().to_string(),
281            }),
282        }
283    }
284}
285
286/// Decrypts a stream's raw bytes before any filter is applied, or returns them unchanged
287/// when there is no decryption function (per ISO 32000-1:2008 §7.6.2, decryption happens
288/// before decompression).
289///
290/// IMPORTANT: for encrypted streams, whitespace must NOT be trimmed before decryption —
291/// encrypted data is binary and trimming could corrupt it (especially for AES, where the
292/// first 16 bytes are the IV). Unencrypted `data` is likewise returned untrimmed: the
293/// parser's `parse_stream_data` already consumes exactly one EOL after the `stream`
294/// keyword per §7.3.8.1, so `data` begins with the first byte of actual stream content —
295/// re-trimming CR/LF here would corrupt binary streams that legitimately start with 0x0A
296/// or 0x0D (e.g. an Indexed palette whose first CMYK byte is 0x0D). ~keep
297///
298/// Split out of [`Object::decode_stream_data_with_decryption_and_expected_size`] purely
299/// to shorten that method.
300fn decrypt_stream_data(
301    data: &[u8],
302    decryption_fn: Option<&dyn Fn(&[u8]) -> Result<Vec<u8>>>,
303    obj_num: u32,
304    gen_num: u32,
305) -> Result<Vec<u8>> {
306    let Some(decrypt) = decryption_fn else {
307        return Ok(data.to_vec());
308    };
309
310    tracing::debug!(
311        object_id = obj_num,
312        generation = gen_num,
313        bytes = data.len(),
314        "decrypting stream"
315    );
316    match decrypt(data) {
317        Ok(data) => {
318            tracing::debug!(bytes = data.len(), "decryption successful");
319            Ok(data)
320        }
321        Err(error) => {
322            tracing::error!(
323                object_id = obj_num,
324                generation = gen_num,
325                error_code = error.telemetry_code(),
326                error_offset = ?error.telemetry_offset(),
327                "decryption failed"
328            );
329            Err(error)
330        }
331    }
332}
333
334/// Extract filter names from a Filter object.
335///
336/// The Filter entry can be either:
337/// - A single Name (e.g., /FlateDecode)
338/// - An Array of Names (e.g., [/ASCII85Decode /FlateDecode])
339fn extract_filter_names(filter_obj: &Object) -> Vec<String> {
340    match filter_obj {
341        Object::Name(name) => vec![name.clone()],
342        Object::Array(arr) => arr
343            .iter()
344            .filter_map(|obj| obj.as_name().map(|s| s.to_string()))
345            .collect(),
346        _ => vec![],
347    }
348}
349
350/// Extract decode parameters from a DecodeParms object.
351///
352/// PDF Spec: ISO 32000-1:2008, Section 7.4.2 - LZWDecode and FlateDecode Parameters
353///
354/// The DecodeParms entry can be:
355/// - A dictionary (for single filter)
356/// - An array of dictionaries (for multiple filters)
357/// - Null or absent (no parameters)
358///
359/// This function extracts predictor parameters used for PNG/TIFF encoding.
360pub(crate) fn extract_decode_params(params_obj: Option<&Object>) -> Option<crate::decoders::DecodeParams> {
361    let dict = match params_obj? {
362        Object::Dictionary(d) => d,
363        Object::Array(arr) => arr.iter().filter_map(|obj| obj.as_dict()).next()?,
364        _ => return None,
365    };
366
367    // Extract predictor parameters per PDF Spec Table 3.7 ~keep
368    let predictor = dict.get("Predictor").and_then(|obj| obj.as_integer()).unwrap_or(1); // Default: no prediction ~keep
369
370    let columns = dict.get("Columns").and_then(|obj| obj.as_integer()).unwrap_or(1) as usize;
371
372    let colors = dict.get("Colors").and_then(|obj| obj.as_integer()).unwrap_or(1) as usize;
373
374    let bits_per_component = dict
375        .get("BitsPerComponent")
376        .and_then(|obj| obj.as_integer())
377        .unwrap_or(8) as usize;
378
379    Some(crate::decoders::DecodeParams {
380        predictor,
381        columns,
382        colors,
383        bits_per_component,
384    })
385}
386
387/// Extract CCITT-specific decode parameters from a DecodeParms object.
388///
389/// PDF Spec: ISO 32000-1:2008, Section 7.4.6 - CCITTFaxDecode Filter Parameters
390///
391/// The DecodeParms entry can be:
392/// - A dictionary (for single filter)
393/// - An array of dictionaries (for multiple filters)
394/// - Null or absent (no parameters, use defaults)
395///
396/// CCITT parameters:
397/// - /K: Group indicator (-1=Group 4, 0=Group 3 1-D, >0=Group 3 2-D)
398/// - /Columns: Image width in pixels
399/// - /Rows: Image height in pixels (optional)
400/// - /BlackIs1: Pixel interpretation (false=white is 0, true=white is 1)
401/// - /EndOfLine: Include EOL code (default false)
402/// - /EncodedByteAlign: Byte-aligned encoding (default false)
403/// - /EndOfBlock: Include RTC code (default true)
404pub fn extract_ccitt_params(params_obj: Option<&Object>) -> Option<crate::decoders::CcittParams> {
405    extract_ccitt_params_with_width(params_obj, None)
406}
407
408/// Extract CCITT decompression parameters from a PDF object with optional width override.
409///
410/// This function extracts CCITT Group 3 or Group 4 decompression parameters from a PDF
411/// /DecodeParms dictionary. If image_width is provided, it will be used as the /Columns
412/// parameter, overriding any value in the dictionary.
413///
414/// # Arguments
415/// * `params_obj` - Optional PDF object containing CCITT parameters (Dictionary or Array)
416/// * `image_width` - Optional width override to use as /Columns parameter
417///
418/// # Returns
419/// Some(CcittParams) if valid parameters are found, None otherwise
420pub fn extract_ccitt_params_with_width(
421    params_obj: Option<&Object>,
422    image_width: Option<u32>,
423) -> Option<crate::decoders::CcittParams> {
424    let dict = match params_obj? {
425        Object::Dictionary(dict) => dict,
426        Object::Array(params) => params.iter().find_map(Object::as_dict)?,
427        _ => return None,
428    };
429    ccitt_params_from_dict(dict, image_width)
430}
431
432pub(crate) fn extract_ccitt_params_with_width_at_index(
433    params_obj: Option<&Object>,
434    filter_index: usize,
435    image_width: Option<u32>,
436) -> Option<crate::decoders::CcittParams> {
437    let dict = match params_obj? {
438        Object::Dictionary(d) => d,
439        Object::Array(arr) => arr.get(filter_index)?.as_dict()?,
440        _ => return None,
441    };
442    ccitt_params_from_dict(dict, image_width)
443}
444
445fn ccitt_params_from_dict(
446    dict: &std::collections::HashMap<String, Object>,
447    image_width: Option<u32>,
448) -> Option<crate::decoders::CcittParams> {
449    let k = dict.get("K").and_then(|obj| obj.as_integer()).unwrap_or(0);
450
451    let columns = dict
452        .get("Columns")
453        .and_then(|obj| obj.as_integer())
454        .map(|v| v as u32)
455        .or(image_width)
456        .unwrap_or(1);
457
458    let rows = dict.get("Rows").and_then(|obj| obj.as_integer()).map(|v| v as u32);
459
460    let black_is_1 = dict.get("BlackIs1").and_then(|obj| obj.as_bool()).unwrap_or(false);
461
462    let end_of_line = dict.get("EndOfLine").and_then(|obj| obj.as_bool()).unwrap_or(false);
463
464    let encoded_byte_align = dict
465        .get("EncodedByteAlign")
466        .and_then(|obj| obj.as_bool())
467        .unwrap_or(false);
468
469    let end_of_block = dict.get("EndOfBlock").and_then(|obj| obj.as_bool()).unwrap_or(true);
470
471    Some(crate::decoders::CcittParams {
472        k,
473        columns,
474        rows,
475        black_is_1,
476        end_of_line,
477        encoded_byte_align,
478        end_of_block,
479    })
480}
481
482#[cfg(test)]
483mod tests;