pdfrum_object/object.rs
1//! The [`Object`] enum and the coercions every accessor is built from.
2//!
3//! A coercion that does not apply to a variant yields that variant's
4//! fallback rather than failing: an array has no string spelling, a name has
5//! no numeric value.
6
7// PDFium reaches these through virtual methods with defaults on the base
8// class — `GetString()` on an array returns `""`, `GetInteger()` on a name
9// returns `0`, and so on. Here they are exhaustive matches, so adding a
10// variant makes every coercion fail to compile until it is considered.
11
12use std::collections::HashSet;
13
14use crate::number::{fmt_int, fmt_number, narrow_to_signed32, truncate_to_signed32, widen_to_f32};
15use crate::{Array, Dict, Error, Name, PdfString, Resolve, Resolved, Stream};
16
17/// An indirect object's identity: the pair a `N G obj` header carries and a
18/// cross-reference entry keys.
19///
20/// A reference token (`12 3 R`) carries a generation too, but resolution
21/// ignores it — see [`Resolve`]. The generation is kept because the
22/// cross-reference table and the writer both need it.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
24pub struct ObjRef {
25 /// The object number.
26 pub num: u32,
27 /// The generation number.
28 ///
29 /// Spelled out rather than `gen`, which Rust 2024 reserves.
30 pub generation: u16,
31}
32
33impl ObjRef {
34 // The object number no object may have; a cross-reference entry naming
35 // it is a broken entry.
36 //
37 // **Private on purpose**: no sentinel belongs in a public surface.
38 // PDFium exports this as `kInvalidObjNum`, but the PDF spec
39 // reserves no such number — the 8388607-object limit simply puts it out
40 // of reach — so a caller has nothing to compare against and no reason
41 // to. `ObjRef::is_invalid` is the whole public surface: the sentinel
42 // keeps existing in the parse, where the bytes contain it, and is asked
43 // about rather than handed over.
44 const INVALID_NUM: u32 = 0xFFFF_FFFF;
45
46 /// A reference to `num` at `generation`.
47 #[must_use]
48 pub const fn new(num: u32, generation: u16) -> Self {
49 Self { num, generation }
50 }
51
52 /// Whether the object number is the reserved invalid one.
53 #[must_use]
54 pub const fn is_invalid(self) -> bool {
55 self.num == Self::INVALID_NUM
56 }
57}
58
59/// A PDF object (ISO 32000-1 §7.3).
60///
61/// The eight basic types plus streams, plus a reference standing in for an
62/// indirect object. `Int` and `Real` are separate variants because the
63/// distinction is observable: an integer and a real that happen to be equal
64/// serialize differently and read back differently through the integer
65/// accessors.
66///
67/// ```
68/// use pdfrum_object::{Object, PdfString};
69///
70/// assert_eq!(Object::Int(1245).as_int(), Some(1245));
71/// assert_eq!(Object::Real(9.5).number(), Some(9.5));
72/// assert_eq!(Object::Bool(true).as_bool(), Some(true));
73/// // A name has no numeric value.
74/// assert_eq!(Object::Name("Foo".into()).number(), None);
75/// // ...but every object has a string spelling, empty for most.
76/// assert_eq!(Object::Str(PdfString::literal(b"hi")).to_byte_string(), b"hi");
77/// assert_eq!(Object::Null.to_byte_string(), b"");
78/// ```
79#[derive(Debug, Clone, PartialEq)]
80pub enum Object {
81 /// The `null` object.
82 Null,
83 /// `true` or `false`.
84 Bool(bool),
85 /// An integer.
86 ///
87 /// Holds the *mathematical* value. Every integer a conforming lexer can
88 /// produce lies in `-2^31 ..= 2^32 - 1`
89 /// (see [`INT_RANGE`](crate::INT_RANGE)) — larger literals fold to zero
90 /// during parsing. Reading it back has two flavours,
91 /// [`as_int`](Object::as_int) and [`number`](Object::number), which
92 /// disagree above `i32::MAX`; see [`narrow_to_signed32`](crate::narrow_to_signed32).
93 Int(i64),
94 /// A real number. `f32` rather than `f64` to match the precision the
95 /// oracle parses, formats and renders with.
96 Real(f32),
97 /// A string, in either syntax.
98 Str(PdfString),
99 /// A name.
100 Name(Name),
101 /// An array.
102 Array(Array),
103 /// A dictionary.
104 Dict(Dict),
105 /// A stream: a dictionary with bytes attached.
106 ///
107 /// Boxed because it is the one wide payload — a `Dict` plus a `ByteSpan`
108 /// is 56 bytes where every other payload is 24 — and the one that never
109 /// sits in the hot structures: ISO 32000-1 §7.3.8.1 forbids a stream as
110 /// a direct array element or dictionary value, so the box is only
111 /// dereferenced on the indirect-object path. It takes `Object` from 56
112 /// bytes to 32 and a dictionary pair from 80 to 56.
113 Stream(Box<Stream>),
114 /// A reference to an indirect object.
115 Ref(ObjRef),
116}
117
118impl Object {
119 /// The boolean value, only for an actual boolean.
120 ///
121 /// `Int(1)` is deliberately *not* a boolean: the type check happens
122 /// before any coercion, so a file that writes `1` for a flag reads as
123 /// "absent, use the default".
124 #[must_use]
125 pub fn as_bool(&self) -> Option<bool> {
126 match self {
127 Self::Bool(b) => Some(*b),
128 _ => None,
129 }
130 }
131
132 /// The integer value of any object that has one, in the C-integer view.
133 ///
134 /// Booleans count as 0 and 1, reals truncate toward zero (saturating, NaN
135 /// to 0), and everything else has no integer value. Note this is not a
136 /// type test — use [`Object::as_number`] for "is this a number".
137 #[must_use]
138 pub fn as_int(&self) -> Option<i64> {
139 match self {
140 Self::Bool(b) => Some(i64::from(*b)),
141 Self::Int(v) => Some(narrow_to_signed32(*v)),
142 Self::Real(v) => Some(truncate_to_signed32(*v)),
143 Self::Null
144 | Self::Str(_)
145 | Self::Name(_)
146 | Self::Array(_)
147 | Self::Dict(_)
148 | Self::Stream(_)
149 | Self::Ref(_) => None,
150 }
151 }
152
153 /// The numeric value of a number, coercing integers to `f32`.
154 ///
155 /// Only numbers have one — unlike [`Object::as_int`], a boolean does not
156 /// count.
157 #[must_use]
158 pub fn number(&self) -> Option<f32> {
159 match self {
160 Self::Int(v) => Some(widen_to_f32(*v)),
161 Self::Real(v) => Some(*v),
162 Self::Null
163 | Self::Bool(_)
164 | Self::Str(_)
165 | Self::Name(_)
166 | Self::Array(_)
167 | Self::Dict(_)
168 | Self::Stream(_)
169 | Self::Ref(_) => None,
170 }
171 }
172
173 /// The number itself, for accessors that type-check before coercing.
174 #[must_use]
175 pub fn as_number(&self) -> Option<&Self> {
176 match self {
177 Self::Int(_) | Self::Real(_) => Some(self),
178 _ => None,
179 }
180 }
181
182 /// The string, only for an actual string object.
183 #[must_use]
184 pub fn as_string(&self) -> Option<&PdfString> {
185 match self {
186 Self::Str(s) => Some(s),
187 _ => None,
188 }
189 }
190
191 /// The name, only for an actual name object.
192 #[must_use]
193 pub fn as_name(&self) -> Option<&Name> {
194 match self {
195 Self::Name(n) => Some(n),
196 _ => None,
197 }
198 }
199
200 /// The array, only for an actual array.
201 #[must_use]
202 pub fn as_array(&self) -> Option<&Array> {
203 match self {
204 Self::Array(a) => Some(a),
205 _ => None,
206 }
207 }
208
209 /// The dictionary — of a dictionary object, or of a stream.
210 ///
211 /// Streams answer with their own dictionary, which is what lets page-tree
212 /// and cross-reference code read `/Type` off either kind of object
213 /// without branching.
214 #[must_use]
215 pub fn as_dict(&self) -> Option<&Dict> {
216 match self {
217 Self::Dict(d) => Some(d),
218 Self::Stream(s) => Some(&s.dict),
219 _ => None,
220 }
221 }
222
223 /// The stream, only for an actual stream.
224 #[must_use]
225 pub fn as_stream(&self) -> Option<&Stream> {
226 match self {
227 Self::Stream(s) => Some(s),
228 _ => None,
229 }
230 }
231
232 /// The reference, only for an actual reference.
233 #[must_use]
234 pub fn as_ref_id(&self) -> Option<ObjRef> {
235 match self {
236 Self::Ref(r) => Some(*r),
237 _ => None,
238 }
239 }
240
241 /// Whether this is the null object.
242 #[must_use]
243 pub fn is_null(&self) -> bool {
244 matches!(self, Self::Null)
245 }
246
247 /// The object's byte-string spelling.
248 ///
249 /// Booleans spell `true`/`false`, numbers spell as the writer would, a
250 /// string yields its bytes and a name its decoded bytes. Everything else
251 /// — null, arrays, dictionaries, streams, references — has no spelling
252 /// and yields empty.
253 #[must_use]
254 pub fn to_byte_string(&self) -> Vec<u8> {
255 match self {
256 Self::Bool(true) => b"true".to_vec(),
257 Self::Bool(false) => b"false".to_vec(),
258 Self::Int(v) => fmt_int(*v).into_bytes(),
259 Self::Real(v) => fmt_number(*v).into_bytes(),
260 Self::Str(s) => s.bytes.to_vec(),
261 Self::Name(n) => n.as_bytes().to_vec(),
262 Self::Null | Self::Array(_) | Self::Dict(_) | Self::Stream(_) | Self::Ref(_) => {
263 Vec::new()
264 }
265 }
266 }
267
268 /// The object read as text: strings and names decode, everything else
269 /// yields empty.
270 ///
271 /// A stream's text needs its filters applied first, which this crate
272 /// cannot do — the reader composes decoding with
273 /// [`decode_text`](crate::decode_text) instead.
274 #[must_use]
275 pub fn to_text(&self) -> String {
276 match self {
277 Self::Str(s) => s.as_text().into_owned(),
278 Self::Name(n) => n.as_text().into_owned(),
279 Self::Null
280 | Self::Bool(_)
281 | Self::Int(_)
282 | Self::Real(_)
283 | Self::Array(_)
284 | Self::Dict(_)
285 | Self::Stream(_)
286 | Self::Ref(_) => String::new(),
287 }
288 }
289
290 /// Resolve one level: a reference becomes the object the store holds,
291 /// anything else is already itself.
292 ///
293 /// The result may still be a reference — an indirect object whose body is
294 /// `8 0 R` resolves to that reference and is *not* chased further, which
295 /// is why typed accessors go through [`Resolved::as_direct`].
296 ///
297 /// # Errors
298 ///
299 /// Whatever the store reports for an unresolvable reference.
300 ///
301 /// ```
302 /// # use std::sync::Arc;
303 /// # use pdfrum_object::{NoResolve, ObjRef, Object};
304 /// let direct = Object::Real(1.5);
305 /// assert_eq!(direct.resolve(&NoResolve).unwrap().number(), Some(1.5));
306 /// // Without a store every reference is dangling.
307 /// assert!(Object::Ref(ObjRef::new(4, 0)).resolve(&NoResolve).is_err());
308 /// ```
309 pub fn resolve<'a>(&'a self, r: &impl Resolve) -> Result<Resolved<'a>, Error> {
310 match self {
311 Self::Ref(id) => Ok(Resolved::Indirect(r.fetch(*id)?)),
312 _ => Ok(Resolved::Direct(self)),
313 }
314 }
315
316 /// Deep-copy the object with every reference replaced by what it points
317 /// at, dropping the edges that would close a cycle. Only a reference back
318 /// to an *ancestor* is a cycle; siblings may share substructure and both
319 /// copies survive. A cut edge **disappears** — the key or element is
320 /// omitted rather than becoming null — and an unresolvable reference
321 /// disappears the same way, indistinguishably.
322 ///
323 /// A reference to a stream flattens into the stream itself, stored
324 /// *directly* in the dictionary or array that held it, with its **raw**,
325 /// still-encoded bytes and its `/Filter` intact: ISO 32000-1 §7.3.8.1
326 /// constrains a *file*, not these in-memory types.
327 ///
328 /// ```
329 /// # use std::collections::HashMap;
330 /// # use std::sync::Arc;
331 /// # use pdfrum_object::{Array, Error, ObjRef, Object, Resolve};
332 /// # struct Store(HashMap<u32, Arc<Object>>);
333 /// # impl Resolve for Store {
334 /// # fn fetch(&self, r: ObjRef) -> Result<Arc<Object>, Error> {
335 /// # self.0.get(&r.num).cloned().ok_or(Error::UnresolvedRef(r))
336 /// # }
337 /// # }
338 /// let store = Store(HashMap::from([(7, Arc::new(Object::Int(42)))]));
339 /// let array = Object::Array(Array::from_iter([Object::Ref(ObjRef::new(7, 0))]));
340 /// assert_eq!(
341 /// array.clone_direct(&store),
342 /// Object::Array(Array::from_iter([Object::Int(42)])),
343 /// );
344 /// ```
345 #[must_use]
346 pub fn clone_direct(&self, r: &impl Resolve) -> Self {
347 // A cut edge and a dangling reference are indistinguishable because
348 // `CPDF_Reference::CloneNonCyclic` returns `nullptr` for both.
349 // Flattening a stream into a container matches
350 // `CPDF_Dictionary::CloneNonCyclic`, whose loop inserts into `map_`
351 // directly and so bypasses the `CHECK(!IsStream())` that guards the
352 // ordinary setters; the raw bytes are
353 // `CPDF_Stream::CloneNonCyclic`'s `LoadAllDataRaw`.
354 let mut ancestors = HashSet::new();
355 clone_flattened(self, r, &mut ancestors).unwrap_or(Self::Null)
356 }
357}
358
359/// Deep-copy `obj` flattening references, cutting edges back to `ancestors`.
360/// `None` means "this edge closes a cycle or dangles" — the caller drops it.
361fn clone_flattened(
362 obj: &Object,
363 r: &impl Resolve,
364 ancestors: &mut HashSet<ObjRef>,
365) -> Option<Object> {
366 match obj {
367 Object::Ref(id) => {
368 if !ancestors.insert(*id) {
369 return None;
370 }
371 let target = r.fetch(*id).ok();
372 let cloned = target
373 .as_deref()
374 .and_then(|t| clone_flattened(t, r, &mut ancestors.clone()));
375 ancestors.remove(id);
376 cloned
377 }
378 Object::Array(a) => Some(Object::Array(
379 a.iter()
380 .filter_map(|e| clone_flattened(e, r, &mut ancestors.clone()))
381 .collect(),
382 )),
383 Object::Dict(d) => Some(Object::Dict(
384 d.iter()
385 .filter_map(|(k, v)| {
386 clone_flattened(v, r, &mut ancestors.clone()).map(|v| (k.clone(), v))
387 })
388 .collect(),
389 )),
390 Object::Stream(s) => {
391 let dict = s
392 .dict
393 .iter()
394 .filter_map(|(k, v)| {
395 clone_flattened(v, r, &mut ancestors.clone()).map(|v| (k.clone(), v))
396 })
397 .collect();
398 Some(Object::Stream(Box::new(Stream::new(dict, s.data.clone()))))
399 }
400 other => Some(other.clone()),
401 }
402}
403
404impl From<bool> for Object {
405 fn from(v: bool) -> Self {
406 Self::Bool(v)
407 }
408}
409
410impl From<i64> for Object {
411 fn from(v: i64) -> Self {
412 Self::Int(v)
413 }
414}
415
416impl From<i32> for Object {
417 fn from(v: i32) -> Self {
418 Self::Int(i64::from(v))
419 }
420}
421
422impl From<f32> for Object {
423 fn from(v: f32) -> Self {
424 Self::Real(v)
425 }
426}
427
428impl From<Name> for Object {
429 fn from(v: Name) -> Self {
430 Self::Name(v)
431 }
432}
433
434impl From<PdfString> for Object {
435 fn from(v: PdfString) -> Self {
436 Self::Str(v)
437 }
438}
439
440impl From<Array> for Object {
441 fn from(v: Array) -> Self {
442 Self::Array(v)
443 }
444}
445
446impl From<Dict> for Object {
447 fn from(v: Dict) -> Self {
448 Self::Dict(v)
449 }
450}
451
452impl From<Stream> for Object {
453 fn from(v: Stream) -> Self {
454 Self::Stream(Box::new(v))
455 }
456}
457
458impl From<ObjRef> for Object {
459 fn from(v: ObjRef) -> Self {
460 Self::Ref(v)
461 }
462}