From 9fcef3478c2d52482af2b13f9eb6655d5eaf66d4 Mon Sep 17 00:00:00 2001 From: Velli20 Date: Tue, 13 Oct 2026 22:45:08 +0300 Subject: [PATCH] Resolve references to missing objects as null in the object reader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ISO 32000 §7.3.10 treats a reference to a nonexistent object as null, but the reader raised MissingObject. Decoders worked around it one by one with is_absent checks, and every other optional read failed instead of yielding None. The reader now resolves an absent object to null and MissingObject is removed. ObjectCollection::insert decodes stream filters while objects are still loading, where a missing object may be defined later, so it reads through a source that fails on unloaded objects and keeps such streams encoded as before. The /Contents and /Annots workarounds are removed and is_absent becomes a plain null check. Co-Authored-By: Claude Opus 5.5 --- .../pdf-annotation-core/src/source_decode.rs | 8 ++--- crates/pdf-document/src/page.rs | 11 +------ .../src/object_collection.rs | 33 ++++++++++++++++++- crates/pdf-object-reader/src/context.rs | 12 +++---- crates/pdf-object-reader/src/error.rs | 6 ---- crates/pdf-object-reader/src/reader.rs | 3 +- crates/pdf-object-reader/src/source.rs | 4 ++- 7 files changed, 44 insertions(+), 33 deletions(-) diff --git a/crates/pdf-annotation-core/src/source_decode.rs b/crates/pdf-annotation-core/src/source_decode.rs index 5ff12b83..039123ae 100644 --- a/crates/pdf-annotation-core/src/source_decode.rs +++ b/crates/pdf-annotation-core/src/source_decode.rs @@ -14,7 +14,7 @@ use crate::pdf_data::{ }; use pdf_graphics::{DashPattern, color::Color, rect::Rect}; use pdf_object_reader::{ - DictionaryContext, FromPdfObject, ObjectAccess, ObjectContext, ObjectReadError, ReadResult, + DictionaryContext, FromPdfObject, ObjectAccess, ObjectContext, ReadResult, object_lookup::ObjectLookupExt, object_resolver::{DanglingAsNullResolver, ObjectResolver}, object_variant::ObjectVariant, @@ -128,11 +128,7 @@ impl SourceAnnotation { }; // `/Annots` that resolves to anything but an array (e.g. a reference whose object // number was reused by a later update) holds no annotations. - let resolved = match context.resolve(entry) { - Ok(resolved) => resolved, - Err(ObjectReadError::MissingObject { .. }) => return Ok(None), - Err(error) => return Err(error), - }; + let resolved = context.resolve(entry)?; let ObjectVariant::Array(annots) = resolved.value() else { return Ok(None); }; diff --git a/crates/pdf-document/src/page.rs b/crates/pdf-document/src/page.rs index 8b429768..5a12f4f1 100644 --- a/crates/pdf-document/src/page.rs +++ b/crates/pdf-document/src/page.rs @@ -167,16 +167,7 @@ impl FromPdfObject for PdfPage { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { let mut context = context.dictionary()?; let dictionary = context.dictionary(); - // A dangling `/Contents` reference is null (ISO 32000 §7.3.10), leaving the page empty. - let contents_absent = match dictionary.get(b"Contents") { - Some(object) => context.is_absent(object)?, - None => true, - }; - let contents = if contents_absent { - None - } else { - context.optional::(b"Contents")? - }; + let contents = context.optional::(b"Contents")?; let media_box = dictionary.optional_media_box(context.source())?; let crop_box = dictionary.optional_rect(b"CropBox", context.source())?; let rotation = dictionary.optional_number::(b"Rotate", context.source())?; diff --git a/crates/pdf-object-collection/src/object_collection.rs b/crates/pdf-object-collection/src/object_collection.rs index ff68a7ef..01882fe8 100644 --- a/crates/pdf-object-collection/src/object_collection.rs +++ b/crates/pdf-object-collection/src/object_collection.rs @@ -83,7 +83,7 @@ impl ObjectCollection { // component, precision, colour, and opacity metadata that a // flat byte buffer would lose, so the image path decodes it. if !stream.filters_applied() { - let reader = ObjectReader::new(&*self); + let reader = ObjectReader::new(LoadedObjects(&*self)); let mut session = reader.session(); if !carries_jpx_codestream(stream, &mut session) && let Ok(data) = @@ -524,6 +524,37 @@ mod tests { } } +/// Reads the objects inserted so far, failing on a reference to one not yet loaded. +/// +/// Insertion decodes stream filters before later objects arrive, so an object missing +/// here may still be defined. Failing the read keeps such a stream encoded for a later +/// decode, where [`ObjectCollection`] resolves an object that is still missing to null. +struct LoadedObjects<'a>(&'a ObjectCollection); + +impl ObjectResolver for LoadedObjects<'_> { + fn resolve_object<'a>( + &'a self, + obj: &'a ObjectVariant, + ) -> Result<&'a ObjectVariant, ObjectError> { + self.0.resolve_object(obj) + } +} + +impl pdf_object_reader::ObjectSource for LoadedObjects<'_> { + type Error = ObjectError; + fn read_object( + &self, + id: pdf_object_reader::object_id::ObjectId, + ) -> Result, Self::Error> { + self.0 + .read_object(id)? + .map(Some) + .ok_or_else(|| ObjectError::FailedResolveObjectReference { + obj_num: id.number(), + }) + } +} + impl pdf_object_reader::ObjectSource for ObjectCollection { type Error = ObjectError; fn read_object( diff --git a/crates/pdf-object-reader/src/context.rs b/crates/pdf-object-reader/src/context.rs index cb0289d4..8812b252 100644 --- a/crates/pdf-object-reader/src/context.rs +++ b/crates/pdf-object-reader/src/context.rs @@ -302,16 +302,12 @@ macro_rules! context_access { self.access.resolve(object) } - /// Returns whether an object is null or references a nonexistent object. + /// Returns whether an object resolves to null. /// - /// Per PDF spec §7.3.10 such a reference is treated as null, and per §7.3.7 a - /// null dictionary value is equivalent to an absent entry. + /// A reference to a nonexistent object resolves to null (PDF spec §7.3.10), and + /// per §7.3.7 a null dictionary value is equivalent to an absent entry. pub fn is_absent(&mut self, object: &ObjectVariant) -> ReadResult { - match self.access.resolve(object) { - Ok(resolved) => Ok(resolved.kind() == ObjectKind::Null), - Err(ObjectReadError::MissingObject { .. }) => Ok(true), - Err(error) => Err(error), - } + Ok(self.access.resolve(object)?.kind() == ObjectKind::Null) } /// Reborrows the active traversal for decoders that take an access directly. diff --git a/crates/pdf-object-reader/src/error.rs b/crates/pdf-object-reader/src/error.rs index 2566af42..a7334acb 100644 --- a/crates/pdf-object-reader/src/error.rs +++ b/crates/pdf-object-reader/src/error.rs @@ -63,12 +63,6 @@ pub enum ObjectReadError { #[source] source: BoxedError, }, - /// The source had no value for a referenced indirect object. - #[error("indirect object {object_id:?} was not found")] - MissingObject { - /// The unresolved object identifier. - object_id: ObjectId, - }, /// Reference traversal revisited an object active in the current read. #[error("cyclic reference encountered at {repeated:?}; active path: {path:?}")] CyclicReference { diff --git a/crates/pdf-object-reader/src/reader.rs b/crates/pdf-object-reader/src/reader.rs index 4a62e622..30dee011 100644 --- a/crates/pdf-object-reader/src/reader.rs +++ b/crates/pdf-object-reader/src/reader.rs @@ -277,7 +277,8 @@ where object_id: id, source: Box::new(source), })? - .ok_or(ObjectReadError::MissingObject { object_id: id })?; + // A reference to a nonexistent object is null (ISO 32000 §7.3.10). + .unwrap_or_else(|| PdfObject::new(ObjectVariant::Null)); self.reader .raw_cache .insert_if_absent(id, object) diff --git a/crates/pdf-object-reader/src/source.rs b/crates/pdf-object-reader/src/source.rs index a5a6a3e3..45ff1c25 100644 --- a/crates/pdf-object-reader/src/source.rs +++ b/crates/pdf-object-reader/src/source.rs @@ -22,7 +22,9 @@ pub trait ObjectSource: crate::object_resolver::ObjectResolver + Send + Sync { /// Reads one indirect object without recursively resolving its references. /// - /// `Ok(None)` indicates that the identifier is absent. Returned objects may + /// `Ok(None)` indicates that the identifier is absent, which the reader resolves + /// to null (ISO 32000 §7.3.10). Return an error instead where absence must fail + /// the read, such as an object that has not been loaded yet. Returned objects may /// themselves be references and may contain references at any depth. fn read_object(&self, object_id: ObjectId) -> Result, Self::Error>; }