Skip to main content

pdfrum_object/
array.rs

1//! Array objects (ISO 32000-1 §7.3.6) and their typed accessors.
2//!
3//! The resolution rules mirror [`Dict`](crate::Dict)'s exactly, index for
4//! key: the scalar accessors do not resolve (though a numeric coercion still
5//! delegates through a reference one level), the composite ones do, and the
6//! type-filtered ones read a reference as absence. An out-of-range index is
7//! never an error — it is the same absence a missing key is.
8
9use pdfrum_common::kurbo::{Affine, Rect};
10
11use crate::{Dict, Name, ObjRef, Object, PdfString, Resolve, Resolved, Stream};
12
13/// A PDF array: an ordered sequence of objects.
14///
15/// # Streams as elements
16///
17/// ISO 32000-1 §7.3.8.1 forbids a *file* from writing a stream as a direct
18/// array element, and the reader drops one found inline while parsing. That
19/// is a **file-format** constraint, not an in-memory invariant, and this
20/// type does not police it:
21/// [`Object::clone_direct`](crate::Object::clone_direct) flattens
22/// references, so an array of indirect streams clones into one holding those
23/// streams directly, and [`Array::stream_at`] reads such an element back.
24/// Enforcing §7.3.8.1 is the **writer's** job: `pdfrum-edit` hoists a direct
25/// stream to an indirect object at serialization time.
26///
27/// ```
28/// use pdfrum_object::{Array, NoResolve, Object};
29///
30/// let a = Array::of([Object::Int(8902), Object::Name("address".into())]);
31/// assert_eq!(a.int_at(0), Some(8902));
32/// assert_eq!(a.name_at(1).and_then(|n| n.as_str()), Some("address"));
33/// // Out of range is absence, never a panic.
34/// assert_eq!(a.int_at(99), None);
35/// ```
36// The inline-stream drop while parsing is `cpdf_syntax_parser.cpp:591-596`.
37// `CPDF_Dictionary::CloneNonCyclic` produces the same flattened shape, since
38// its loop writes straight into `map_` and bypasses the `CHECK(!IsStream())`
39// that guards the ordinary setters; `Array::stream_at` mirrors
40// `CPDF_Array::GetStreamAt` in reading it back.
41#[derive(Debug, Clone, Default, PartialEq)]
42pub struct Array(Vec<Object>);
43
44impl Array {
45    /// An empty array.
46    #[must_use]
47    pub fn new() -> Self {
48        Self(Vec::new())
49    }
50
51    /// An array of these values, in order. The counterpart of
52    /// [`Dict::from_pairs`].
53    #[must_use]
54    pub fn of(values: impl IntoIterator<Item = Object>) -> Self {
55        values.into_iter().collect()
56    }
57
58    /// Append an element.
59    ///
60    /// Any object, a stream included — see the type-level note on §7.3.8.1.
61    pub fn push(&mut self, value: Object) {
62        self.0.push(value);
63    }
64
65    /// Inserts `value` at `index`, shifting later items; `index == len()`
66    /// appends.
67    ///
68    /// This is the same contract as [`Vec::insert`]: a panic here is a
69    /// caller bug, not a response to untrusted PDF bytes. [`Array::remove`]
70    /// returns [`None`] out of range because absence is a normal outcome
71    /// there.
72    ///
73    /// # Panics
74    ///
75    /// When `index > len()`.
76    pub fn insert(&mut self, index: usize, value: Object) {
77        self.0.insert(index, value);
78    }
79
80    /// Removes and returns the item at `index`; `None` when out of range.
81    pub fn remove(&mut self, index: usize) -> Option<Object> {
82        (index < self.0.len()).then(|| self.0.remove(index))
83    }
84
85    /// Number of elements.
86    #[must_use]
87    pub fn len(&self) -> usize {
88        self.0.len()
89    }
90
91    /// Whether the array is empty.
92    #[must_use]
93    pub fn is_empty(&self) -> bool {
94        self.0.is_empty()
95    }
96
97    /// The elements, in order.
98    pub fn iter(&self) -> impl Iterator<Item = &Object> {
99        self.0.iter()
100    }
101
102    /// The elements as a slice.
103    #[must_use]
104    pub fn as_slice(&self) -> &[Object] {
105        &self.0
106    }
107
108    // ---- non-resolving accessors ----
109
110    /// The element at `index`, whatever its type, without resolving.
111    #[must_use]
112    pub fn raw_at(&self, index: usize) -> Option<&Object> {
113        self.0.get(index)
114    }
115
116    /// The integer value at `index` in the C-integer view, coercing any type
117    /// that has one.
118    #[must_use]
119    pub fn int_at(&self, index: usize) -> Option<i64> {
120        self.raw_at(index)?.as_int()
121    }
122
123    /// The numeric value at `index`, coercing integers to `f32`.
124    #[must_use]
125    pub fn number_at(&self, index: usize) -> Option<f32> {
126        self.raw_at(index)?.number()
127    }
128
129    /// The numeric value at `index`, or 0.0 when it is missing or not a
130    /// number. The fallback [`Array::as_rect`] and [`Array::as_matrix`] use.
131    #[must_use]
132    pub fn number_at_or_zero(&self, index: usize) -> f32 {
133        self.number_at(index).unwrap_or(0.0)
134    }
135
136    /// The value of a `Boolean`-typed element. An `Int(1)` reads as absent.
137    #[must_use]
138    pub fn bool_at(&self, index: usize) -> Option<bool> {
139        self.raw_at(index)?.as_bool()
140    }
141
142    /// The name at `index`, only for an actual name.
143    #[must_use]
144    pub fn name_at(&self, index: usize) -> Option<&Name> {
145        self.raw_at(index)?.as_name()
146    }
147
148    /// The string at `index`, only for an actual string.
149    #[must_use]
150    pub fn string_at(&self, index: usize) -> Option<&PdfString> {
151        self.raw_at(index)?.as_string()
152    }
153
154    /// A `Number`-typed element as an object, without resolving.
155    ///
156    /// This is how a cross-reference stream's `/Index` is validated: an
157    /// indirect number there is *skipped*, not chased.
158    #[must_use]
159    pub fn number_obj_at(&self, index: usize) -> Option<&Object> {
160        self.raw_at(index)?.as_number()
161    }
162
163    /// The byte-string spelling at `index` — see [`Object::to_byte_string`].
164    #[must_use]
165    pub fn byte_string_at(&self, index: usize) -> Option<Vec<u8>> {
166        Some(self.raw_at(index)?.to_byte_string())
167    }
168
169    /// The element at `index` read as text — see [`Object::to_text`].
170    #[must_use]
171    pub fn text_at(&self, index: usize) -> Option<String> {
172        Some(self.raw_at(index)?.to_text())
173    }
174
175    /// The reference at `index`, without resolving it.
176    #[must_use]
177    pub fn reference_at(&self, index: usize) -> Option<ObjRef> {
178        self.raw_at(index)?.as_ref_id()
179    }
180
181    // ---- resolving accessors ----
182
183    /// The element at `index`, following one level of indirection.
184    #[must_use]
185    pub fn get<'a>(&'a self, index: usize, r: &impl Resolve) -> Option<Resolved<'a>> {
186        self.raw_at(index)?.resolve(r).ok()
187    }
188
189    /// The dictionary at `index`, following one level of indirection. A
190    /// stream answers with its own dictionary.
191    #[must_use]
192    pub fn dict_at(&self, index: usize, r: &impl Resolve) -> Option<Dict> {
193        self.get(index, r)?.as_direct()?.as_dict().cloned()
194    }
195
196    /// The array at `index`, following one level of indirection.
197    #[must_use]
198    pub fn array_at(&self, index: usize, r: &impl Resolve) -> Option<Array> {
199        self.get(index, r)?.as_direct()?.as_array().cloned()
200    }
201
202    /// The stream at `index`, following one level of indirection.
203    #[must_use]
204    pub fn stream_at(&self, index: usize, r: &impl Resolve) -> Option<Stream> {
205        self.get(index, r)?.as_direct()?.as_stream().cloned()
206    }
207
208    // ---- geometry ----
209
210    /// The array read as a rectangle: exactly four elements, or the zero
211    /// rectangle.
212    ///
213    /// The PDF order is left, bottom, right, top, mapping onto kurbo's
214    /// `(x0, y0, x1, y1)` in that order. The result is **not** normalized:
215    /// files write inverted boxes and consumers that care normalize
216    /// themselves.
217    ///
218    /// ```
219    /// use pdfrum_object::{Array, Object};
220    /// use pdfrum_common::kurbo::Rect;
221    ///
222    /// let media_box = Array::of([0, 0, 612, 792].map(Object::from));
223    /// assert_eq!(media_box.as_rect(), Rect::new(0.0, 0.0, 612.0, 792.0));
224    /// ```
225    #[must_use]
226    pub fn as_rect(&self) -> Rect {
227        if self.len() != 4 {
228            return Rect::new(0.0, 0.0, 0.0, 0.0);
229        }
230        Rect::new(
231            f64::from(self.number_at_or_zero(0)),
232            f64::from(self.number_at_or_zero(1)),
233            f64::from(self.number_at_or_zero(2)),
234            f64::from(self.number_at_or_zero(3)),
235        )
236    }
237
238    /// The array read as a transformation matrix: exactly six elements
239    /// `[a b c d e f]`, or the identity.
240    #[must_use]
241    pub fn as_matrix(&self) -> Affine {
242        if self.len() != 6 {
243            return Affine::IDENTITY;
244        }
245        Affine::new([
246            f64::from(self.number_at_or_zero(0)),
247            f64::from(self.number_at_or_zero(1)),
248            f64::from(self.number_at_or_zero(2)),
249            f64::from(self.number_at_or_zero(3)),
250            f64::from(self.number_at_or_zero(4)),
251            f64::from(self.number_at_or_zero(5)),
252        ])
253    }
254
255    /// The numbers in the array, missing or non-numeric elements reading as
256    /// 0.0. Used wherever the specification says "an array of `n` numbers".
257    #[must_use]
258    pub fn to_numbers(&self) -> Vec<f32> {
259        (0..self.len()).map(|i| self.number_at_or_zero(i)).collect()
260    }
261}
262
263impl FromIterator<Object> for Array {
264    fn from_iter<I: IntoIterator<Item = Object>>(iter: I) -> Self {
265        let mut array = Self::new();
266        for value in iter {
267            array.push(value);
268        }
269        array
270    }
271}
272
273impl<'a> IntoIterator for &'a Array {
274    type Item = &'a Object;
275    type IntoIter = std::slice::Iter<'a, Object>;
276
277    fn into_iter(self) -> Self::IntoIter {
278        self.0.iter()
279    }
280}
281
282#[cfg(test)]
283#[expect(
284    clippy::float_cmp,
285    reason = "these assertions pin exact bit patterns the oracle produces"
286)]
287mod tests {
288    use pdfrum_common::kurbo::{Affine, Rect};
289
290    use super::Array;
291    use crate::test_resolve::TestStore;
292    use crate::{Dict, Name, NoResolve, ObjRef, Object, PdfString, Stream, names};
293
294    // From cpdf_array_unittest.cpp:18-37.
295    #[test]
296    fn boolean_accessor_rejects_integers() {
297        let a = Array::of([
298            Object::Bool(true),
299            Object::Bool(false),
300            Object::Int(0),
301            Object::Int(1),
302        ]);
303        assert_eq!(a.bool_at(0), Some(true));
304        assert_eq!(a.bool_at(1), Some(false));
305        assert_eq!(a.bool_at(2), None);
306        assert_eq!(a.bool_at(3), None);
307        assert_eq!(a.bool_at(100), None);
308    }
309
310    #[test]
311    fn out_of_range_is_absence_everywhere() {
312        let a = Array::of([Object::Int(1)]);
313        assert_eq!(a.raw_at(9), None);
314        assert_eq!(a.int_at(9), None);
315        assert_eq!(a.number_at(9), None);
316        assert_eq!(a.number_at_or_zero(9), 0.0);
317        assert_eq!(a.name_at(9), None);
318        assert_eq!(a.string_at(9), None);
319        assert_eq!(a.byte_string_at(9), None);
320        assert_eq!(a.text_at(9), None);
321        assert_eq!(a.reference_at(9), None);
322        assert!(a.dict_at(9, &NoResolve).is_none());
323        assert!(a.array_at(9, &NoResolve).is_none());
324        assert!(a.stream_at(9, &NoResolve).is_none());
325    }
326
327    #[test]
328    fn scalar_accessors_do_not_resolve_but_composite_ones_do() {
329        let inner = Dict::from_pairs([(names::TYPE.clone(), Object::Name(names::PAGE.clone()))]);
330        let store = TestStore::from_pairs([
331            (1, Object::Int(42)),
332            (2, Object::Dict(inner.clone())),
333            (3, Object::Array(Array::of([Object::Int(1)]))),
334            (
335                4,
336                Object::Stream(Box::new(Stream::new(inner.clone(), b"xyz".to_vec().into()))),
337            ),
338        ]);
339        let a = Array::of([
340            Object::Ref(ObjRef::new(1, 0)),
341            Object::Ref(ObjRef::new(2, 0)),
342            Object::Ref(ObjRef::new(3, 0)),
343            Object::Ref(ObjRef::new(4, 0)),
344        ]);
345
346        // A reference is not a name and never will be.
347        assert_eq!(a.name_at(0), None);
348        assert_eq!(a.bool_at(0), None);
349        // An indirect number in /Index is skipped, not chased.
350        assert_eq!(a.number_obj_at(0), None);
351
352        assert_eq!(a.dict_at(1, &store), Some(inner.clone()));
353        assert_eq!(a.array_at(2, &store).map(|x| x.len()), Some(1));
354        assert!(a.stream_at(3, &store).is_some());
355        // A stream answers the dictionary accessor with its own dictionary.
356        assert_eq!(a.dict_at(3, &store), Some(inner));
357    }
358
359    // From cpdf_object_unittest.cpp:471-507.
360    #[test]
361    fn rect_and_matrix_need_exactly_the_right_element_count() {
362        let numbers = |n: usize| {
363            (1..=n)
364                .map(|i| Object::Real(f32::from(u8::try_from(i).unwrap_or(0))))
365                .collect::<Array>()
366        };
367        assert_eq!(numbers(4).as_rect(), Rect::new(1.0, 2.0, 3.0, 4.0));
368        assert_eq!(numbers(3).as_rect(), Rect::new(0.0, 0.0, 0.0, 0.0));
369        assert_eq!(numbers(5).as_rect(), Rect::new(0.0, 0.0, 0.0, 0.0));
370        assert_eq!(Array::new().as_rect(), Rect::new(0.0, 0.0, 0.0, 0.0));
371
372        assert_eq!(
373            numbers(6).as_matrix(),
374            Affine::new([1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
375        );
376        assert_eq!(numbers(5).as_matrix(), Affine::IDENTITY);
377        assert_eq!(numbers(7).as_matrix(), Affine::IDENTITY);
378    }
379
380    #[test]
381    fn rect_is_not_normalized() {
382        // A file that writes its box inverted keeps it inverted.
383        let inverted = Array::of([
384            Object::Int(612),
385            Object::Int(792),
386            Object::Int(0),
387            Object::Int(0),
388        ]);
389        assert_eq!(inverted.as_rect(), Rect::new(612.0, 792.0, 0.0, 0.0));
390    }
391
392    #[test]
393    fn malformed_geometry_elements_read_as_zero() {
394        let a = Array::of([
395            Object::Int(1),
396            Object::Name(Name::from("nope")),
397            Object::Null,
398            Object::Real(4.0),
399        ]);
400        assert_eq!(a.as_rect(), Rect::new(1.0, 0.0, 0.0, 4.0));
401        assert_eq!(a.to_numbers(), [1.0, 0.0, 0.0, 4.0]);
402    }
403
404    // From cpdf_object_unittest.cpp:210-236, restated over an array.
405    #[test]
406    fn byte_string_spelling_per_element_type() {
407        let a = Array::of([
408            Object::Bool(false),
409            Object::Bool(true),
410            Object::Int(1245),
411            Object::Real(9.003_45),
412            Object::Str(PdfString::literal(b"A simple test")),
413            Object::Name(Name::from("space")),
414            Object::Array(Array::new()),
415            Object::Dict(Dict::new()),
416            Object::Null,
417        ]);
418        let spellings: Vec<Vec<u8>> = (0..a.len())
419            .map(|i| a.byte_string_at(i).unwrap_or_default())
420            .collect();
421        assert_eq!(
422            spellings,
423            [
424                &b"false"[..],
425                b"true",
426                b"1245",
427                b"9.00345",
428                b"A simple test",
429                b"space",
430                b"",
431                b"",
432                b"",
433            ]
434        );
435    }
436
437    // From cpdf_array_unittest.cpp:207-241: PDFium's Find/Contains compare
438    // resolved pointer identity; over values, structural equality is the
439    // equivalent question and the one callers can actually ask.
440    #[test]
441    fn membership_is_structural() {
442        let a = Array::of([Object::Int(1), Object::Name(Name::from("x"))]);
443        assert!(a.iter().any(|o| o == &Object::Int(1)));
444        assert!(!a.iter().any(|o| o == &Object::Int(2)));
445        assert_eq!(a.as_slice().len(), 2);
446    }
447
448    #[test]
449    fn array_insert_shifts_and_appends_at_len() {
450        let mut a = Array::new();
451        a.push(Object::Int(1));
452        a.push(Object::Int(3));
453        a.insert(1, Object::Int(2));
454        assert_eq!(
455            a.as_slice(),
456            [Object::Int(1), Object::Int(2), Object::Int(3)]
457        );
458        a.insert(3, Object::Int(4));
459        assert_eq!(a.raw_at(3), Some(&Object::Int(4)));
460    }
461
462    #[test]
463    fn array_remove_out_of_range_is_none() {
464        let mut a = Array::of([Object::Int(1), Object::Int(2)]);
465        assert_eq!(a.remove(5), None);
466        assert_eq!(a.remove(0), Some(Object::Int(1)));
467        assert_eq!(a.as_slice(), [Object::Int(2)]);
468    }
469}