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;