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 = impl Into<Object>>) -> Self {
55        values.into_iter().map(Into::into).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: impl Into<Object>) {
62        self.0.push(value.into());
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;
220    /// use pdfrum_common::kurbo::Rect;
221    ///
222    /// let media_box = Array::of([0, 0, 612, 792]);
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
256impl AsRef<[Object]> for Array {
257    fn as_ref(&self) -> &[Object] {
258        self.as_slice()
259    }
260}
261
262impl From<Vec<Object>> for Array {
263    fn from(values: Vec<Object>) -> Self {
264        Self(values)
265    }
266}
267
268impl From<Array> for Vec<Object> {
269    fn from(array: Array) -> Self {
270        array.0
271    }
272}
273
274impl FromIterator<Object> for Array {
275    fn from_iter<I: IntoIterator<Item = Object>>(iter: I) -> Self {
276        Self(iter.into_iter().collect())
277    }
278}
279
280impl IntoIterator for Array {
281    type Item = Object;
282    type IntoIter = std::vec::IntoIter<Object>;
283
284    fn into_iter(self) -> Self::IntoIter {
285        self.0.into_iter()
286    }
287}
288
289impl<'a> IntoIterator for &'a Array {
290    type Item = &'a Object;
291    type IntoIter = std::slice::Iter<'a, Object>;
292
293    fn into_iter(self) -> Self::IntoIter {
294        self.0.iter()
295    }
296}
297
298#[cfg(test)]
299#[expect(
300    clippy::float_cmp,
301    reason = "these assertions pin exact bit patterns the oracle produces"
302)]
303mod tests {
304    use pdfrum_common::kurbo::{Affine, Rect};
305
306    use super::Array;
307    use crate::test_resolve::TestStore;
308    use crate::{Dict, Name, NoResolve, ObjRef, Object, PdfString, Stream, names};
309
310    #[test]
311    fn from_and_into_vec_are_the_elements() {
312        let values = vec![Object::Int(1), Object::Int(2)];
313        let array = Array::from(values);
314        assert_eq!(array.as_ref(), &[Object::Int(1), Object::Int(2)]);
315        let back: Vec<Object> = array.into();
316        assert_eq!(back, [Object::Int(1), Object::Int(2)]);
317    }
318
319    #[test]
320    fn into_iterator_yields_the_objects() {
321        let array = Array::of([Object::Int(1), Object::Name("x".into())]);
322        let got: Vec<_> = array.into_iter().collect();
323        assert_eq!(got, [Object::Int(1), Object::Name("x".into())]);
324    }
325
326    // From cpdf_array_unittest.cpp:18-37.
327    #[test]
328    fn boolean_accessor_rejects_integers() {
329        let a = Array::of([
330            Object::Bool(true),
331            Object::Bool(false),
332            Object::Int(0),
333            Object::Int(1),
334        ]);
335        assert_eq!(a.bool_at(0), Some(true));
336        assert_eq!(a.bool_at(1), Some(false));
337        assert_eq!(a.bool_at(2), None);
338        assert_eq!(a.bool_at(3), None);
339        assert_eq!(a.bool_at(100), None);
340    }
341
342    #[test]
343    fn out_of_range_is_absence_everywhere() {
344        let a = Array::of([Object::Int(1)]);
345        assert_eq!(a.raw_at(9), None);
346        assert_eq!(a.int_at(9), None);
347        assert_eq!(a.number_at(9), None);
348        assert_eq!(a.number_at_or_zero(9), 0.0);
349        assert_eq!(a.name_at(9), None);
350        assert_eq!(a.string_at(9), None);
351        assert_eq!(a.byte_string_at(9), None);
352        assert_eq!(a.text_at(9), None);
353        assert_eq!(a.reference_at(9), None);
354        assert!(a.dict_at(9, &NoResolve).is_none());
355        assert!(a.array_at(9, &NoResolve).is_none());
356        assert!(a.stream_at(9, &NoResolve).is_none());
357    }
358
359    #[test]
360    fn scalar_accessors_do_not_resolve_but_composite_ones_do() {
361        let inner = Dict::from_pairs([(names::TYPE.clone(), Object::Name(names::PAGE.clone()))]);
362        let store = TestStore::from_pairs([
363            (1, Object::Int(42)),
364            (2, Object::Dict(inner.clone())),
365            (3, Object::Array(Array::of([Object::Int(1)]))),
366            (
367                4,
368                Object::Stream(Box::new(Stream::new(inner.clone(), b"xyz".to_vec().into()))),
369            ),
370        ]);
371        let a = Array::of([
372            Object::Ref(ObjRef::new(1, 0)),
373            Object::Ref(ObjRef::new(2, 0)),
374            Object::Ref(ObjRef::new(3, 0)),
375            Object::Ref(ObjRef::new(4, 0)),
376        ]);
377
378        // A reference is not a name and never will be.
379        assert_eq!(a.name_at(0), None);
380        assert_eq!(a.bool_at(0), None);
381        // An indirect number in /Index is skipped, not chased.
382        assert_eq!(a.number_obj_at(0), None);
383
384        assert_eq!(a.dict_at(1, &store), Some(inner.clone()));
385        assert_eq!(a.array_at(2, &store).map(|x| x.len()), Some(1));
386        assert!(a.stream_at(3, &store).is_some());
387        // A stream answers the dictionary accessor with its own dictionary.
388        assert_eq!(a.dict_at(3, &store), Some(inner));
389    }
390
391    // From cpdf_object_unittest.cpp:471-507.
392    #[test]
393    fn rect_and_matrix_need_exactly_the_right_element_count() {
394        let numbers = |n: usize| {
395            (1..=n)
396                .map(|i| Object::Real(f32::from(u8::try_from(i).unwrap_or(0))))
397                .collect::<Array>()
398        };
399        assert_eq!(numbers(4).as_rect(), Rect::new(1.0, 2.0, 3.0, 4.0));
400        assert_eq!(numbers(3).as_rect(), Rect::new(0.0, 0.0, 0.0, 0.0));
401        assert_eq!(numbers(5).as_rect(), Rect::new(0.0, 0.0, 0.0, 0.0));
402        assert_eq!(Array::new().as_rect(), Rect::new(0.0, 0.0, 0.0, 0.0));
403
404        assert_eq!(
405            numbers(6).as_matrix(),
406            Affine::new([1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
407        );
408        assert_eq!(numbers(5).as_matrix(), Affine::IDENTITY);
409        assert_eq!(numbers(7).as_matrix(), Affine::IDENTITY);
410    }
411
412    #[test]
413    fn rect_is_not_normalized() {
414        // A file that writes its box inverted keeps it inverted.
415        let inverted = Array::of([
416            Object::Int(612),
417            Object::Int(792),
418            Object::Int(0),
419            Object::Int(0),
420        ]);
421        assert_eq!(inverted.as_rect(), Rect::new(612.0, 792.0, 0.0, 0.0));
422    }
423
424    #[test]
425    fn malformed_geometry_elements_read_as_zero() {
426        let a = Array::of([
427            Object::Int(1),
428            Object::Name(Name::from("nope")),
429            Object::Null,
430            Object::Real(4.0),
431        ]);
432        assert_eq!(a.as_rect(), Rect::new(1.0, 0.0, 0.0, 4.0));
433    }
434
435    // From cpdf_object_unittest.cpp:210-236, restated over an array.
436    #[test]
437    fn byte_string_spelling_per_element_type() {
438        let a = Array::of([
439            Object::Bool(false),
440            Object::Bool(true),
441            Object::Int(1245),
442            Object::Real(9.003_45),
443            Object::Str(PdfString::literal(b"A simple test")),
444            Object::Name(Name::from("space")),
445            Object::Array(Array::new()),
446            Object::Dict(Dict::new()),
447            Object::Null,
448        ]);
449        let spellings: Vec<Vec<u8>> = (0..a.len())
450            .map(|i| a.byte_string_at(i).unwrap_or_default())
451            .collect();
452        assert_eq!(
453            spellings,
454            [
455                &b"false"[..],
456                b"true",
457                b"1245",
458                b"9.00345",
459                b"A simple test",
460                b"space",
461                b"",
462                b"",
463                b"",
464            ]
465        );
466    }
467
468    // From cpdf_array_unittest.cpp:207-241: PDFium's Find/Contains compare
469    // resolved pointer identity; over values, structural equality is the
470    // equivalent question and the one callers can actually ask.
471    #[test]
472    fn membership_is_structural() {
473        let a = Array::of([Object::Int(1), Object::Name(Name::from("x"))]);
474        assert!(a.iter().any(|o| o == &Object::Int(1)));
475        assert!(!a.iter().any(|o| o == &Object::Int(2)));
476        assert_eq!(a.as_slice().len(), 2);
477    }
478
479    #[test]
480    fn array_insert_shifts_and_appends_at_len() {
481        let mut a = Array::new();
482        a.push(Object::Int(1));
483        a.push(Object::Int(3));
484        a.insert(1, Object::Int(2));
485        assert_eq!(
486            a.as_slice(),
487            [Object::Int(1), Object::Int(2), Object::Int(3)]
488        );
489        a.insert(3, Object::Int(4));
490        assert_eq!(a.raw_at(3), Some(&Object::Int(4)));
491    }
492
493    #[test]
494    fn array_remove_out_of_range_is_none() {
495        let mut a = Array::of([Object::Int(1), Object::Int(2)]);
496        assert_eq!(a.remove(5), None);
497        assert_eq!(a.remove(0), Some(Object::Int(1)));
498        assert_eq!(a.as_slice(), [Object::Int(2)]);
499    }
500}