From 31c2c360b810000aebb7bbaf168c2ba259668dc0 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 8 Jul 2026 00:05:25 +0300 Subject: [PATCH 1/2] pdf-object: add typed lookup helpers Add shared lookup extensions for dictionaries and arrays so callers can read optional and required typed values through one API. Update PDF parsing code to use the helpers and consistently treat missing or null optional values as absent. Co-authored-by: Codex --- crates/pdf-annotation-types/src/action.rs | 50 +- crates/pdf-annotation-types/src/annotation.rs | 29 +- .../src/annotation_border.rs | 9 +- .../src/appearance_characteristics.rs | 24 +- .../pdf-annotation-types/src/border_effect.rs | 9 +- .../pdf-annotation-types/src/border_style.rs | 14 +- .../pdf-annotation-types/src/destination.rs | 64 +- .../src/file_specification.rs | 38 +- .../src/subtypes/caret.rs | 9 +- .../src/subtypes/circle.rs | 9 +- .../src/subtypes/file_attachment.rs | 9 +- .../src/subtypes/free_text.rs | 39 +- .../src/subtypes/highlight.rs | 9 +- .../pdf-annotation-types/src/subtypes/line.rs | 24 +- .../src/subtypes/movie.rs | 9 +- .../src/subtypes/polygon.rs | 9 +- .../src/subtypes/polyline.rs | 9 +- .../src/subtypes/popup.rs | 9 +- .../src/subtypes/sound.rs | 14 +- .../src/subtypes/square.rs | 9 +- .../src/subtypes/squiggly.rs | 9 +- .../src/subtypes/stamp.rs | 10 +- .../src/subtypes/strikeout.rs | 9 +- .../pdf-annotation-types/src/subtypes/text.rs | 29 +- .../src/subtypes/underline.rs | 9 +- .../src/subtypes/widget.rs | 38 +- crates/pdf-ccitt/src/ccitt_fax_params.rs | 37 +- .../src/cal_gray_color_space.rs | 16 +- .../src/cal_rgb_color_space.rs | 19 +- .../src/icc_based_color_space.rs | 9 +- crates/pdf-color-space/src/lab_color_space.rs | 16 +- crates/pdf-document/src/encryption.rs | 33 +- crates/pdf-document/src/object_stream.rs | 7 +- crates/pdf-document/src/pages.rs | 8 +- crates/pdf-document/src/reader.rs | 11 +- crates/pdf-filter/src/filter.rs | 22 +- crates/pdf-filter/src/predictor.rs | 20 +- crates/pdf-font/src/cid_system_info.rs | 15 +- crates/pdf-font/src/fallback.rs | 9 +- crates/pdf-font/src/font.rs | 7 +- crates/pdf-font/src/simple_font_glyph_map.rs | 13 +- crates/pdf-font/src/true_type_font.rs | 15 +- crates/pdf-font/src/type0_font.rs | 9 +- crates/pdf-font/src/type1_font.rs | 33 +- crates/pdf-font/src/type3_font.rs | 16 +- .../src/exponential_interpolation.rs | 23 +- crates/pdf-function/src/function.rs | 7 +- .../pdf-function/src/postscript_calculator.rs | 12 +- crates/pdf-function/src/sampled.rs | 18 +- crates/pdf-function/src/stitching.rs | 18 +- crates/pdf-image/src/image_xobject.rs | 29 +- crates/pdf-object/src/lib.rs | 1 + crates/pdf-object/src/object_lookup.rs | 1276 +++++++++++++++++ .../pdf-parser/src/cross_reference_stream.rs | 9 +- crates/pdf-parser/src/stream.rs | 9 +- .../src/external_graphics_state.rs | 7 +- crates/pdf-resources/src/form.rs | 9 +- crates/pdf-resources/src/pattern.rs | 23 +- crates/pdf-resources/src/resources.rs | 5 +- crates/pdf-resources/src/xobject.rs | 7 +- crates/pdf-shading/src/parse.rs | 63 +- 61 files changed, 1642 insertions(+), 688 deletions(-) create mode 100644 crates/pdf-object/src/object_lookup.rs diff --git a/crates/pdf-annotation-types/src/action.rs b/crates/pdf-annotation-types/src/action.rs index 2a9f4afd..eb96e5f3 100644 --- a/crates/pdf-annotation-types/src/action.rs +++ b/crates/pdf-annotation-types/src/action.rs @@ -1,6 +1,6 @@ use pdf_object::{ - dictionary::Dictionary, error::ObjectError, object_resolver::ObjectResolver, - object_variant::ObjectVariant, + dictionary::Dictionary, error::ObjectError, object_lookup::ObjectLookupExt, + object_resolver::ObjectResolver, object_variant::ObjectVariant, }; use crate::{ @@ -113,10 +113,7 @@ impl AnnotationAction { }; let action_dictionary = value.try_dictionary(objects)?.clone(); - let action_type = action_dictionary - .get_or_err("S")? - .try_str(objects)? - .into_owned(); + let action_type = action_dictionary.required_str("S", objects)?.into_owned(); let action = match action_type.as_ref() { "GoTo" => Self::GoTo { @@ -135,19 +132,11 @@ impl AnnotationAction { .get("D") .map(|value| AnnotationDestination::from_object(value, "D", objects)) .transpose()?, - new_window: action_dictionary - .get("NewWindow") - .map(|value| value.try_boolean(objects)) - .transpose()?, + new_window: action_dictionary.optional_boolean("NewWindow", objects)?, }, "URI" => Self::Uri { - uri: action_dictionary - .get_or_err("URI")? - .try_bytes_vec(objects)?, - is_map: action_dictionary - .get("IsMap") - .map(|value| value.try_boolean(objects)) - .transpose()?, + uri: action_dictionary.required_bytes_vec("URI", objects)?, + is_map: action_dictionary.optional_boolean("IsMap", objects)?, }, "Launch" => Self::Launch { file_specification: FileSpecification::from_dictionary( @@ -161,7 +150,7 @@ impl AnnotationAction { .transpose()?, }, "Named" => Self::Named { - name: action_dictionary.get_or_err("N")?.try_bytes_vec(objects)?, + name: action_dictionary.required_bytes_vec("N", objects)?, }, "SubmitForm" => Self::SubmitForm { file_specification: FileSpecification::from_dictionary( @@ -170,17 +159,11 @@ impl AnnotationAction { objects, )?, fields: name_list(&action_dictionary, "Fields", objects)?, - flags: action_dictionary - .get("Flags") - .map(|value| value.try_number::(objects)) - .transpose()?, + flags: action_dictionary.optional_number::("Flags", objects)?, }, "ResetForm" => Self::ResetForm { fields: name_list(&action_dictionary, "Fields", objects)?, - flags: action_dictionary - .get("Flags") - .map(|value| value.try_number::(objects)) - .transpose()?, + flags: action_dictionary.optional_number::("Flags", objects)?, }, "ImportData" => Self::ImportData { file_specification: FileSpecification::from_object( @@ -193,16 +176,10 @@ impl AnnotationAction { }, "SetOCGState" => Self::SetOCGState { state: name_list(&action_dictionary, "State", objects)?.unwrap_or_default(), - preserve_rb: action_dictionary - .get("PreserveRB") - .map(|value| value.try_boolean(objects)) - .transpose()?, + preserve_rb: action_dictionary.optional_boolean("PreserveRB", objects)?, }, "Rendition" => Self::Rendition { - operation: action_dictionary - .get("OP") - .map(|value| value.try_number::(objects)) - .transpose()?, + operation: action_dictionary.optional_number::("OP", objects)?, rendition: Rendition::from_dictionary(&action_dictionary, objects)?, }, "Trans" => Self::Trans { @@ -210,10 +187,7 @@ impl AnnotationAction { .get("Trans") .map(|value| helpers::dictionary(value, objects)) .transpose()?, - duration: action_dictionary - .get("D") - .map(|value| value.try_number::(objects)) - .transpose()?, + duration: action_dictionary.optional_number::("D", objects)?, }, "GoTo3DView" => Self::GoTo3DView { view: three_d_view(&action_dictionary, "TA", objects)?, diff --git a/crates/pdf-annotation-types/src/annotation.rs b/crates/pdf-annotation-types/src/annotation.rs index ea84e02f..ef9b17b2 100644 --- a/crates/pdf-annotation-types/src/annotation.rs +++ b/crates/pdf-annotation-types/src/annotation.rs @@ -1,6 +1,8 @@ use pdf_content_stream::ContentStreamIdAllocator; use pdf_graphics::rect::Rect; -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use pdf_resources::{object_reader::ReadCycleTracker, resource_cache::ResourceCache}; use crate::{ @@ -118,26 +120,13 @@ impl Annotation { let kind = AnnotationKind::from_dictionary(&subtype, dictionary, objects)?; - let contents = dictionary - .get("Contents") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let name = dictionary - .get("NM") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let flags = dictionary - .get("F") - .map(|value| value.try_number::(objects)) - .transpose()?; + let contents = dictionary.optional_bytes_vec("Contents", objects)?; + let name = dictionary.optional_bytes_vec("NM", objects)?; + let flags = dictionary.optional_number::("F", objects)?; let appearance_state = dictionary - .get("AS") - .map(|value| value.try_str(objects).map(|s| s.into_owned())) - .transpose()?; - let struct_parent = dictionary - .get("StructParent") - .map(|value| value.try_number::(objects)) - .transpose()?; + .optional_str("AS", objects)? + .map(|s| s.into_owned()); + let struct_parent = dictionary.optional_number::("StructParent", objects)?; let appearance = AppearanceDictionary::from_dictionary( dictionary, diff --git a/crates/pdf-annotation-types/src/annotation_border.rs b/crates/pdf-annotation-types/src/annotation_border.rs index 81262c98..4b90d49d 100644 --- a/crates/pdf-annotation-types/src/annotation_border.rs +++ b/crates/pdf-annotation-types/src/annotation_border.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::AnnotationError; @@ -19,12 +21,11 @@ impl AnnotationBorder { dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> Result, AnnotationError> { - let Some(value) = dictionary.get("Border") else { + let Some(value) = dictionary.optional_array("Border", objects)? else { return Ok(None); }; - let [horizontal_radius, vertical_radius, width, rest @ ..] = value.try_array(objects)? - else { + let [horizontal_radius, vertical_radius, width, rest @ ..] = value else { return Err(AnnotationError::InvalidEntry { entry: "Border", reason: "expected an array with at least 3 numbers".to_owned(), diff --git a/crates/pdf-annotation-types/src/appearance_characteristics.rs b/crates/pdf-annotation-types/src/appearance_characteristics.rs index dbf917ed..e00eaa28 100644 --- a/crates/pdf-annotation-types/src/appearance_characteristics.rs +++ b/crates/pdf-annotation-types/src/appearance_characteristics.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationColor, AnnotationError}; @@ -28,24 +30,12 @@ impl AppearanceCharacteristics { }; let dictionary = value.try_dictionary(objects)?; - let rotation = dictionary - .get("R") - .map(|value| value.try_number::(objects)) - .transpose()?; + let rotation = dictionary.optional_number::("R", objects)?; let border_color = AnnotationColor::from_dictionary(dictionary, "BC", objects)?; let background_color = AnnotationColor::from_dictionary(dictionary, "BG", objects)?; - let normal_caption = dictionary - .get("CA") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let rollover_caption = dictionary - .get("RC") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let alternate_caption = dictionary - .get("AC") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; + let normal_caption = dictionary.optional_bytes_vec("CA", objects)?; + let rollover_caption = dictionary.optional_bytes_vec("RC", objects)?; + let alternate_caption = dictionary.optional_bytes_vec("AC", objects)?; Ok(Some(Self { rotation, diff --git a/crates/pdf-annotation-types/src/border_effect.rs b/crates/pdf-annotation-types/src/border_effect.rs index 88e9f564..31aa535b 100644 --- a/crates/pdf-annotation-types/src/border_effect.rs +++ b/crates/pdf-annotation-types/src/border_effect.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationError, BorderEffectStyle}; @@ -29,10 +31,7 @@ impl BorderEffect { .map(|name| BorderEffectStyle::from(name.as_ref())) }) .transpose()?; - let intensity = dictionary - .get("I") - .map(|value| value.try_number::(objects)) - .transpose()?; + let intensity = dictionary.optional_number::("I", objects)?; Ok(Some(Self { style, intensity })) } diff --git a/crates/pdf-annotation-types/src/border_style.rs b/crates/pdf-annotation-types/src/border_style.rs index 21b5e69a..a150bf44 100644 --- a/crates/pdf-annotation-types/src/border_style.rs +++ b/crates/pdf-annotation-types/src/border_style.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationError, BorderStyleName}; @@ -23,10 +25,7 @@ impl BorderStyle { }; let dictionary = value.try_dictionary(objects)?; - let width = dictionary - .get("W") - .map(|value| value.try_number::(objects)) - .transpose()?; + let width = dictionary.optional_number::("W", objects)?; let style = dictionary .get("S") .map(|value| { @@ -35,10 +34,7 @@ impl BorderStyle { .map(|name| BorderStyleName::from(name.as_ref())) }) .transpose()?; - let dash_pattern = dictionary - .get("D") - .map(|value| value.try_vec_of::(objects)) - .transpose()?; + let dash_pattern = dictionary.optional_vec_of::("D", objects)?; Ok(Some(Self { width, diff --git a/crates/pdf-annotation-types/src/destination.rs b/crates/pdf-annotation-types/src/destination.rs index c73da11b..5750f56d 100644 --- a/crates/pdf-annotation-types/src/destination.rs +++ b/crates/pdf-annotation-types/src/destination.rs @@ -1,6 +1,6 @@ use pdf_object::{ - dictionary::Dictionary, error::ObjectError, object_resolver::ObjectResolver, - object_variant::ObjectVariant, + dictionary::Dictionary, error::ObjectError, object_lookup::ObjectLookupExt, + object_resolver::ObjectResolver, object_variant::ObjectVariant, }; use crate::AnnotationError; @@ -147,34 +147,34 @@ fn explicit_destination( match mode_item.try_str(objects)?.as_ref() { "XYZ" => Ok(ExplicitDestination::Xyz { page, - left: optional_item_number(items, 2, objects)?, - top: optional_item_number(items, 3, objects)?, - zoom: optional_item_number(items, 4, objects)?, + left: items.optional_number(2, objects)?, + top: items.optional_number(3, objects)?, + zoom: items.optional_number(4, objects)?, }), "Fit" => Ok(ExplicitDestination::Fit { page }), "FitH" => Ok(ExplicitDestination::FitH { page, - top: optional_item_number(items, 2, objects)?, + top: items.optional_number(2, objects)?, }), "FitV" => Ok(ExplicitDestination::FitV { page, - left: optional_item_number(items, 2, objects)?, + left: items.optional_number(2, objects)?, }), "FitR" => Ok(ExplicitDestination::FitR { page, - left: required_item_number(items, 2, entry, objects)?, - bottom: required_item_number(items, 3, entry, objects)?, - right: required_item_number(items, 4, entry, objects)?, - top: required_item_number(items, 5, entry, objects)?, + left: items.required_number(2, objects)?, + bottom: items.required_number(3, objects)?, + right: items.required_number(4, objects)?, + top: items.required_number(5, objects)?, }), "FitB" => Ok(ExplicitDestination::FitB { page }), "FitBH" => Ok(ExplicitDestination::FitBH { page, - top: optional_item_number(items, 2, objects)?, + top: items.optional_number(2, objects)?, }), "FitBV" => Ok(ExplicitDestination::FitBV { page, - left: optional_item_number(items, 2, objects)?, + left: items.optional_number(2, objects)?, }), other => Err(AnnotationError::InvalidEntry { entry, @@ -192,41 +192,3 @@ fn destination_target( _ => DestinationTarget::Dictionary(value.try_dictionary(objects)?.clone()), }) } - -fn optional_item_number( - values: &[ObjectVariant], - index: usize, - objects: &dyn ObjectResolver, -) -> Result, AnnotationError> -where - T: num_traits::FromPrimitive, -{ - let Some(value) = values.get(index) else { - return Ok(None); - }; - let value = if let ObjectVariant::Reference(_) = value { - objects.resolve_object(value)? - } else { - value - }; - - match value { - ObjectVariant::Null => Ok(None), - _ => Ok(Some(value.try_number::(objects)?)), - } -} - -fn required_item_number( - values: &[ObjectVariant], - index: usize, - entry: &'static str, - objects: &dyn ObjectResolver, -) -> Result -where - T: num_traits::FromPrimitive, -{ - Ok(values - .get(index) - .ok_or(AnnotationError::MissingEntry { entry })? - .try_number::(objects)?) -} diff --git a/crates/pdf-annotation-types/src/file_specification.rs b/crates/pdf-annotation-types/src/file_specification.rs index 0ae8f382..50c8d87d 100644 --- a/crates/pdf-annotation-types/src/file_specification.rs +++ b/crates/pdf-annotation-types/src/file_specification.rs @@ -1,5 +1,6 @@ use pdf_object::{ - dictionary::Dictionary, object_resolver::ObjectResolver, object_variant::ObjectVariant, + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, + object_variant::ObjectVariant, }; use crate::AnnotationError; @@ -50,34 +51,13 @@ impl FileSpecification { return Ok(Self::Path(value.try_bytes(objects)?.to_vec())); }; - let file_system = dictionary - .get("FS") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let file_name = dictionary - .get("F") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let unicode_file_name = dictionary - .get("UF") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let mac_file_name = dictionary - .get("Mac") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let dos_file_name = dictionary - .get("DOS") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let unix_file_name = dictionary - .get("Unix") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let volatile = dictionary - .get("V") - .map(|value| value.try_boolean(objects)) - .transpose()?; + let file_system = dictionary.optional_bytes_vec("FS", objects)?; + let file_name = dictionary.optional_bytes_vec("F", objects)?; + let unicode_file_name = dictionary.optional_bytes_vec("UF", objects)?; + let mac_file_name = dictionary.optional_bytes_vec("Mac", objects)?; + let dos_file_name = dictionary.optional_bytes_vec("DOS", objects)?; + let unix_file_name = dictionary.optional_bytes_vec("Unix", objects)?; + let volatile = dictionary.optional_boolean("V", objects)?; Ok(Self::Dictionary(FileSpecificationDictionary { file_system, diff --git a/crates/pdf-annotation-types/src/subtypes/caret.rs b/crates/pdf-annotation-types/src/subtypes/caret.rs index 63399cf5..d2064eb2 100644 --- a/crates/pdf-annotation-types/src/subtypes/caret.rs +++ b/crates/pdf-annotation-types/src/subtypes/caret.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationError, CaretSymbolStyle}; @@ -15,10 +17,7 @@ impl CaretAnnotation { dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> Result { - let difference_rect = dictionary - .get("RD") - .map(|value| value.try_array_of::(objects)) - .transpose()?; + let difference_rect = dictionary.optional_array_of::("RD", objects)?; let style = dictionary .get("Sy") .map(|value| { diff --git a/crates/pdf-annotation-types/src/subtypes/circle.rs b/crates/pdf-annotation-types/src/subtypes/circle.rs index bcc520b9..87b0f06a 100644 --- a/crates/pdf-annotation-types/src/subtypes/circle.rs +++ b/crates/pdf-annotation-types/src/subtypes/circle.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationColor, AnnotationError, BorderEffect, BorderStyle}; @@ -22,10 +24,7 @@ impl CircleAnnotation { let border_style = BorderStyle::from_dictionary(dictionary, "BS", objects)?; let interior_color = AnnotationColor::from_dictionary(dictionary, "IC", objects)?; let border_effect = BorderEffect::from_dictionary(dictionary, "BE", objects)?; - let difference_rect = dictionary - .get("RD") - .map(|value| value.try_array_of::(objects)) - .transpose()?; + let difference_rect = dictionary.optional_array_of::("RD", objects)?; Ok(Self { border_style, diff --git a/crates/pdf-annotation-types/src/subtypes/file_attachment.rs b/crates/pdf-annotation-types/src/subtypes/file_attachment.rs index 35bf0192..9fbc390b 100644 --- a/crates/pdf-annotation-types/src/subtypes/file_attachment.rs +++ b/crates/pdf-annotation-types/src/subtypes/file_attachment.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationError, FileSpecification}; @@ -17,10 +19,7 @@ impl FileAttachmentAnnotation { ) -> Result { let file_specification = FileSpecification::from_dictionary(dictionary, "FS", objects)? .ok_or(AnnotationError::MissingEntry { entry: "FS" })?; - let name = dictionary - .get("Name") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; + let name = dictionary.optional_bytes_vec("Name", objects)?; Ok(Self { file_specification, diff --git a/crates/pdf-annotation-types/src/subtypes/free_text.rs b/crates/pdf-annotation-types/src/subtypes/free_text.rs index 3273385d..d0709d55 100644 --- a/crates/pdf-annotation-types/src/subtypes/free_text.rs +++ b/crates/pdf-annotation-types/src/subtypes/free_text.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationError, BorderEffect}; @@ -27,34 +29,13 @@ impl FreeTextAnnotation { dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> Result { - let default_appearance = dictionary - .get("DA") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let quadding = dictionary - .get("Q") - .map(|value| value.try_number::(objects)) - .transpose()?; - let rich_contents = dictionary - .get("RC") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let default_style = dictionary - .get("DS") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let callout_line = dictionary - .get("CL") - .map(|value| value.try_vec_of::(objects)) - .transpose()?; - let difference_rect = dictionary - .get("RD") - .map(|value| value.try_array_of::(objects)) - .transpose()?; - let intent = dictionary - .get("IT") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; + let default_appearance = dictionary.optional_bytes_vec("DA", objects)?; + let quadding = dictionary.optional_number::("Q", objects)?; + let rich_contents = dictionary.optional_bytes_vec("RC", objects)?; + let default_style = dictionary.optional_bytes_vec("DS", objects)?; + let callout_line = dictionary.optional_vec_of::("CL", objects)?; + let difference_rect = dictionary.optional_array_of::("RD", objects)?; + let intent = dictionary.optional_bytes_vec("IT", objects)?; let border_effect = BorderEffect::from_dictionary(dictionary, "BE", objects)?; Ok(Self { diff --git a/crates/pdf-annotation-types/src/subtypes/highlight.rs b/crates/pdf-annotation-types/src/subtypes/highlight.rs index 9d4d3a4f..7bf48ec5 100644 --- a/crates/pdf-annotation-types/src/subtypes/highlight.rs +++ b/crates/pdf-annotation-types/src/subtypes/highlight.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationColor, AnnotationError, QuadPoints}; @@ -19,10 +21,7 @@ impl HighlightAnnotation { ) -> Result { let quad_points = super::required_quad_points(dictionary, objects)?; let color = AnnotationColor::from_dictionary(dictionary, "C", objects)?; - let constant_opacity = dictionary - .get("CA") - .map(|value| value.try_number::(objects)) - .transpose()?; + let constant_opacity = dictionary.optional_number::("CA", objects)?; Ok(Self { quad_points, diff --git a/crates/pdf-annotation-types/src/subtypes/line.rs b/crates/pdf-annotation-types/src/subtypes/line.rs index 487c7696..5575da1e 100644 --- a/crates/pdf-annotation-types/src/subtypes/line.rs +++ b/crates/pdf-annotation-types/src/subtypes/line.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationColor, AnnotationError, BorderStyle, LineEndingStyle}; @@ -33,22 +35,10 @@ impl LineAnnotation { let line_endings = super::line_endings(dictionary, objects)?; let border_style = BorderStyle::from_dictionary(dictionary, "BS", objects)?; let interior_color = AnnotationColor::from_dictionary(dictionary, "IC", objects)?; - let leader_line_length = dictionary - .get("LL") - .map(|value| value.try_number::(objects)) - .transpose()?; - let leader_line_extension = dictionary - .get("LLE") - .map(|value| value.try_number::(objects)) - .transpose()?; - let caption = dictionary - .get("Cap") - .map(|value| value.try_boolean(objects)) - .transpose()?; - let intent = dictionary - .get("IT") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; + let leader_line_length = dictionary.optional_number::("LL", objects)?; + let leader_line_extension = dictionary.optional_number::("LLE", objects)?; + let caption = dictionary.optional_boolean("Cap", objects)?; + let intent = dictionary.optional_bytes_vec("IT", objects)?; Ok(Self { line, diff --git a/crates/pdf-annotation-types/src/subtypes/movie.rs b/crates/pdf-annotation-types/src/subtypes/movie.rs index 93db8df1..aabd4e6a 100644 --- a/crates/pdf-annotation-types/src/subtypes/movie.rs +++ b/crates/pdf-annotation-types/src/subtypes/movie.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationError, helpers}; @@ -19,10 +21,7 @@ impl MovieAnnotation { .get("Movie") .map(|value| helpers::dictionary(value, objects)) .transpose()?; - let title = dictionary - .get("T") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; + let title = dictionary.optional_bytes_vec("T", objects)?; Ok(Self { movie, title }) } diff --git a/crates/pdf-annotation-types/src/subtypes/polygon.rs b/crates/pdf-annotation-types/src/subtypes/polygon.rs index 43eecf93..03c8cec1 100644 --- a/crates/pdf-annotation-types/src/subtypes/polygon.rs +++ b/crates/pdf-annotation-types/src/subtypes/polygon.rs @@ -1,5 +1,7 @@ use pdf_graphics::pdf_path::PdfPath; -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationColor, AnnotationError, BorderStyle, LineEndingStyle, helpers}; @@ -29,10 +31,7 @@ impl PolygonAnnotation { let line_endings = super::line_endings(dictionary, objects)?; let interior_color = AnnotationColor::from_dictionary(dictionary, "IC", objects)?; let border_style = BorderStyle::from_dictionary(dictionary, "BS", objects)?; - let intent = dictionary - .get("IT") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; + let intent = dictionary.optional_bytes_vec("IT", objects)?; Ok(Self { vertices, diff --git a/crates/pdf-annotation-types/src/subtypes/polyline.rs b/crates/pdf-annotation-types/src/subtypes/polyline.rs index 4a6cfe5b..ad36ca1c 100644 --- a/crates/pdf-annotation-types/src/subtypes/polyline.rs +++ b/crates/pdf-annotation-types/src/subtypes/polyline.rs @@ -1,5 +1,7 @@ use pdf_graphics::pdf_path::PdfPath; -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationColor, AnnotationError, BorderStyle, LineEndingStyle, helpers}; @@ -27,10 +29,7 @@ impl PolyLineAnnotation { let line_endings = super::line_endings(dictionary, objects)?; let interior_color = AnnotationColor::from_dictionary(dictionary, "IC", objects)?; let border_style = BorderStyle::from_dictionary(dictionary, "BS", objects)?; - let intent = dictionary - .get("IT") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; + let intent = dictionary.optional_bytes_vec("IT", objects)?; Ok(Self { vertices, diff --git a/crates/pdf-annotation-types/src/subtypes/popup.rs b/crates/pdf-annotation-types/src/subtypes/popup.rs index 30483164..76edc6f6 100644 --- a/crates/pdf-annotation-types/src/subtypes/popup.rs +++ b/crates/pdf-annotation-types/src/subtypes/popup.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::AnnotationError; @@ -19,10 +21,7 @@ impl PopupAnnotation { .get("Parent") .map(|obj| obj.try_object_number()) .transpose()?; - let open = dictionary - .get("Open") - .map(|value| value.try_boolean(objects)) - .transpose()?; + let open = dictionary.optional_boolean("Open", objects)?; Ok(Self { parent, open }) } diff --git a/crates/pdf-annotation-types/src/subtypes/sound.rs b/crates/pdf-annotation-types/src/subtypes/sound.rs index ad555c9a..01ed2d65 100644 --- a/crates/pdf-annotation-types/src/subtypes/sound.rs +++ b/crates/pdf-annotation-types/src/subtypes/sound.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationError, helpers}; @@ -21,14 +23,8 @@ impl SoundAnnotation { .get("Sound") .map(|value| helpers::dictionary(value, objects)) .transpose()?; - let rate = dictionary - .get("R") - .map(|value| value.try_number::(objects)) - .transpose()?; - let channels = dictionary - .get("C") - .map(|value| value.try_number::(objects)) - .transpose()?; + let rate = dictionary.optional_number::("R", objects)?; + let channels = dictionary.optional_number::("C", objects)?; Ok(Self { sound, diff --git a/crates/pdf-annotation-types/src/subtypes/square.rs b/crates/pdf-annotation-types/src/subtypes/square.rs index 9e087cc0..2cccc796 100644 --- a/crates/pdf-annotation-types/src/subtypes/square.rs +++ b/crates/pdf-annotation-types/src/subtypes/square.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationColor, AnnotationError, BorderEffect, BorderStyle}; @@ -22,10 +24,7 @@ impl SquareAnnotation { let border_style = BorderStyle::from_dictionary(dictionary, "BS", objects)?; let interior_color = AnnotationColor::from_dictionary(dictionary, "IC", objects)?; let border_effect = BorderEffect::from_dictionary(dictionary, "BE", objects)?; - let difference_rect = dictionary - .get("RD") - .map(|value| value.try_array_of::(objects)) - .transpose()?; + let difference_rect = dictionary.optional_array_of::("RD", objects)?; Ok(Self { border_style, diff --git a/crates/pdf-annotation-types/src/subtypes/squiggly.rs b/crates/pdf-annotation-types/src/subtypes/squiggly.rs index 7a9bea73..b56998e4 100644 --- a/crates/pdf-annotation-types/src/subtypes/squiggly.rs +++ b/crates/pdf-annotation-types/src/subtypes/squiggly.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationColor, AnnotationError, QuadPoints}; @@ -19,10 +21,7 @@ impl SquigglyAnnotation { ) -> Result { let quad_points = super::required_quad_points(dictionary, objects)?; let color = AnnotationColor::from_dictionary(dictionary, "C", objects)?; - let constant_opacity = dictionary - .get("CA") - .map(|value| value.try_number::(objects)) - .transpose()?; + let constant_opacity = dictionary.optional_number::("CA", objects)?; Ok(Self { quad_points, diff --git a/crates/pdf-annotation-types/src/subtypes/stamp.rs b/crates/pdf-annotation-types/src/subtypes/stamp.rs index 81528aba..89adad02 100644 --- a/crates/pdf-annotation-types/src/subtypes/stamp.rs +++ b/crates/pdf-annotation-types/src/subtypes/stamp.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::AnnotationError; @@ -13,11 +15,7 @@ impl StampAnnotation { dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> Result { - let name = dictionary - .get("Name") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - + let name = dictionary.optional_bytes_vec("Name", objects)?; Ok(Self { name }) } } diff --git a/crates/pdf-annotation-types/src/subtypes/strikeout.rs b/crates/pdf-annotation-types/src/subtypes/strikeout.rs index 317e33af..8bff913b 100644 --- a/crates/pdf-annotation-types/src/subtypes/strikeout.rs +++ b/crates/pdf-annotation-types/src/subtypes/strikeout.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationColor, AnnotationError, QuadPoints}; @@ -19,10 +21,7 @@ impl StrikeOutAnnotation { ) -> Result { let quad_points = super::required_quad_points(dictionary, objects)?; let color = AnnotationColor::from_dictionary(dictionary, "C", objects)?; - let constant_opacity = dictionary - .get("CA") - .map(|value| value.try_number::(objects)) - .transpose()?; + let constant_opacity = dictionary.optional_number::("CA", objects)?; Ok(Self { quad_points, diff --git a/crates/pdf-annotation-types/src/subtypes/text.rs b/crates/pdf-annotation-types/src/subtypes/text.rs index 5a858c5e..201e3b85 100644 --- a/crates/pdf-annotation-types/src/subtypes/text.rs +++ b/crates/pdf-annotation-types/src/subtypes/text.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::AnnotationError; @@ -23,26 +25,11 @@ impl TextAnnotation { dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> Result { - let open = dictionary - .get("Open") - .map(|value| value.try_boolean(objects)) - .transpose()?; - let name = dictionary - .get("Name") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let state = dictionary - .get("State") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let state_model = dictionary - .get("StateModel") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let intent = dictionary - .get("IT") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; + let open = dictionary.optional_boolean("Open", objects)?; + let name = dictionary.optional_bytes_vec("Name", objects)?; + let state = dictionary.optional_bytes_vec("State", objects)?; + let state_model = dictionary.optional_bytes_vec("StateModel", objects)?; + let intent = dictionary.optional_bytes_vec("IT", objects)?; let ex_data = dictionary .get("ExData") .map(|value| crate::helpers::dictionary(value, objects)) diff --git a/crates/pdf-annotation-types/src/subtypes/underline.rs b/crates/pdf-annotation-types/src/subtypes/underline.rs index f7926b73..606c03a4 100644 --- a/crates/pdf-annotation-types/src/subtypes/underline.rs +++ b/crates/pdf-annotation-types/src/subtypes/underline.rs @@ -1,4 +1,6 @@ -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{AnnotationColor, AnnotationError, QuadPoints}; @@ -19,10 +21,7 @@ impl UnderlineAnnotation { ) -> Result { let quad_points = super::required_quad_points(dictionary, objects)?; let color = AnnotationColor::from_dictionary(dictionary, "C", objects)?; - let constant_opacity = dictionary - .get("CA") - .map(|value| value.try_number::(objects)) - .transpose()?; + let constant_opacity = dictionary.optional_number::("CA", objects)?; Ok(Self { quad_points, diff --git a/crates/pdf-annotation-types/src/subtypes/widget.rs b/crates/pdf-annotation-types/src/subtypes/widget.rs index c871d17a..e6f0ca51 100644 --- a/crates/pdf-annotation-types/src/subtypes/widget.rs +++ b/crates/pdf-annotation-types/src/subtypes/widget.rs @@ -1,5 +1,6 @@ use pdf_object::{ - dictionary::Dictionary, object_resolver::ObjectResolver, object_variant::ObjectVariant, + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, + object_variant::ObjectVariant, }; use crate::{AnnotationAction, AnnotationError, AppearanceCharacteristics, BorderStyle, helpers}; @@ -51,26 +52,11 @@ impl WidgetAnnotation { dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> Result { - let field_type = dictionary - .get("FT") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let field_name = dictionary - .get("T") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let alternate_name = dictionary - .get("TU") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let mapping_name = dictionary - .get("TM") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let field_flags = dictionary - .get("Ff") - .map(|value| value.try_number::(objects)) - .transpose()?; + let field_type = dictionary.optional_bytes_vec("FT", objects)?; + let field_name = dictionary.optional_bytes_vec("T", objects)?; + let alternate_name = dictionary.optional_bytes_vec("TU", objects)?; + let mapping_name = dictionary.optional_bytes_vec("TM", objects)?; + let field_flags = dictionary.optional_number::("Ff", objects)?; let value = dictionary .get("V") .map(|value| widget_field_value("V", value, objects)) @@ -79,14 +65,8 @@ impl WidgetAnnotation { .get("DV") .map(|value| widget_field_value("DV", value, objects)) .transpose()?; - let default_appearance = dictionary - .get("DA") - .map(|value| value.try_bytes_vec(objects)) - .transpose()?; - let quadding = dictionary - .get("Q") - .map(|value| value.try_number::(objects)) - .transpose()?; + let default_appearance = dictionary.optional_bytes_vec("DA", objects)?; + let quadding = dictionary.optional_number::("Q", objects)?; let additional_actions = dictionary .get("AA") .map(|value| helpers::dictionary(value, objects)) diff --git a/crates/pdf-ccitt/src/ccitt_fax_params.rs b/crates/pdf-ccitt/src/ccitt_fax_params.rs index 80d3bc63..dfd37460 100644 --- a/crates/pdf-ccitt/src/ccitt_fax_params.rs +++ b/crates/pdf-ccitt/src/ccitt_fax_params.rs @@ -1,4 +1,7 @@ -use pdf_object::{dictionary::Dictionary, error::ObjectError, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, error::ObjectError, object_lookup::ObjectLookupExt, + object_resolver::ObjectResolver, +}; /// Decode parameters for the `CCITTFaxDecode` filter (PDF spec §7.4.6, Table 11). #[derive(Debug, Clone)] @@ -57,29 +60,29 @@ impl CCITTFaxParams { ) -> Result { let mut p = Self::default(); - if let Some(obj) = dict.get("K") { - p.k = obj.try_number::(objects)?; + if let Some(value) = dict.optional_number::("K", objects)? { + p.k = value; } - if let Some(obj) = dict.get("Columns") { - p.columns = obj.try_number::(objects)?; + if let Some(value) = dict.optional_number::("Columns", objects)? { + p.columns = value; } - if let Some(obj) = dict.get("Rows") { - p.rows = obj.try_number::(objects)?; + if let Some(value) = dict.optional_number::("Rows", objects)? { + p.rows = value; } - if let Some(obj) = dict.get("EndOfLine") { - p.end_of_line = obj.try_boolean(objects)?; + if let Some(obj) = dict.optional_boolean("EndOfLine", objects)? { + p.end_of_line = obj; } - if let Some(obj) = dict.get("EncodedByteAlign") { - p.encoded_byte_align = obj.try_boolean(objects)?; + if let Some(obj) = dict.optional_boolean("EncodedByteAlign", objects)? { + p.encoded_byte_align = obj; } - if let Some(obj) = dict.get("EndOfBlock") { - p.end_of_block = obj.try_boolean(objects)?; + if let Some(obj) = dict.optional_boolean("EndOfBlock", objects)? { + p.end_of_block = obj; } - if let Some(obj) = dict.get("BlackIs1") { - p.black_is1 = obj.try_boolean(objects)?; + if let Some(obj) = dict.optional_boolean("BlackIs1", objects)? { + p.black_is1 = obj; } - if let Some(obj) = dict.get("DamagedRowsBeforeError") { - p.damaged_rows_before_error = obj.try_number::(objects)?; + if let Some(value) = dict.optional_number::("DamagedRowsBeforeError", objects)? { + p.damaged_rows_before_error = value; } Ok(p) diff --git a/crates/pdf-color-space/src/cal_gray_color_space.rs b/crates/pdf-color-space/src/cal_gray_color_space.rs index b21d7ca2..6f908d2f 100644 --- a/crates/pdf-color-space/src/cal_gray_color_space.rs +++ b/crates/pdf-color-space/src/cal_gray_color_space.rs @@ -1,5 +1,7 @@ use pdf_graphics::color::Color; -use pdf_object::{object_resolver::ObjectResolver, object_variant::ObjectVariant}; +use pdf_object::{ + object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, object_variant::ObjectVariant, +}; use crate::{color_space::ColorSpace, error::ColorSpaceError}; @@ -28,18 +30,12 @@ pub(crate) fn parse_cal_gray_color_space( }); }; let dict = dict_obj.try_dictionary(objects)?; - let white_point = dict - .get_or_err("WhitePoint")? - .try_array_of::(objects)?; + let white_point = dict.required_array_of::("WhitePoint", objects)?; let black_point = dict - .get("BlackPoint") - .map(|bp| bp.try_array_of::(objects)) - .transpose()? + .optional_array_of::("BlackPoint", objects)? .unwrap_or([0.0, 0.0, 0.0]); let gamma = dict - .get("Gamma") - .map(|g| g.try_number::(objects)) - .transpose()? + .optional_number::("Gamma", objects)? .unwrap_or(1.0); Ok(ColorSpace::CalGray(CalGrayColorSpace { white_point, diff --git a/crates/pdf-color-space/src/cal_rgb_color_space.rs b/crates/pdf-color-space/src/cal_rgb_color_space.rs index 6aa4fa48..2fd81aa5 100644 --- a/crates/pdf-color-space/src/cal_rgb_color_space.rs +++ b/crates/pdf-color-space/src/cal_rgb_color_space.rs @@ -1,4 +1,5 @@ use pdf_graphics::color::Color; +use pdf_object::object_lookup::ObjectLookupExt; use pdf_object::{object_resolver::ObjectResolver, object_variant::ObjectVariant}; use crate::cal_gray_color_space::xyz_to_srgb; @@ -34,24 +35,16 @@ pub(crate) fn parse_cal_rgb_color_space( }); }; let dict = dict_obj.try_dictionary(objects)?; - let white_point = dict - .get_or_err("WhitePoint")? - .try_array_of::(objects)?; + let white_point = dict.required_array_of::("WhitePoint", objects)?; let black_point = dict - .get("BlackPoint") - .map(|bp| bp.try_array_of::(objects)) - .transpose()? - .unwrap_or([0.0, 0.0, 0.0]); + .optional_array_of::("BlackPoint", objects)? + .unwrap_or_default(); let gamma = dict - .get("Gamma") - .map(|g| g.try_array_of::(objects)) - .transpose()? + .optional_array_of::("Gamma", objects)? .unwrap_or([1.0, 1.0, 1.0]); // Column-major 3×3: [Xa Ya Za Xb Yb Zb Xc Yc Zc], default identity. let matrix = dict - .get("Matrix") - .map(|m| m.try_array_of::(objects)) - .transpose()? + .optional_array_of::("Matrix", objects)? .unwrap_or([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]); Ok(ColorSpace::CalRGB(CalRGBColorSpace { white_point, diff --git a/crates/pdf-color-space/src/icc_based_color_space.rs b/crates/pdf-color-space/src/icc_based_color_space.rs index 49890e14..1ec89ca2 100644 --- a/crates/pdf-color-space/src/icc_based_color_space.rs +++ b/crates/pdf-color-space/src/icc_based_color_space.rs @@ -1,7 +1,9 @@ use std::sync::Arc; use pdf_graphics::color::Color; -use pdf_object::{object_resolver::ObjectResolver, object_variant::ObjectVariant}; +use pdf_object::{ + object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, object_variant::ObjectVariant, +}; use crate::{ color_space::ColorSpace, color_space_reader::parse_color_space_object, error::ColorSpaceError, @@ -42,10 +44,7 @@ pub(crate) fn parse_icc_based_color_space( }; let stream = icc_stream.try_stream(objects)?; - let num_components = stream - .dictionary - .get_or_err("N")? - .try_number::(objects)?; + let num_components = stream.dictionary.required_number::("N", objects)?; // N shall be 1, 3, or 4. if !matches!(num_components, 1 | 3 | 4) { diff --git a/crates/pdf-color-space/src/lab_color_space.rs b/crates/pdf-color-space/src/lab_color_space.rs index 6c676c90..b49a0649 100644 --- a/crates/pdf-color-space/src/lab_color_space.rs +++ b/crates/pdf-color-space/src/lab_color_space.rs @@ -1,5 +1,7 @@ use pdf_graphics::color::Color; -use pdf_object::{object_resolver::ObjectResolver, object_variant::ObjectVariant}; +use pdf_object::{ + object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, object_variant::ObjectVariant, +}; use crate::{color_space::ColorSpace, error::ColorSpaceError}; @@ -32,18 +34,12 @@ pub(crate) fn parse_lab_color_space( }); }; let dict = dict_obj.try_dictionary(objects)?; - let white_point = dict - .get_or_err("WhitePoint")? - .try_array_of::(objects)?; + let white_point = dict.required_array_of::("WhitePoint", objects)?; let black_point = dict - .get("BlackPoint") - .map(|bp| bp.try_array_of::(objects)) - .transpose()? + .optional_array_of::("BlackPoint", objects)? .unwrap_or([0.0, 0.0, 0.0]); let range = dict - .get("Range") - .map(|r| r.try_array_of::(objects)) - .transpose()? + .optional_array_of::("Range", objects)? .unwrap_or([-100.0, 100.0, -100.0, 100.0]); Ok(ColorSpace::Lab(LabColorSpace { white_point, diff --git a/crates/pdf-document/src/encryption.rs b/crates/pdf-document/src/encryption.rs index baaa38d8..c868ead4 100644 --- a/crates/pdf-document/src/encryption.rs +++ b/crates/pdf-document/src/encryption.rs @@ -10,7 +10,9 @@ //! - The encryption dictionary contains parameters needed to decrypt the document. //! - Before reading other objects, the encryption dictionary must be resolved first. -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::error::PdfReaderError; @@ -148,32 +150,19 @@ impl EncryptDictionary { objects: &dyn ObjectResolver, ) -> Result { let filter = dict - .get_or_err("Filter")? - .try_str(objects) + .required_str("Filter", objects) .map(|obj| EncryptionFilter::from(obj.as_ref()))?; - let version_obj = dict.get_or_err("V")?; - - let version_num = version_obj.try_number::(objects)?; + let version_num = dict.required_number::("V", objects)?; let version = EncryptionVersion::try_from(version_num)?; - let revision = dict.get_or_err("R")?.try_number::(objects)?; - - let owner_password_hash = dict.get_or_err("O")?.try_bytes(objects)?.to_vec(); - - let user_password_hash = dict.get_or_err("U")?.try_bytes(objects)?.to_vec(); - - let permissions = dict.get_or_err("P")?.try_number::(objects)?; - - let key_length = dict - .get("Length") - .map(|l| l.try_number::(objects)) - .transpose()?; - + let revision = dict.required_number::("R", objects)?; + let owner_password_hash = dict.required_bytes("O", objects)?.to_vec(); + let user_password_hash = dict.required_bytes("U", objects)?.to_vec(); + let permissions = dict.required_number::("P", objects)?; + let key_length = dict.optional_number::("Length", objects)?; let encrypt_metadata = dict - .get("EncryptMetadata") - .map(|em| em.try_boolean(objects)) - .transpose()? + .optional_boolean("EncryptMetadata", objects)? .unwrap_or(Self::ENCRYPT_METADATA_DEFAULT); Ok(EncryptDictionary { diff --git a/crates/pdf-document/src/object_stream.rs b/crates/pdf-document/src/object_stream.rs index 332441f5..87b1d28e 100644 --- a/crates/pdf-document/src/object_stream.rs +++ b/crates/pdf-document/src/object_stream.rs @@ -1,5 +1,6 @@ use pdf_object::{ - object_resolver::ObjectResolver, object_variant::ObjectVariant, stream::StreamObject, + object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, object_variant::ObjectVariant, + stream::StreamObject, }; use pdf_parser::parser::PdfParser; @@ -22,10 +23,10 @@ pub fn read_object_stream( let dict = stream.dictionary.as_ref(); // /N: number of objects in this stream (required) - let n = dict.get_or_err("N")?.try_number::(objects)?; + let n = dict.required_number::("N", objects)?; // /First: byte offset of the first object data within the decoded stream (required) - let first = dict.get_or_err("First")?.try_number::(objects)?; + let first = dict.required_number::("First", objects)?; // Decode stream data (applies filters) let data = stream.data()?; diff --git a/crates/pdf-document/src/pages.rs b/crates/pdf-document/src/pages.rs index 59d1b196..281f3534 100644 --- a/crates/pdf-document/src/pages.rs +++ b/crates/pdf-document/src/pages.rs @@ -1,6 +1,8 @@ use crate::page::PdfPage; use pdf_content_stream::ContentStreamIdAllocator; -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use pdf_resources::{ error::PdfPagesError, media_box::MediaBox, @@ -29,7 +31,7 @@ impl ReadFromDictionary for PdfPages { // The `/Kids` array is a required entry in a Pages dictionary. It contains // indirect references to child objects, which can be either other Pages nodes // or leaf Page nodes. - let kids_array = dictionary.get_or_err("Kids")?.try_array(objects)?; + let kids_array = dictionary.required_array("Kids", objects)?; // This vector will store the flattened list of all leaf `PdfPage` objects // found by traversing the page tree. @@ -46,7 +48,7 @@ impl ReadFromDictionary for PdfPages { let dictionary = value.try_dictionary(objects)?; // Determine the type of the child object by reading its `/Type` entry. - match dictionary.get_or_err("Type")?.try_str(objects)?.as_ref() { + match dictionary.required_str("Type", objects)?.as_ref() { PdfPage::KEY => { // If the child is a leaf node (`/Type /Page`), parse it as a `PdfPage`. let page = PdfPage::from_dictionary( diff --git a/crates/pdf-document/src/reader.rs b/crates/pdf-document/src/reader.rs index 090cccd1..b247f482 100644 --- a/crates/pdf-document/src/reader.rs +++ b/crates/pdf-document/src/reader.rs @@ -8,6 +8,7 @@ use crate::page::PdfPage; use crate::pages::PdfPages; use pdf_content_stream::ContentStreamIdAllocator; use pdf_object::indirect_object::IndirectObject; +use pdf_object::object_lookup::ObjectLookupExt; use pdf_object::object_resolver::{ObjectResolver, PassthroughResolver}; use pdf_object::{ cross_reference_table::{CrossReferenceEntry, CrossReferenceEntryType, CrossReferenceTable}, @@ -112,13 +113,10 @@ fn extract_page_tree( objects: &mut dyn ObjectResolver, ) -> Result, PdfReaderError> { // Get the document catalog via the /Root entry in the trailer - let catalog = trailer - .dictionary - .get_or_err("Root")? - .try_dictionary(objects)?; + let catalog = trailer.dictionary.required_dictionary("Root", objects)?; // Get the page tree via the /Pages entry in the catalog - let pages_dict = catalog.get_or_err("Pages")?.try_dictionary(objects)?; + let pages_dict = catalog.required_dictionary("Pages", objects)?; let mut cache = DefaultResourceCache::default(); let mut cycle_tracker = ReadCycleTracker::default(); @@ -222,8 +220,7 @@ fn extract_document_id(trailer: &Trailer) -> Result, PdfReaderError> { // Get the first element of the /ID array let first_element = trailer .dictionary - .get_or_err("ID")? - .try_array(&PassthroughResolver)? + .required_array("ID", &PassthroughResolver)? .first() .ok_or(PdfReaderError::MissingDocumentId)?; diff --git a/crates/pdf-filter/src/filter.rs b/crates/pdf-filter/src/filter.rs index 88c35628..bc48f893 100644 --- a/crates/pdf-filter/src/filter.rs +++ b/crates/pdf-filter/src/filter.rs @@ -6,6 +6,7 @@ use crate::{error::FilterError, predictor::PredictorParams}; use pdf_ccitt::CCITTFaxParams; use pdf_object::{ dictionary::Dictionary, + object_lookup::ObjectLookupExt, object_resolver::{ObjectResolver, PassthroughResolver}, object_variant::ObjectVariant, stream::StreamObject, @@ -446,12 +447,7 @@ fn decode_parms_for_filter( } (Filter::CCITTFaxDecode, None) => DecodeParms::CcittFax(CCITTFaxParams::default()), (Filter::LZWDecode, Some(d)) => { - let early_change = d - .get("EarlyChange") - .map(|v| v.try_number::(objects)) - .transpose()? - .unwrap_or(1) - != 0; + let early_change = d.optional_number("EarlyChange", objects)?.unwrap_or(1) != 0; let predictor = PredictorParams::from_dictionary(d, objects)?; DecodeParms::Lzw { early_change, @@ -483,15 +479,8 @@ fn resolve_jbig2_dimensions( dict: &Dictionary, objects: &dyn ObjectResolver, ) -> Result<(u16, u16), FilterError> { - let width = dict - .get("Width") - .ok_or_else(|| FilterError::Decompression("JBIG2Decode requires Width".into()))? - .try_number::(objects)?; - let height = dict - .get("Height") - .ok_or_else(|| FilterError::Decompression("JBIG2Decode requires Height".into()))? - .try_number::(objects)?; - + let width = dict.required_number::("Width", objects)?; + let height = dict.required_number::("Height", objects)?; if width == 0 || height == 0 { return Err(FilterError::Decompression( "JBIG2Decode requires positive Width and Height".into(), @@ -505,11 +494,10 @@ fn resolve_jbig2_globals( dict: &Dictionary, objects: &dyn ObjectResolver, ) -> Result>, FilterError> { - let Some(globals_obj) = dict.get("JBIG2Globals") else { + let Some(globals_stream) = dict.optional_stream("JBIG2Globals", objects)? else { return Ok(None); }; - let globals_stream = globals_obj.try_stream(objects)?; Ok(Some(globals_stream.raw_data().to_vec())) } diff --git a/crates/pdf-filter/src/predictor.rs b/crates/pdf-filter/src/predictor.rs index 24b0c5ba..f3bbb61b 100644 --- a/crates/pdf-filter/src/predictor.rs +++ b/crates/pdf-filter/src/predictor.rs @@ -1,5 +1,7 @@ use crate::error::FilterError; -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; /// Parameters for the Predictor post-processing step shared by /// LZWDecode and FlateDecode (PDF spec §7.4.4.4, Table 10). @@ -36,17 +38,17 @@ impl PredictorParams { ) -> Result { let mut p = Self::default(); - if let Some(obj) = dict.get("Predictor") { - p.predictor = obj.try_number::(objects)?; + if let Some(value) = dict.optional_number::("Predictor", objects)? { + p.predictor = value; } - if let Some(obj) = dict.get("Colors") { - p.colors = obj.try_number::(objects)?; + if let Some(value) = dict.optional_number::("Colors", objects)? { + p.colors = value; } - if let Some(obj) = dict.get("BitsPerComponent") { - p.bits_per_component = obj.try_number::(objects)?; + if let Some(value) = dict.optional_number::("BitsPerComponent", objects)? { + p.bits_per_component = value; } - if let Some(obj) = dict.get("Columns") { - p.columns = obj.try_number::(objects)?; + if let Some(value) = dict.optional_number::("Columns", objects)? { + p.columns = value; } Ok(p) diff --git a/crates/pdf-font/src/cid_system_info.rs b/crates/pdf-font/src/cid_system_info.rs index 57f83d32..200206cb 100644 --- a/crates/pdf-font/src/cid_system_info.rs +++ b/crates/pdf-font/src/cid_system_info.rs @@ -1,5 +1,7 @@ use pdf_cmap::predefined::CidOrdering; -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::error::FontError; @@ -18,18 +20,11 @@ pub(crate) fn cid_ordering_from_dictionary( dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> Result, FontError> { - let Some(cid_system_info) = dictionary - .get("CIDSystemInfo") - .map(|obj| obj.try_dictionary(objects)) - .transpose()? - else { + let Some(cid_system_info) = dictionary.optional_dictionary("CIDSystemInfo", objects)? else { return Ok(None); }; - let Some(ordering) = cid_system_info - .get("Ordering") - .and_then(|obj| obj.try_str(objects).ok()) - else { + let Some(ordering) = cid_system_info.optional_str("Ordering", objects)? else { return Ok(None); }; diff --git a/crates/pdf-font/src/fallback.rs b/crates/pdf-font/src/fallback.rs index f374c471..f48c2ffc 100644 --- a/crates/pdf-font/src/fallback.rs +++ b/crates/pdf-font/src/fallback.rs @@ -2,7 +2,8 @@ use std::borrow::Cow; use pdf_cmap::ToUnicodeCMap; use pdf_object::{ - dictionary::Dictionary, object_resolver::ObjectResolver, object_variant::ObjectVariant, + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, + object_variant::ObjectVariant, }; use crate::{ @@ -147,11 +148,7 @@ pub(crate) fn descriptor_flags( dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> Result { - let Some(descriptor) = dictionary - .get("FontDescriptor") - .map(|obj| obj.try_dictionary(objects)) - .transpose()? - else { + let Some(descriptor) = dictionary.optional_dictionary("FontDescriptor", objects)? else { return Ok(FontFlags::empty()); }; diff --git a/crates/pdf-font/src/font.rs b/crates/pdf-font/src/font.rs index 7a0e8d5a..784af6f0 100644 --- a/crates/pdf-font/src/font.rs +++ b/crates/pdf-font/src/font.rs @@ -2,7 +2,9 @@ use std::borrow::Cow; use pdf_cmap::ToUnicodeCMap; use pdf_content_stream::ContentStreamIdAllocator; -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::{ char_vec::CharVec, @@ -39,8 +41,7 @@ impl Font { id_allocator: &mut ContentStreamIdAllocator, ) -> Result { // Determine the font subtype from the dictionary. - let subtype = dictionary.get_or_err("Subtype")?.try_str(objects)?; - + let subtype = dictionary.required_str("Subtype", objects)?; match subtype.as_ref() { "Type0" => { let type0_font = Type0Font::from_dictionary(dictionary, objects)?; diff --git a/crates/pdf-font/src/simple_font_glyph_map.rs b/crates/pdf-font/src/simple_font_glyph_map.rs index 96ac8c0d..2f8d74a0 100644 --- a/crates/pdf-font/src/simple_font_glyph_map.rs +++ b/crates/pdf-font/src/simple_font_glyph_map.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use crate::error::FontError; @@ -26,13 +28,8 @@ impl SimpleFontGlyphWidthsMap { return Ok(None); }; - let first_char = dictionary - .get_or_err("FirstChar")? - .try_number::(objects)?; - - let last_char = dictionary - .get_or_err("LastChar")? - .try_number::(objects)?; + let first_char = dictionary.required_number::("FirstChar", objects)?; + let last_char = dictionary.required_number::("LastChar", objects)?; // Validate: FirstChar must not exceed LastChar. if first_char > last_char { diff --git a/crates/pdf-font/src/true_type_font.rs b/crates/pdf-font/src/true_type_font.rs index 067eae3a..7881a8fa 100644 --- a/crates/pdf-font/src/true_type_font.rs +++ b/crates/pdf-font/src/true_type_font.rs @@ -2,7 +2,8 @@ use std::{borrow::Cow, collections::HashMap}; use pdf_cmap::ToUnicodeCMap; use pdf_object::{ - dictionary::Dictionary, object_resolver::ObjectResolver, object_variant::ObjectVariant, + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, + object_variant::ObjectVariant, }; use crate::{ @@ -154,22 +155,14 @@ impl TrueTypeFont { dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> Result { - if let Some(descriptor) = dictionary - .get("FontDescriptor") - .map(|obj| obj.try_dictionary(objects)) - .transpose()? - { + if let Some(descriptor) = dictionary.optional_dictionary("FontDescriptor", objects)? { let flags = descriptor .get("Flags") .and_then(|obj| obj.try_number::(objects).ok()) .map(FontFlags::from_bits_truncate) .unwrap_or_default(); - if let Some(stream) = descriptor - .get("FontFile2") - .map(|obj| obj.try_stream(objects)) - .transpose()? - { + if let Some(stream) = descriptor.optional_stream("FontFile2", objects)? { return Ok(TrueTypeFontProgram { font_file: Cow::Owned(stream.data()?.to_vec()), standard14: None, diff --git a/crates/pdf-font/src/type0_font.rs b/crates/pdf-font/src/type0_font.rs index 35cf534b..3822dd37 100644 --- a/crates/pdf-font/src/type0_font.rs +++ b/crates/pdf-font/src/type0_font.rs @@ -2,7 +2,8 @@ use std::collections::HashMap; use pdf_cmap::{ToUnicodeCMap, Type0EncodingCMap}; use pdf_object::{ - dictionary::Dictionary, object_resolver::ObjectResolver, object_variant::ObjectVariant, + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, + object_variant::ObjectVariant, }; use read_fonts::{FontRef, TableProvider}; @@ -253,9 +254,7 @@ fn descendant_font_dictionary<'a>( dictionary: &'a Dictionary, objects: &'a dyn ObjectResolver, ) -> Result<&'a Dictionary, FontError> { - let descendant_fonts = dictionary - .get_or_err("DescendantFonts")? - .try_array(objects)?; + let descendant_fonts = dictionary.required_array("DescendantFonts", objects)?; if descendant_fonts.len() != 1 { return Err(FontError::InvalidDescendantFonts( "Expected exactly one descendant font", @@ -284,7 +283,7 @@ fn cid_font_subtype( dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> Result { - match dictionary.get_or_err("Subtype")?.try_str(objects)?.as_ref() { + match dictionary.required_str("Subtype", objects)?.as_ref() { "CIDFontType0" => Ok(CidFontSubType::Type0), "CIDFontType2" => Ok(CidFontSubType::Type2), other => Err(FontError::UnsupportedCidFontSubtype { diff --git a/crates/pdf-font/src/type1_font.rs b/crates/pdf-font/src/type1_font.rs index 2d3ab979..77d08ee3 100644 --- a/crates/pdf-font/src/type1_font.rs +++ b/crates/pdf-font/src/type1_font.rs @@ -2,7 +2,8 @@ use std::collections::HashMap; use pdf_cmap::ToUnicodeCMap; use pdf_object::{ - dictionary::Dictionary, object_resolver::ObjectResolver, object_variant::ObjectVariant, + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, + object_variant::ObjectVariant, }; use crate::{ @@ -109,23 +110,12 @@ impl Type1Font { dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> Result<(Vec, Type1FontProgramFormat), FontError> { - let descriptor = dictionary - .get("FontDescriptor") - .map(|obj| obj.try_dictionary(objects)) - .transpose()?; - - let Some(descriptor) = descriptor else { - return Err(FontError::MissingFontFile); - }; + let descriptor = dictionary.required_dictionary("FontDescriptor", objects)?; // Path 1: FontFile3 stream with subtype-driven handling. if let Some(font_file3) = descriptor.get("FontFile3") { let stream = font_file3.try_stream(objects)?; - let subtype = stream - .dictionary - .get("Subtype") - .map(|obj| obj.try_str(objects)) - .transpose()?; + let subtype = stream.dictionary.optional_str("Subtype", objects)?; let data = stream.data()?; @@ -178,18 +168,9 @@ fn trim_classic_type1_by_lengths( data: &[u8], objects: &dyn ObjectResolver, ) -> Result>, FontError> { - let length1 = descriptor - .get("Length1") - .map(|obj| obj.try_number::(objects)) - .transpose()?; - let length2 = descriptor - .get("Length2") - .map(|obj| obj.try_number::(objects)) - .transpose()?; - let length3 = descriptor - .get("Length3") - .map(|obj| obj.try_number::(objects)) - .transpose()?; + let length1 = descriptor.optional_number::("Length1", objects)?; + let length2 = descriptor.optional_number::("Length2", objects)?; + let length3 = descriptor.optional_number::("Length3", objects)?; let Some(total_length) = length1 .zip(length2) diff --git a/crates/pdf-font/src/type3_font.rs b/crates/pdf-font/src/type3_font.rs index 576902d5..aa96f378 100644 --- a/crates/pdf-font/src/type3_font.rs +++ b/crates/pdf-font/src/type3_font.rs @@ -4,7 +4,8 @@ use pdf_cmap::ToUnicodeCMap; use pdf_content_stream::{ContentStream, ContentStreamIdAllocator}; use pdf_graphics::{rect::Rect, transform::Transform}; use pdf_object::{ - dictionary::Dictionary, object_resolver::ObjectResolver, object_variant::ObjectVariant, + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, + object_variant::ObjectVariant, }; use crate::{ @@ -39,14 +40,11 @@ impl Type3Font { objects: &dyn ObjectResolver, id_allocator: &mut ContentStreamIdAllocator, ) -> Result { - let [a, b, c, d, e, f] = dictionary - .get_or_err("FontMatrix")? - .try_array_of::(objects)?; + let [a, b, c, d, e, f] = dictionary.required_array_of::("FontMatrix", objects)?; let font_matrix = Transform::from_row(a, b, c, d, e, f); - let [left, top, right, bottom] = dictionary - .get_or_err("FontBBox")? - .try_array_of::(objects)?; + let [left, top, right, bottom] = + dictionary.required_array_of::("FontBBox", objects)?; let bounds = Rect { left, top, @@ -65,9 +63,7 @@ impl Type3Font { }) .transpose()?; - let char_proc_dictionary = dictionary - .get_or_err("CharProcs")? - .try_dictionary(objects)?; + let char_proc_dictionary = dictionary.required_dictionary("CharProcs", objects)?; let mut char_procs = HashMap::new(); for (name, value) in char_proc_dictionary.dictionary.iter() { diff --git a/crates/pdf-function/src/exponential_interpolation.rs b/crates/pdf-function/src/exponential_interpolation.rs index 6aaa33d8..9e6f6970 100644 --- a/crates/pdf-function/src/exponential_interpolation.rs +++ b/crates/pdf-function/src/exponential_interpolation.rs @@ -1,4 +1,6 @@ -use pdf_object::{object_resolver::ObjectResolver, object_variant::ObjectVariant}; +use pdf_object::{ + object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, object_variant::ObjectVariant, +}; use crate::{ error::FunctionReadError, @@ -86,22 +88,16 @@ impl FunctionImpl for ExponentialFunction { objects: &dyn ObjectResolver, ) -> Result { let dictionary = object.try_dictionary(objects)?; - let domain = dictionary - .get_or_err("Domain")? - .try_array_of::(objects)?; + let domain = dictionary.required_array_of::("Domain", objects)?; // /C0: Output values at domain[0]. Defaults to [0.0]. let c0 = dictionary - .get("C0") - .map(|o| o.try_vec_of::(objects)) - .transpose()? + .optional_vec_of::("C0", objects)? .unwrap_or_else(|| vec![0.0]); // /C1: Output values at domain[1]. Defaults to [1.0]. let c1 = dictionary - .get("C1") - .map(|o| o.try_vec_of::(objects)) - .transpose()? + .optional_vec_of::("C1", objects)? .unwrap_or_else(|| vec![1.0]); // Validate C0/C1 length match at parse time. @@ -110,13 +106,10 @@ impl FunctionImpl for ExponentialFunction { } // /N: Interpolation exponent (required). - let exponent = dictionary.get_or_err("N")?.try_number::(objects)?; + let exponent = dictionary.required_number::("N", objects)?; // /Range: Optional. Output range for clamping (ISO 32000 §7.10.3). - let range = dictionary - .get("Range") - .map(|o| o.try_vec_of::(objects)) - .transpose()?; + let range = dictionary.optional_vec_of::("Range", objects)?; Ok(Function::Exponential(ExponentialFunction { c0, diff --git a/crates/pdf-function/src/function.rs b/crates/pdf-function/src/function.rs index c35ea645..fa54e30f 100644 --- a/crates/pdf-function/src/function.rs +++ b/crates/pdf-function/src/function.rs @@ -6,7 +6,9 @@ //! - Type 3: Stitching functions (combining multiple functions) //! - Type 4: PostScript Calculator functions -use pdf_object::{object_resolver::ObjectResolver, object_variant::ObjectVariant}; +use pdf_object::{ + object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, object_variant::ObjectVariant, +}; use crate::{ error::FunctionReadError, exponential_interpolation::ExponentialFunction, @@ -122,8 +124,7 @@ impl FunctionImpl for Function { ) -> Result { let function_type = object .try_dictionary(objects)? - .get_or_err("FunctionType")? - .try_number::(objects) + .required_number::("FunctionType", objects) .map(FunctionType::from_i32)? .ok_or(FunctionReadError::InvalidFunctionType)?; diff --git a/crates/pdf-function/src/postscript_calculator.rs b/crates/pdf-function/src/postscript_calculator.rs index da5deabd..633636b9 100644 --- a/crates/pdf-function/src/postscript_calculator.rs +++ b/crates/pdf-function/src/postscript_calculator.rs @@ -1,5 +1,7 @@ use num_traits::ToPrimitive; -use pdf_object::{object_resolver::ObjectResolver, object_variant::ObjectVariant}; +use pdf_object::{ + object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, object_variant::ObjectVariant, +}; use pdf_postscript::{operator::Operator, value::Value}; use crate::{ @@ -119,13 +121,9 @@ impl FunctionImpl for PostScriptCalculatorFunction { let stream = object.try_stream(objects)?; let domain = stream .dictionary - .get_or_err("Domain")? - .try_vec_of::(objects)?; + .required_vec_of::("Domain", objects)?; - let range = stream - .dictionary - .get_or_err("Range")? - .try_vec_of::(objects)?; + let range = stream.dictionary.required_vec_of::("Range", objects)?; // Parse PostScript code: add spaces around braces for tokenization let stream_data = stream.data()?; diff --git a/crates/pdf-function/src/sampled.rs b/crates/pdf-function/src/sampled.rs index 1f8960ca..9acb40c0 100644 --- a/crates/pdf-function/src/sampled.rs +++ b/crates/pdf-function/src/sampled.rs @@ -1,7 +1,9 @@ use num_derive::FromPrimitive; use num_traits::{FromPrimitive, ToPrimitive}; use pdf_decode::decode_normalized_samples; -use pdf_object::{object_resolver::ObjectResolver, object_variant::ObjectVariant}; +use pdf_object::{ + object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, object_variant::ObjectVariant, +}; use crate::{ error::FunctionReadError, @@ -232,17 +234,13 @@ impl FunctionImpl for SampledFunction { let dictionary = &stream.dictionary; // /Domain: Required. Array of 2*m numbers defining input domain. - let domain = dictionary - .get_or_err("Domain")? - .try_vec_of::(objects)?; + let domain = dictionary.required_vec_of::("Domain", objects)?; // /Range: Required for sampled functions. Array of 2*n numbers. - let range = dictionary.get_or_err("Range")?.try_vec_of::(objects)?; + let range = dictionary.required_vec_of::("Range", objects)?; // /Size: Required. Array of m integers specifying samples per input dimension. - let size = dictionary - .get_or_err("Size")? - .try_vec_of::(objects)?; + let size = dictionary.required_vec_of::("Size", objects)?; if size.is_empty() { return Err(FunctionReadError::InvalidSizeArray); } @@ -250,9 +248,7 @@ impl FunctionImpl for SampledFunction { let output_count = range.len() / 2; // /BitsPerSample: Required. Must be 1, 2, 4, 8, 12, 16, 24, or 32. - let bits_per_sample = dictionary - .get_or_err("BitsPerSample")? - .try_number::(objects)?; + let bits_per_sample = dictionary.required_number::("BitsPerSample", objects)?; if !matches!(bits_per_sample, 1 | 2 | 4 | 8 | 12 | 16 | 24 | 32) { return Err(FunctionReadError::InvalidBitsPerSample); } diff --git a/crates/pdf-function/src/stitching.rs b/crates/pdf-function/src/stitching.rs index d999a55e..49819f59 100644 --- a/crates/pdf-function/src/stitching.rs +++ b/crates/pdf-function/src/stitching.rs @@ -1,6 +1,8 @@ use std::cmp::Ordering; -use pdf_object::{object_resolver::ObjectResolver, object_variant::ObjectVariant}; +use pdf_object::{ + object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, object_variant::ObjectVariant, +}; use crate::{ error::FunctionReadError, @@ -108,26 +110,20 @@ impl FunctionImpl for StitchingFunction { ) -> Result { let dictionary = object.try_dictionary(objects)?; - let domain = dictionary - .get_or_err("Domain")? - .try_array_of::(objects)?; + let domain = dictionary.required_array_of::("Domain", objects)?; // Parse /Functions array (sub-functions to stitch together) - let functions_arr = dictionary.get_or_err("Functions")?.try_array(objects)?; + let functions_arr = dictionary.required_array("Functions", objects)?; let functions = functions_arr .iter() .map(|obj| Function::parse(obj, objects)) .collect::, _>>()?; // Parse /Bounds array (boundaries between sub-functions) - let bounds = dictionary - .get_or_err("Bounds")? - .try_vec_of::(objects)?; + let bounds = dictionary.required_vec_of::("Bounds", objects)?; // Parse /Encode array (input mapping for each sub-function) - let encode = dictionary - .get_or_err("Encode")? - .try_vec_of::(objects)?; + let encode = dictionary.required_vec_of::("Encode", objects)?; // Validate structural relationships let expected_bounds = functions diff --git a/crates/pdf-image/src/image_xobject.rs b/crates/pdf-image/src/image_xobject.rs index 8896ca2b..5250cff4 100644 --- a/crates/pdf-image/src/image_xobject.rs +++ b/crates/pdf-image/src/image_xobject.rs @@ -2,7 +2,10 @@ use pdf_color_space::{color_space::ColorSpace, indexed_color_space::IndexedColor use pdf_decode::{DecodeMap, SampleLayout, decode_sample_codes}; use pdf_filter::filter::{Filter, decode_with_resolver}; use pdf_graphics::PixelFormat; -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver, stream::StreamObject}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, + stream::StreamObject, +}; use crate::InlineImage; use crate::error::PdfImageError; @@ -113,35 +116,29 @@ impl ImageXObject { dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> Result { - let width = dictionary - .get_or_err("Width")? - .try_number::(objects)?; - let height = dictionary - .get_or_err("Height")? - .try_number::(objects)?; + let width = dictionary.required_number::("Width", objects)?; + let height = dictionary.required_number::("Height", objects)?; if width == 0 || height == 0 { return Err(PdfImageError::InvalidImageDimensions { width, height }); } let image_mask = dictionary - .get("ImageMask") - .map_or(Ok(false), |value| value.try_boolean(objects))?; + .optional_boolean("ImageMask", objects)? + .unwrap_or(false); let (bits_per_component, color_space) = if image_mask { let bits_per_component = dictionary - .get("BitsPerComponent") - .map_or(Ok(1), |value| value.try_number::(objects))?; + .optional_number::("BitsPerComponent", objects)? + .unwrap_or(1); Self::validate_bits_per_component(bits_per_component, image_mask, None)?; (bits_per_component, None) } else { let bits_per_component = if Self::has_jpx_filter(dictionary, objects)? { dictionary - .get("BitsPerComponent") - .map_or(Ok(8), |value| value.try_number::(objects))? + .optional_number::("BitsPerComponent", objects)? + .unwrap_or(8) } else { - dictionary - .get_or_err("BitsPerComponent")? - .try_number::(objects)? + dictionary.required_number::("BitsPerComponent", objects)? }; let color_space = ColorSpace::from_dictionary(dictionary, objects)?; Self::validate_bits_per_component( diff --git a/crates/pdf-object/src/lib.rs b/crates/pdf-object/src/lib.rs index 161c8f18..fb5740bd 100644 --- a/crates/pdf-object/src/lib.rs +++ b/crates/pdf-object/src/lib.rs @@ -2,6 +2,7 @@ pub mod cross_reference_table; pub mod dictionary; pub mod error; pub mod indirect_object; +pub mod object_lookup; pub mod object_resolver; pub mod object_variant; pub mod stream; diff --git a/crates/pdf-object/src/object_lookup.rs b/crates/pdf-object/src/object_lookup.rs new file mode 100644 index 00000000..2bb17baa --- /dev/null +++ b/crates/pdf-object/src/object_lookup.rs @@ -0,0 +1,1276 @@ +use std::borrow::Cow; + +use num_traits::FromPrimitive; + +use crate::{ + dictionary::Dictionary, error::ObjectError, object_resolver::ObjectResolver, + object_variant::ObjectVariant, stream::StreamObject, +}; + +/// Provides typed lookup helpers for PDF object containers. +/// +/// Implementations support dictionary key lookup and array index lookup while +/// delegating object conversion to [`ObjectVariant`] conversion methods. +/// +/// # Type Parameters +/// +/// - `K`: The lookup key type used by the container. Dictionaries use `&str` +/// keys and arrays use `usize` indexes. +pub trait ObjectLookupExt { + /// Returns an optional dictionary value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_dictionary`]. Missing entries and explicit PDF + /// `null` values are treated as absent. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(Some(&Dictionary))` when the value exists and converts + /// successfully, `Ok(None)` when the value is missing or resolves to + /// `null`, or `Err` if reference resolution or conversion fails. + fn optional_dictionary<'a>( + &'a self, + key: K, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError>; + + /// Returns a required dictionary value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_dictionary`]. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(&Dictionary)` when the value exists and converts + /// successfully, or `Err` if the value is missing, reference resolution + /// fails, or conversion fails. + fn required_dictionary<'a>( + &'a self, + key: K, + objects: &'a dyn ObjectResolver, + ) -> Result<&'a Dictionary, ObjectError>; + + /// Returns an optional stream value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_stream`]. Missing entries and explicit PDF `null` + /// values are treated as absent. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(Some(&StreamObject))` when the value exists and converts + /// successfully, `Ok(None)` when the value is missing or resolves to + /// `null`, or `Err` if reference resolution or conversion fails. + fn optional_stream<'a>( + &'a self, + key: K, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError>; + + /// Returns a required stream value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_stream`]. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(&StreamObject)` when the value exists and converts + /// successfully, or `Err` if the value is missing, reference resolution + /// fails, or conversion fails. + fn required_stream<'a>( + &'a self, + key: K, + objects: &'a dyn ObjectResolver, + ) -> Result<&'a StreamObject, ObjectError>; + + /// Returns an optional array value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_array`]. Missing entries and explicit PDF `null` + /// values are treated as absent. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(Some(&[ObjectVariant]))` when the value exists and converts + /// successfully, `Ok(None)` when the value is missing or resolves to + /// `null`, or `Err` if reference resolution or conversion fails. + fn optional_array<'a>( + &'a self, + key: K, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError>; + + /// Returns a required array value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_array`]. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(&[ObjectVariant])` when the value exists and converts + /// successfully, or `Err` if the value is missing, reference resolution + /// fails, or conversion fails. + fn required_array<'a>( + &'a self, + key: K, + objects: &'a dyn ObjectResolver, + ) -> Result<&'a [ObjectVariant], ObjectError>; + + /// Returns an optional string-like value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_str`]. Missing entries and explicit PDF `null` + /// values are treated as absent. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(Some(Cow))` when the value exists and converts + /// successfully, `Ok(None)` when the value is missing or resolves to + /// `null`, or `Err` if reference resolution or conversion fails. + fn optional_str<'a>( + &'a self, + key: K, + objects: &'a dyn ObjectResolver, + ) -> Result>, ObjectError>; + + /// Returns a required string-like value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_str`]. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(Cow)` when the value exists and converts successfully, + /// or `Err` if the value is missing, reference resolution fails, or + /// conversion fails. + fn required_str<'a>( + &'a self, + key: K, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError>; + + /// Returns an optional byte string value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_bytes`]. Missing entries and explicit PDF `null` + /// values are treated as absent. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(Some(&[u8]))` when the value exists and converts + /// successfully, `Ok(None)` when the value is missing or resolves to + /// `null`, or `Err` if reference resolution or conversion fails. + fn optional_bytes<'a>( + &'a self, + key: K, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError>; + + /// Returns a required byte string value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_bytes`]. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(&[u8])` when the value exists and converts successfully, or + /// `Err` if the value is missing, reference resolution fails, or conversion + /// fails. + fn required_bytes<'a>( + &'a self, + key: K, + objects: &'a dyn ObjectResolver, + ) -> Result<&'a [u8], ObjectError>; + + /// Returns an optional owned byte string value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_bytes_vec`]. Missing entries and explicit PDF + /// `null` values are treated as absent. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(Some(Vec))` when the value exists and converts + /// successfully, `Ok(None)` when the value is missing or resolves to + /// `null`, or `Err` if reference resolution or conversion fails. + fn optional_bytes_vec<'a>( + &'a self, + key: K, + objects: &'a dyn ObjectResolver, + ) -> Result>, ObjectError>; + + /// Returns a required owned byte string value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_bytes_vec`]. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(Vec)` when the value exists and converts successfully, + /// or `Err` if the value is missing, reference resolution fails, or + /// conversion fails. + fn required_bytes_vec<'a>( + &'a self, + key: K, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError>; + + /// Returns an optional numeric value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_number`]. Missing entries and explicit PDF `null` + /// values are treated as absent. + /// + /// # Type Parameters + /// + /// - `T`: The numeric type to convert the object to. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(Some(T))` when the value exists and converts successfully, + /// `Ok(None)` when the value is missing or resolves to `null`, or `Err` if + /// reference resolution or numeric conversion fails. + fn optional_number( + &self, + key: K, + objects: &dyn ObjectResolver, + ) -> Result, ObjectError> + where + T: FromPrimitive; + + /// Returns a required numeric value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_number`]. + /// + /// # Type Parameters + /// + /// - `T`: The numeric type to convert the object to. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(T)` when the value exists and converts successfully, or + /// `Err` if the value is missing, reference resolution fails, or numeric + /// conversion fails. + fn required_number(&self, key: K, objects: &dyn ObjectResolver) -> Result + where + T: FromPrimitive; + + /// Returns an optional numeric vector value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_vec_of`]. Missing entries and explicit PDF `null` + /// values are treated as absent. + /// + /// # Type Parameters + /// + /// - `T`: The numeric type to convert each array item to. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(Some(Vec))` when the value exists and converts + /// successfully, `Ok(None)` when the value is missing or resolves to + /// `null`, or `Err` if reference resolution or conversion fails. + fn optional_vec_of( + &self, + key: K, + objects: &dyn ObjectResolver, + ) -> Result>, ObjectError> + where + T: FromPrimitive + Copy + Default; + + /// Returns a required numeric vector value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_vec_of`]. + /// + /// # Type Parameters + /// + /// - `T`: The numeric type to convert each array item to. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(Vec)` when the value exists and converts successfully, + /// or `Err` if the value is missing, reference resolution fails, or + /// conversion fails. + fn required_vec_of( + &self, + key: K, + objects: &dyn ObjectResolver, + ) -> Result, ObjectError> + where + T: FromPrimitive + Copy + Default; + + /// Returns an optional fixed-size numeric array value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_array_of`]. Missing entries and explicit PDF `null` + /// values are treated as absent. + /// + /// # Type Parameters + /// + /// - `T`: The numeric type to convert each array item to. + /// - `N`: The expected array length. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(Some([T; N]))` when the value exists and converts + /// successfully, `Ok(None)` when the value is missing or resolves to + /// `null`, or `Err` if reference resolution or conversion fails. + fn optional_array_of( + &self, + key: K, + objects: &dyn ObjectResolver, + ) -> Result, ObjectError> + where + T: FromPrimitive + Copy + Default; + + /// Returns a required fixed-size numeric array value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_array_of`]. + /// + /// # Type Parameters + /// + /// - `T`: The numeric type to convert each array item to. + /// - `N`: The expected array length. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok([T; N])` when the value exists and converts successfully, + /// or `Err` if the value is missing, reference resolution fails, or + /// conversion fails. + fn required_array_of( + &self, + key: K, + objects: &dyn ObjectResolver, + ) -> Result<[T; N], ObjectError> + where + T: FromPrimitive + Copy + Default; + + /// Returns an optional boolean value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_boolean`]. Missing entries and explicit PDF `null` + /// values are treated as absent. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(Some(bool))` when the value exists and converts + /// successfully, `Ok(None)` when the value is missing or resolves to + /// `null`, or `Err` if reference resolution or conversion fails. + fn optional_boolean( + &self, + key: K, + objects: &dyn ObjectResolver, + ) -> Result, ObjectError>; + + /// Returns a required boolean value from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_boolean`]. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// - `objects`: The object resolver used when the looked-up value is an + /// indirect reference. + /// + /// # Returns + /// + /// Returns `Ok(bool)` when the value exists and converts successfully, or + /// `Err` if the value is missing, reference resolution fails, or conversion + /// fails. + fn required_boolean(&self, key: K, objects: &dyn ObjectResolver) -> Result; + + /// Returns an optional object number from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_object_number`]. Missing entries and explicit PDF + /// `null` values are treated as absent. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// + /// # Returns + /// + /// Returns `Ok(Some(usize))` when the value exists and has an object + /// number, `Ok(None)` when the value is missing or is `null`, or `Err` if + /// conversion fails. + fn optional_object_number(&self, key: K) -> Result, ObjectError>; + + /// Returns a required object number from this container. + /// + /// This method looks up a value by key or index and converts it using + /// [`ObjectVariant::try_object_number`]. + /// + /// # Parameters + /// + /// - `key`: The dictionary key or array index to look up. + /// + /// # Returns + /// + /// Returns `Ok(usize)` when the value exists and has an object number, or + /// `Err` if the value is missing or conversion fails. + fn required_object_number(&self, key: K) -> Result; +} + +impl ObjectLookupExt for [ObjectVariant] { + fn optional_dictionary<'a>( + &'a self, + index: usize, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError> { + optional_resolved_value(self.get(index), objects)? + .map(|value| value.try_dictionary(objects)) + .transpose() + } + + fn required_dictionary<'a>( + &'a self, + index: usize, + objects: &'a dyn ObjectResolver, + ) -> Result<&'a Dictionary, ObjectError> { + required_slice_value(self, index)?.try_dictionary(objects) + } + + fn optional_stream<'a>( + &'a self, + index: usize, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError> { + optional_resolved_value(self.get(index), objects)? + .map(|value| value.try_stream(objects)) + .transpose() + } + + fn required_stream<'a>( + &'a self, + index: usize, + objects: &'a dyn ObjectResolver, + ) -> Result<&'a StreamObject, ObjectError> { + required_slice_value(self, index)?.try_stream(objects) + } + + fn optional_array<'a>( + &'a self, + index: usize, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError> { + optional_resolved_value(self.get(index), objects)? + .map(|value| value.try_array(objects)) + .transpose() + } + + fn required_array<'a>( + &'a self, + index: usize, + objects: &'a dyn ObjectResolver, + ) -> Result<&'a [ObjectVariant], ObjectError> { + required_slice_value(self, index)?.try_array(objects) + } + + fn optional_str<'a>( + &'a self, + index: usize, + objects: &'a dyn ObjectResolver, + ) -> Result>, ObjectError> { + optional_resolved_value(self.get(index), objects)? + .map(|value| value.try_str(objects)) + .transpose() + } + + fn required_str<'a>( + &'a self, + index: usize, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError> { + required_slice_value(self, index)?.try_str(objects) + } + + fn optional_bytes<'a>( + &'a self, + index: usize, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError> { + optional_resolved_value(self.get(index), objects)? + .map(|value| value.try_bytes(objects)) + .transpose() + } + + fn required_bytes<'a>( + &'a self, + index: usize, + objects: &'a dyn ObjectResolver, + ) -> Result<&'a [u8], ObjectError> { + required_slice_value(self, index)?.try_bytes(objects) + } + + fn optional_bytes_vec<'a>( + &'a self, + index: usize, + objects: &'a dyn ObjectResolver, + ) -> Result>, ObjectError> { + optional_resolved_value(self.get(index), objects)? + .map(|value| value.try_bytes_vec(objects)) + .transpose() + } + + fn required_bytes_vec<'a>( + &'a self, + index: usize, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError> { + required_slice_value(self, index)?.try_bytes_vec(objects) + } + + fn optional_number( + &self, + index: usize, + objects: &dyn ObjectResolver, + ) -> Result, ObjectError> + where + T: FromPrimitive, + { + optional_resolved_value(self.get(index), objects)? + .map(|value| value.try_number(objects)) + .transpose() + } + + fn required_number( + &self, + index: usize, + objects: &dyn ObjectResolver, + ) -> Result + where + T: FromPrimitive, + { + required_slice_value(self, index)?.try_number(objects) + } + + fn optional_vec_of( + &self, + index: usize, + objects: &dyn ObjectResolver, + ) -> Result>, ObjectError> + where + T: FromPrimitive + Copy + Default, + { + optional_resolved_value(self.get(index), objects)? + .map(|value| value.try_vec_of(objects)) + .transpose() + } + + fn required_vec_of( + &self, + index: usize, + objects: &dyn ObjectResolver, + ) -> Result, ObjectError> + where + T: FromPrimitive + Copy + Default, + { + required_slice_value(self, index)?.try_vec_of(objects) + } + + fn optional_array_of( + &self, + index: usize, + objects: &dyn ObjectResolver, + ) -> Result, ObjectError> + where + T: FromPrimitive + Copy + Default, + { + optional_resolved_value(self.get(index), objects)? + .map(|value| value.try_array_of(objects)) + .transpose() + } + + fn required_array_of( + &self, + index: usize, + objects: &dyn ObjectResolver, + ) -> Result<[T; N], ObjectError> + where + T: FromPrimitive + Copy + Default, + { + required_slice_value(self, index)?.try_array_of(objects) + } + + fn optional_boolean( + &self, + index: usize, + objects: &dyn ObjectResolver, + ) -> Result, ObjectError> { + optional_resolved_value(self.get(index), objects)? + .map(|value| value.try_boolean(objects)) + .transpose() + } + + fn required_boolean( + &self, + index: usize, + objects: &dyn ObjectResolver, + ) -> Result { + required_slice_value(self, index)?.try_boolean(objects) + } + + fn optional_object_number(&self, index: usize) -> Result, ObjectError> { + optional_direct_value(self.get(index))? + .map(ObjectVariant::try_object_number) + .transpose() + } + + fn required_object_number(&self, index: usize) -> Result { + required_slice_value(self, index)?.try_object_number() + } +} + +impl ObjectLookupExt<&str> for Dictionary { + fn optional_dictionary<'a>( + &'a self, + key: &str, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError> { + optional_resolved_value(self.get(key), objects)? + .map(|value| value.try_dictionary(objects)) + .transpose() + } + + fn required_dictionary<'a>( + &'a self, + key: &str, + objects: &'a dyn ObjectResolver, + ) -> Result<&'a Dictionary, ObjectError> { + self.get_or_err(key)?.try_dictionary(objects) + } + + fn optional_stream<'a>( + &'a self, + key: &str, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError> { + optional_resolved_value(self.get(key), objects)? + .map(|value| value.try_stream(objects)) + .transpose() + } + + fn required_stream<'a>( + &'a self, + key: &str, + objects: &'a dyn ObjectResolver, + ) -> Result<&'a StreamObject, ObjectError> { + self.get_or_err(key)?.try_stream(objects) + } + + fn optional_array<'a>( + &'a self, + key: &str, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError> { + optional_resolved_value(self.get(key), objects)? + .map(|value| value.try_array(objects)) + .transpose() + } + + fn required_array<'a>( + &'a self, + key: &str, + objects: &'a dyn ObjectResolver, + ) -> Result<&'a [ObjectVariant], ObjectError> { + self.get_or_err(key)?.try_array(objects) + } + + fn optional_str<'a>( + &'a self, + key: &str, + objects: &'a dyn ObjectResolver, + ) -> Result>, ObjectError> { + optional_resolved_value(self.get(key), objects)? + .map(|value| value.try_str(objects)) + .transpose() + } + + fn required_str<'a>( + &'a self, + key: &str, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError> { + self.get_or_err(key)?.try_str(objects) + } + + fn optional_bytes<'a>( + &'a self, + key: &str, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError> { + optional_resolved_value(self.get(key), objects)? + .map(|value| value.try_bytes(objects)) + .transpose() + } + + fn required_bytes<'a>( + &'a self, + key: &str, + objects: &'a dyn ObjectResolver, + ) -> Result<&'a [u8], ObjectError> { + self.get_or_err(key)?.try_bytes(objects) + } + + fn optional_bytes_vec<'a>( + &'a self, + key: &str, + objects: &'a dyn ObjectResolver, + ) -> Result>, ObjectError> { + optional_resolved_value(self.get(key), objects)? + .map(|value| value.try_bytes_vec(objects)) + .transpose() + } + + fn required_bytes_vec<'a>( + &'a self, + key: &str, + objects: &'a dyn ObjectResolver, + ) -> Result, ObjectError> { + self.get_or_err(key)?.try_bytes_vec(objects) + } + + fn optional_number( + &self, + key: &str, + objects: &dyn ObjectResolver, + ) -> Result, ObjectError> + where + T: FromPrimitive, + { + optional_resolved_value(self.get(key), objects)? + .map(|value| value.try_number(objects)) + .transpose() + } + + fn required_number(&self, key: &str, objects: &dyn ObjectResolver) -> Result + where + T: FromPrimitive, + { + self.get_or_err(key)?.try_number(objects) + } + + fn optional_vec_of( + &self, + key: &str, + objects: &dyn ObjectResolver, + ) -> Result>, ObjectError> + where + T: FromPrimitive + Copy + Default, + { + optional_resolved_value(self.get(key), objects)? + .map(|value| value.try_vec_of(objects)) + .transpose() + } + + fn required_vec_of( + &self, + key: &str, + objects: &dyn ObjectResolver, + ) -> Result, ObjectError> + where + T: FromPrimitive + Copy + Default, + { + self.get_or_err(key)?.try_vec_of(objects) + } + + fn optional_array_of( + &self, + key: &str, + objects: &dyn ObjectResolver, + ) -> Result, ObjectError> + where + T: FromPrimitive + Copy + Default, + { + optional_resolved_value(self.get(key), objects)? + .map(|value| value.try_array_of(objects)) + .transpose() + } + + fn required_array_of( + &self, + key: &str, + objects: &dyn ObjectResolver, + ) -> Result<[T; N], ObjectError> + where + T: FromPrimitive + Copy + Default, + { + self.get_or_err(key)?.try_array_of(objects) + } + + fn optional_boolean( + &self, + key: &str, + objects: &dyn ObjectResolver, + ) -> Result, ObjectError> { + optional_resolved_value(self.get(key), objects)? + .map(|value| value.try_boolean(objects)) + .transpose() + } + + fn required_boolean( + &self, + key: &str, + objects: &dyn ObjectResolver, + ) -> Result { + self.get_or_err(key)?.try_boolean(objects) + } + + fn optional_object_number(&self, key: &str) -> Result, ObjectError> { + optional_direct_value(self.get(key))? + .map(ObjectVariant::try_object_number) + .transpose() + } + + fn required_object_number(&self, key: &str) -> Result { + self.get_or_err(key)?.try_object_number() + } +} + +fn optional_resolved_value<'a>( + value: Option<&'a ObjectVariant>, + objects: &'a dyn ObjectResolver, +) -> Result, ObjectError> { + let Some(value) = value else { + return Ok(None); + }; + let value = if let ObjectVariant::Reference(_) = value { + objects.resolve_object(value)? + } else { + value + }; + + match value { + ObjectVariant::Null => Ok(None), + _ => Ok(Some(value)), + } +} + +fn optional_direct_value( + value: Option<&ObjectVariant>, +) -> Result, ObjectError> { + match value { + Some(ObjectVariant::Null) | None => Ok(None), + Some(value) => Ok(Some(value)), + } +} + +fn required_slice_value( + values: &[ObjectVariant], + index: usize, +) -> Result<&ObjectVariant, ObjectError> { + values + .get(index) + .ok_or_else(|| ObjectError::InvalidArrayLength { + expected: index.saturating_add(1), + found: values.len(), + }) +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use crate::object_resolver::PassthroughResolver; + + use super::*; + + struct NullResolver; + + impl ObjectResolver for NullResolver { + fn resolve_object<'a>( + &'a self, + obj: &'a ObjectVariant, + ) -> Result<&'a ObjectVariant, ObjectError> { + match obj { + ObjectVariant::Reference(_) => Ok(&NULL_OBJECT), + _ => Ok(obj), + } + } + } + + static NULL_OBJECT: ObjectVariant = ObjectVariant::Null; + + fn dictionary_with(key: &str, value: ObjectVariant) -> Dictionary { + Dictionary::new(BTreeMap::from([(key.to_owned(), value)])) + } + + #[test] + fn slice_optional_number_converts_existing_value() { + let values = [ObjectVariant::Integer(42)]; + + let value = values + .optional_number::(0, &PassthroughResolver) + .expect("integer converts to u16"); + + assert_eq!(value, Some(42)); + } + + #[test] + fn slice_optional_number_returns_none_for_missing_or_null() { + let values = [ObjectVariant::Null, ObjectVariant::Reference(1)]; + + let missing = values + .optional_number::(2, &PassthroughResolver) + .expect("missing optional item is absent"); + let direct_null = values + .optional_number::(0, &PassthroughResolver) + .expect("null optional item is absent"); + let resolved_null = values + .optional_number::(1, &NullResolver) + .expect("resolved null optional item is absent"); + + assert_eq!(missing, None); + assert_eq!(direct_null, None); + assert_eq!(resolved_null, None); + } + + #[test] + fn slice_required_number_reports_missing_index() { + let values = [ObjectVariant::Integer(42)]; + + let error = values + .required_number::(2, &PassthroughResolver) + .expect_err("missing required item fails"); + + assert_eq!( + error, + ObjectError::InvalidArrayLength { + expected: 3, + found: 1 + } + ); + } + + #[test] + fn dictionary_optional_number_converts_existing_value() { + let dictionary = dictionary_with("Count", ObjectVariant::Real(12.0)); + + let value = dictionary + .optional_number::("Count", &PassthroughResolver) + .expect("real converts to f32"); + + assert_eq!(value, Some(12.0)); + } + + #[test] + fn dictionary_optional_number_returns_none_for_missing_or_null() { + let dictionary = dictionary_with("Null", ObjectVariant::Null); + + let missing = dictionary + .optional_number::("Missing", &PassthroughResolver) + .expect("missing optional key is absent"); + let direct_null = dictionary + .optional_number::("Null", &PassthroughResolver) + .expect("null optional key is absent"); + + assert_eq!(missing, None); + assert_eq!(direct_null, None); + } + + #[test] + fn dictionary_required_number_reports_missing_key() { + let dictionary = Dictionary::new(BTreeMap::new()); + + let error = dictionary + .required_number::("Count", &PassthroughResolver) + .expect_err("missing required key fails"); + + assert_eq!( + error, + ObjectError::MissingRequiredKey { + key: "Count".to_owned() + } + ); + } + + #[test] + fn dictionary_lookup_methods_convert_common_types() { + let nested = Dictionary::new(BTreeMap::new()); + let dictionary = Dictionary::new(BTreeMap::from([ + ( + "Dictionary".to_owned(), + ObjectVariant::Dictionary(Box::new(nested.clone())), + ), + ( + "Stream".to_owned(), + ObjectVariant::Stream(StreamObject::new( + 7, + 0, + Box::new(Dictionary::new(BTreeMap::new())), + Vec::new(), + )), + ), + ( + "Array".to_owned(), + ObjectVariant::Array(vec![ObjectVariant::Integer(1), ObjectVariant::Integer(2)]), + ), + ("String".to_owned(), ObjectVariant::Name(b"Name".to_vec())), + ( + "Bytes".to_owned(), + ObjectVariant::LiteralString(b"abc".to_vec()), + ), + ("Boolean".to_owned(), ObjectVariant::Boolean(true)), + ("Reference".to_owned(), ObjectVariant::Reference(9)), + ])); + + let stream = dictionary + .required_stream("Stream", &PassthroughResolver) + .expect("stream exists"); + + assert_eq!( + dictionary + .required_dictionary("Dictionary", &PassthroughResolver) + .expect("dictionary exists"), + &nested + ); + assert_eq!(stream.object_number, 7); + assert_eq!( + dictionary + .required_array("Array", &PassthroughResolver) + .expect("array exists") + .len(), + 2 + ); + assert_eq!( + dictionary + .required_str("String", &PassthroughResolver) + .expect("string exists") + .as_ref(), + "Name" + ); + assert_eq!( + dictionary + .required_bytes("Bytes", &PassthroughResolver) + .expect("bytes exist"), + b"abc" + ); + assert_eq!( + dictionary + .required_bytes_vec("Bytes", &PassthroughResolver) + .expect("bytes exist"), + b"abc".to_vec() + ); + assert!( + dictionary + .required_boolean("Boolean", &PassthroughResolver) + .expect("boolean exists") + ); + assert_eq!( + dictionary + .required_object_number("Reference") + .expect("reference object number exists"), + 9 + ); + } + + #[test] + fn dictionary_lookup_methods_convert_numeric_arrays() { + let dictionary = Dictionary::new(BTreeMap::from([( + "Numbers".to_owned(), + ObjectVariant::Array(vec![ + ObjectVariant::Integer(1), + ObjectVariant::Integer(2), + ObjectVariant::Integer(3), + ]), + )])); + + assert_eq!( + dictionary + .required_vec_of::("Numbers", &PassthroughResolver) + .expect("numeric vector converts"), + vec![1, 2, 3] + ); + assert_eq!( + dictionary + .required_array_of::("Numbers", &PassthroughResolver) + .expect("numeric array converts"), + [1, 2, 3] + ); + } + + #[test] + fn slice_lookup_methods_convert_common_types() { + let nested = Dictionary::new(BTreeMap::new()); + let values = [ + ObjectVariant::Dictionary(Box::new(nested.clone())), + ObjectVariant::Array(vec![ObjectVariant::Integer(1), ObjectVariant::Integer(2)]), + ObjectVariant::LiteralString(b"text".to_vec()), + ObjectVariant::Boolean(false), + ObjectVariant::Reference(3), + ]; + + assert_eq!( + values + .required_dictionary(0, &PassthroughResolver) + .expect("dictionary exists"), + &nested + ); + assert_eq!( + values + .required_array(1, &PassthroughResolver) + .expect("array exists") + .len(), + 2 + ); + assert_eq!( + values + .required_str(2, &PassthroughResolver) + .expect("string exists") + .as_ref(), + "text" + ); + assert!( + !values + .required_boolean(3, &PassthroughResolver) + .expect("boolean exists") + ); + assert_eq!( + values + .required_object_number(4) + .expect("reference object number exists"), + 3 + ); + } + + #[test] + fn optional_dictionary_returns_none_for_resolved_null() { + let values = [ObjectVariant::Reference(1)]; + + let value = values + .optional_dictionary(0, &NullResolver) + .expect("resolved null is absent"); + + assert!(value.is_none()); + } + + #[test] + fn required_lookup_propagates_type_mismatch() { + let dictionary = dictionary_with("Boolean", ObjectVariant::Boolean(true)); + + let error = dictionary + .required_str("Boolean", &PassthroughResolver) + .expect_err("boolean is not a string"); + + assert_eq!(error, ObjectError::TypeMismatch("String", "Boolean")); + } +} diff --git a/crates/pdf-parser/src/cross_reference_stream.rs b/crates/pdf-parser/src/cross_reference_stream.rs index 211f6baf..3ea6eac0 100644 --- a/crates/pdf-parser/src/cross_reference_stream.rs +++ b/crates/pdf-parser/src/cross_reference_stream.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use pdf_object::{ cross_reference_table::{CrossReferenceEntry, CrossReferenceTable}, + object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, stream::StreamObject, trailer::Trailer, @@ -24,7 +25,7 @@ pub fn parse_xref_stream( let dict = stream.dictionary.as_ref(); validate_stream_type(stream, objects)?; - let size = dict.get_or_err("Size")?.try_number::(objects)?; + let size = dict.required_number::("Size", objects)?; let layout = XrefStreamLayout::from_dictionary(stream, objects)?; if layout.entry_width == 0 { @@ -62,11 +63,10 @@ fn validate_stream_type( stream: &StreamObject, objects: &dyn ObjectResolver, ) -> Result<(), ParserError> { - let Some(type_value) = stream.dictionary.get("Type") else { + let Some(type_name) = stream.dictionary.optional_str("Type", objects)? else { return Ok(()); }; - let type_name = type_value.try_str(objects)?; if type_name.as_ref() == "XRef" { Ok(()) } else { @@ -94,8 +94,7 @@ impl XrefStreamLayout { ) -> Result { let [type_width, second_field_width, third_field_width] = stream .dictionary - .get_or_err("W")? - .try_array_of::(objects)?; + .required_array_of::("W", objects)?; Ok(Self { type_width, diff --git a/crates/pdf-parser/src/stream.rs b/crates/pdf-parser/src/stream.rs index a6aab29c..5c855df7 100644 --- a/crates/pdf-parser/src/stream.rs +++ b/crates/pdf-parser/src/stream.rs @@ -1,5 +1,7 @@ use crate::{error::ParserError, parser::PdfParser}; -use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver}; +use pdf_object::{ + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, +}; use pdf_tokenizer::error::TokenizerError; impl PdfParser<'_> { @@ -29,10 +31,7 @@ impl PdfParser<'_> { self.read_keyword(STREAM_START)?; let stream_data_start = self.tokenizer.position; - let length = dictionary - .get("Length") - .map(|value| value.try_number::(objects)) - .transpose()?; + let length = dictionary.optional_number::("Length", objects)?; match length { Some(length) => self.parse_stream_with_length(stream_data_start, length), diff --git a/crates/pdf-resources/src/external_graphics_state.rs b/crates/pdf-resources/src/external_graphics_state.rs index f84f448e..30c70930 100644 --- a/crates/pdf-resources/src/external_graphics_state.rs +++ b/crates/pdf-resources/src/external_graphics_state.rs @@ -1,5 +1,6 @@ use pdf_object::{ - dictionary::Dictionary, object_resolver::ObjectResolver, object_variant::ObjectVariant, + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, + object_variant::ObjectVariant, }; use crate::{ @@ -263,10 +264,10 @@ fn parse_soft_mask( ) -> Result { let smask = match value { ObjectVariant::Dictionary(dict) => { - let mask_type = parse_mask_mode(dict.get_or_err("S")?.try_str(objects)?.as_ref())?; + let mask_type = parse_mask_mode(dict.required_str("S", objects)?.as_ref())?; // Parse the "G" key for the `XObject` - let stream = dict.get_or_err("G")?.try_stream(objects)?; + let stream = dict.required_stream("G", objects)?; let shape = match XObject::read_xobject( &ObjectVariant::Stream(stream.clone()), diff --git a/crates/pdf-resources/src/form.rs b/crates/pdf-resources/src/form.rs index c1fd96c8..cd3abde9 100644 --- a/crates/pdf-resources/src/form.rs +++ b/crates/pdf-resources/src/form.rs @@ -1,6 +1,7 @@ use pdf_content_stream::{ContentStream, ContentStreamIdAllocator}; use pdf_graphics::rect::Rect; use pdf_graphics::transform::Transform; +use pdf_object::object_lookup::ObjectLookupExt; use pdf_object::{ dictionary::Dictionary, object_resolver::ObjectResolver, object_variant::ObjectVariant, }; @@ -37,12 +38,8 @@ impl FormXObject { content_stream: ContentStream, ) -> Result { // Retrieve the `/BBox` entry. - let bbox = Rect::from( - dictionary - .get_or_err("BBox")? - .try_array_of::(objects)?, - ) - .normalized(); + let bbox = + Rect::from(dictionary.required_array_of::("BBox", objects)?).normalized(); // Retrieve the `/Matrix` entry if present. let matrix = Matrix::from_dictionary(dictionary, objects)?; diff --git a/crates/pdf-resources/src/pattern.rs b/crates/pdf-resources/src/pattern.rs index 4351789b..fcbad867 100644 --- a/crates/pdf-resources/src/pattern.rs +++ b/crates/pdf-resources/src/pattern.rs @@ -1,6 +1,8 @@ use pdf_content_stream::{ContentStream, ContentStreamIdAllocator}; use pdf_graphics::{rect::Rect, transform::Transform}; -use pdf_object::{object_resolver::ObjectResolver, object_variant::ObjectVariant}; +use pdf_object::{ + object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, object_variant::ObjectVariant, +}; use crate::{ error::PdfPagesError, @@ -141,9 +143,7 @@ impl Pattern { ) -> Result { let dictionary = object.try_dictionary(objects)?; - let pattern_type = dictionary - .get_or_err("PatternType")? - .try_number::(objects)?; + let pattern_type = dictionary.required_number::("PatternType", objects)?; // Read the transformation matrix for the pattern. Defaults to identity. let matrix = Matrix::from_dictionary(dictionary, objects)?; @@ -151,29 +151,24 @@ impl Pattern { match PatternType::try_from(pattern_type)? { PatternType::Tiling => { // Read the `/PaintType` entry. - let paint_type_int = dictionary - .get_or_err("PaintType")? - .try_number::(objects)?; + let paint_type_int = dictionary.required_number::("PaintType", objects)?; let paint_type = PaintType::try_from(paint_type_int)?; // Read the `/TilingType` entry. - let tiling_type_int = dictionary - .get_or_err("TilingType")? - .try_number::(objects)?; + let tiling_type_int = dictionary.required_number::("TilingType", objects)?; let tiling_type = TilingType::try_from(tiling_type_int)?; // Read the `/BBox` entry. let bbox = dictionary - .get_or_err("BBox")? - .try_array_of::(objects)? + .required_array_of::("BBox", objects)? .into(); // Read the `/XStep` entry. - let x_step = dictionary.get_or_err("XStep")?.try_number::(objects)?; + let x_step = dictionary.required_number::("XStep", objects)?; // Read the `/YStep` entry. - let y_step = dictionary.get_or_err("YStep")?.try_number::(objects)?; + let y_step = dictionary.required_number::("YStep", objects)?; // Read the `/Resources` entry. Needed by the pattern's content stream. let parsed_resources = diff --git a/crates/pdf-resources/src/resources.rs b/crates/pdf-resources/src/resources.rs index cadefa1e..ed4251bb 100644 --- a/crates/pdf-resources/src/resources.rs +++ b/crates/pdf-resources/src/resources.rs @@ -10,7 +10,8 @@ use std::rc::Rc; use pdf_content_stream::ContentStreamIdAllocator; use pdf_font::font::Font; use pdf_object::{ - dictionary::Dictionary, object_resolver::ObjectResolver, object_variant::ObjectVariant, + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, + object_variant::ObjectVariant, }; use crate::{ @@ -228,7 +229,7 @@ fn read_xobject_resource( Ok(Some(resource)) } ObjectVariant::Dictionary(dictionary) => { - let subtype = dictionary.get_or_err("Subtype")?.try_str(objects)?; + let subtype = dictionary.required_str("Subtype", objects)?; if subtype.as_ref() != "Form" { return Err(crate::error::PdfPagesError::UnsupportedXObjectSubtype { subtype: subtype.into_owned(), diff --git a/crates/pdf-resources/src/xobject.rs b/crates/pdf-resources/src/xobject.rs index 4fba74dc..4b16d3c1 100644 --- a/crates/pdf-resources/src/xobject.rs +++ b/crates/pdf-resources/src/xobject.rs @@ -7,8 +7,8 @@ use crate::{ use pdf_content_stream::ContentStreamIdAllocator; use pdf_image::{ImageXObject, PdfImageError}; use pdf_object::{ - dictionary::Dictionary, object_resolver::ObjectResolver, object_variant::ObjectVariant, - stream::StreamObject, + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, + object_variant::ObjectVariant, stream::StreamObject, }; /// Represents a PDF External Object (XObject). @@ -34,8 +34,7 @@ impl ReadXObject for XObject { cycle_tracker: &mut ReadCycleTracker, id_allocator: &mut ContentStreamIdAllocator, ) -> Result { - let subtype = dictionary.get_or_err("Subtype")?.try_str(objects)?; - + let subtype = dictionary.required_str("Subtype", objects)?; match subtype.as_ref() { "Image" => { let soft_mask = resolve_image_soft_mask( diff --git a/crates/pdf-shading/src/parse.rs b/crates/pdf-shading/src/parse.rs index 5b54f1b8..c2962831 100644 --- a/crates/pdf-shading/src/parse.rs +++ b/crates/pdf-shading/src/parse.rs @@ -5,7 +5,8 @@ use pdf_color_space::color_space::ColorSpace; use pdf_function::function::{Function, FunctionImpl}; use pdf_graphics::{color::Color, point::Point, rect::Rect}; use pdf_object::{ - dictionary::Dictionary, object_resolver::ObjectResolver, object_variant::ObjectVariant, + dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, + object_variant::ObjectVariant, }; use crate::{ @@ -20,9 +21,7 @@ pub fn shading_from_dictionary( objects: &dyn ObjectResolver, ) -> Result { let dictionary = object.try_dictionary(objects)?; - let shading_type_value = dictionary - .get_or_err("ShadingType")? - .try_number::(objects)?; + let shading_type_value = dictionary.required_number::("ShadingType", objects)?; let shading_type = ShadingType::try_from(shading_type_value)?; match shading_type { @@ -43,23 +42,12 @@ fn parse_function_based( objects: &dyn ObjectResolver, ) -> Result { let color_space = ColorSpace::from_dictionary(dictionary, objects)?; - let background = dictionary - .get("Background") - .map(|object| object.try_vec_of::(objects)) - .transpose()?; + let background = dictionary.optional_vec_of::("Background", objects)?; let bbox = dictionary - .get("BBox") - .map(|object| object.try_array_of::(objects)) - .transpose()? + .optional_array_of::("BBox", objects)? .map(Rect::from); - let domain = dictionary - .get("Domain") - .map(|object| object.try_vec_of::(objects)) - .transpose()?; - let anti_alias = dictionary - .get("AntiAlias") - .map(|object| object.try_boolean(objects)) - .transpose()?; + let domain = dictionary.optional_vec_of::("Domain", objects)?; + let anti_alias = dictionary.optional_boolean("AntiAlias", objects)?; let functions = parse_functions(dictionary, objects)?; Ok(Shading::FunctionBased { @@ -77,9 +65,7 @@ fn parse_axial( objects: &dyn ObjectResolver, ) -> Result { let dictionary = object.try_dictionary(objects)?; - let coords = dictionary - .get_or_err("Coords")? - .try_array_of::(objects)?; + let coords = dictionary.required_array_of::("Coords", objects)?; let color_space = ColorSpace::from_dictionary(dictionary, objects)?.ok_or( PdfShadingError::MissingRequiredEntry { entry: "ColorSpace", @@ -101,18 +87,14 @@ fn parse_radial( objects: &dyn ObjectResolver, ) -> Result { let dictionary = object.try_dictionary(objects)?; - let coords = dictionary - .get_or_err("Coords")? - .try_array_of::(objects)?; + let coords = dictionary.required_array_of::("Coords", objects)?; let color_space = ColorSpace::from_dictionary(dictionary, objects)?.ok_or( PdfShadingError::MissingRequiredEntry { entry: "ColorSpace", }, )?; let bbox = dictionary - .get("BBox") - .map(|object| object.try_array_of::(objects)) - .transpose()? + .optional_array_of::("BBox", objects)? .map(Rect::from); let function_object = dictionary.get_or_err("Function")?; let function = Function::parse(function_object, objects)?; @@ -138,18 +120,10 @@ fn parse_patch_mesh( entry: "ColorSpace", }, )?; - let bits_per_coordinate = dictionary - .get_or_err("BitsPerCoordinate")? - .try_number::(objects)?; - let bits_per_component = dictionary - .get_or_err("BitsPerComponent")? - .try_number::(objects)?; - let bits_per_flag = dictionary - .get_or_err("BitsPerFlag")? - .try_number::(objects)?; - let decode_values = dictionary - .get_or_err("Decode")? - .try_vec_of::(objects)?; + let bits_per_coordinate = dictionary.required_number::("BitsPerCoordinate", objects)?; + let bits_per_component = dictionary.required_number::("BitsPerComponent", objects)?; + let bits_per_flag = dictionary.required_number::("BitsPerFlag", objects)?; + let decode_values = dictionary.required_vec_of::("Decode", objects)?; if decode_values.len() < 6 || decode_values.len() % 2 != 0 { return Err(PdfShadingError::InvalidShadingMeshData { @@ -158,14 +132,9 @@ fn parse_patch_mesh( } let bbox = dictionary - .get("BBox") - .map(|object| object.try_array_of::(objects)) - .transpose()? + .optional_array_of::("BBox", objects)? .map(Rect::from); - let anti_alias = dictionary - .get("AntiAlias") - .map(|object| object.try_boolean(objects)) - .transpose()?; + let anti_alias = dictionary.optional_boolean("AntiAlias", objects)?; let functions = if dictionary.get("Function").is_some() { parse_functions(dictionary, objects)? } else { From 0049e3b7667063d91789ac3e97365ab04add3194 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 8 Jul 2026 00:43:02 +0300 Subject: [PATCH 2/2] pdf-object: return borrowed strings Change string lookup helpers to return borrowed &str values and report invalid UTF-8 as a typed object error. Update downstream parsers to allocate explicitly only where owned strings are still needed. Co-authored-by: Codex --- crates/pdf-annotation-types/src/action.rs | 21 +++--- crates/pdf-annotation-types/src/annotation.rs | 11 ++-- .../pdf-annotation-types/src/border_effect.rs | 6 +- .../pdf-annotation-types/src/border_style.rs | 6 +- .../pdf-annotation-types/src/destination.rs | 2 +- .../src/subtypes/caret.rs | 6 +- .../pdf-annotation-types/src/subtypes/link.rs | 6 +- .../pdf-annotation-types/src/subtypes/mod.rs | 2 +- .../pdf-color-space/src/color_space_reader.rs | 6 +- .../src/device_n_color_space.rs | 2 +- .../src/separation_color_space.rs | 2 +- .../src/operands.rs | 11 ++-- crates/pdf-document/src/encryption.rs | 2 +- crates/pdf-document/src/pages.rs | 2 +- crates/pdf-filter/src/filter.rs | 37 ++++++----- crates/pdf-font/src/cid_system_info.rs | 2 +- crates/pdf-font/src/encoding.rs | 10 +-- crates/pdf-font/src/fallback.rs | 2 +- crates/pdf-font/src/font.rs | 2 +- crates/pdf-font/src/type0_font.rs | 6 +- crates/pdf-font/src/type1_font.rs | 4 +- crates/pdf-object/src/error.rs | 6 ++ crates/pdf-object/src/object_lookup.rs | 64 ++++++++++++++----- crates/pdf-object/src/object_variant.rs | 40 ++++++++---- .../pdf-parser/src/cross_reference_stream.rs | 4 +- crates/pdf-parser/src/inline_image.rs | 3 +- .../src/external_graphics_state.rs | 8 +-- crates/pdf-resources/src/resources.rs | 4 +- crates/pdf-resources/src/xobject.rs | 3 +- 29 files changed, 150 insertions(+), 130 deletions(-) diff --git a/crates/pdf-annotation-types/src/action.rs b/crates/pdf-annotation-types/src/action.rs index eb96e5f3..e9254501 100644 --- a/crates/pdf-annotation-types/src/action.rs +++ b/crates/pdf-annotation-types/src/action.rs @@ -108,14 +108,11 @@ impl AnnotationAction { key: &'static str, objects: &dyn ObjectResolver, ) -> Result, AnnotationError> { - let Some(value) = dictionary.get(key) else { + let Some(action_dictionary) = dictionary.optional_dictionary(key, objects)? else { return Ok(None); }; - let action_dictionary = value.try_dictionary(objects)?.clone(); - let action_type = action_dictionary.required_str("S", objects)?.into_owned(); - - let action = match action_type.as_ref() { + let action = match action_dictionary.required_str("S", objects)? { "GoTo" => Self::GoTo { destination: AnnotationDestination::from_object( action_dictionary.get_or_err("D")?, @@ -140,7 +137,7 @@ impl AnnotationAction { }, "Launch" => Self::Launch { file_specification: FileSpecification::from_dictionary( - &action_dictionary, + action_dictionary, "F", objects, )?, @@ -154,15 +151,15 @@ impl AnnotationAction { }, "SubmitForm" => Self::SubmitForm { file_specification: FileSpecification::from_dictionary( - &action_dictionary, + action_dictionary, "F", objects, )?, - fields: name_list(&action_dictionary, "Fields", objects)?, + fields: name_list(action_dictionary, "Fields", objects)?, flags: action_dictionary.optional_number::("Flags", objects)?, }, "ResetForm" => Self::ResetForm { - fields: name_list(&action_dictionary, "Fields", objects)?, + fields: name_list(action_dictionary, "Fields", objects)?, flags: action_dictionary.optional_number::("Flags", objects)?, }, "ImportData" => Self::ImportData { @@ -175,12 +172,12 @@ impl AnnotationAction { script: javascript_bytes(action_dictionary.get_or_err("JS")?, objects)?, }, "SetOCGState" => Self::SetOCGState { - state: name_list(&action_dictionary, "State", objects)?.unwrap_or_default(), + state: name_list(action_dictionary, "State", objects)?.unwrap_or_default(), preserve_rb: action_dictionary.optional_boolean("PreserveRB", objects)?, }, "Rendition" => Self::Rendition { operation: action_dictionary.optional_number::("OP", objects)?, - rendition: Rendition::from_dictionary(&action_dictionary, objects)?, + rendition: Rendition::from_dictionary(action_dictionary, objects)?, }, "Trans" => Self::Trans { transition: action_dictionary @@ -190,7 +187,7 @@ impl AnnotationAction { duration: action_dictionary.optional_number::("D", objects)?, }, "GoTo3DView" => Self::GoTo3DView { - view: three_d_view(&action_dictionary, "TA", objects)?, + view: three_d_view(action_dictionary, "TA", objects)?, }, other => Self::Unknown { action_type: other.to_owned(), diff --git a/crates/pdf-annotation-types/src/annotation.rs b/crates/pdf-annotation-types/src/annotation.rs index ef9b17b2..d97ea384 100644 --- a/crates/pdf-annotation-types/src/annotation.rs +++ b/crates/pdf-annotation-types/src/annotation.rs @@ -84,8 +84,8 @@ impl Annotation { // Some PDFs omit `/Type` on annotation dictionaries even though the // entry is nominally expected to be `/Annot`, so only validate it when // the key is actually present. - if let Some(annotation_type) = dictionary.get("Type") { - match annotation_type.try_str(objects)?.as_ref() { + if let Some(annotation_type) = dictionary.optional_str("Type", objects)? { + match annotation_type { "Annot" => {} other => { return Err(AnnotationError::InvalidEntry { @@ -98,10 +98,7 @@ impl Annotation { // `/Subtype` identifies the concrete annotation kind and is required // for dispatching to the subtype-specific parser. - let subtype = dictionary - .get_or_err("Subtype")? - .try_str(objects)? - .into_owned(); + let subtype = dictionary.required_str("Subtype", objects)?.to_owned(); let rect = dictionary .get("Rect") @@ -125,7 +122,7 @@ impl Annotation { let flags = dictionary.optional_number::("F", objects)?; let appearance_state = dictionary .optional_str("AS", objects)? - .map(|s| s.into_owned()); + .map(|s| s.to_owned()); let struct_parent = dictionary.optional_number::("StructParent", objects)?; let appearance = AppearanceDictionary::from_dictionary( diff --git a/crates/pdf-annotation-types/src/border_effect.rs b/crates/pdf-annotation-types/src/border_effect.rs index 31aa535b..0cc8b1c1 100644 --- a/crates/pdf-annotation-types/src/border_effect.rs +++ b/crates/pdf-annotation-types/src/border_effect.rs @@ -25,11 +25,7 @@ impl BorderEffect { let dictionary = value.try_dictionary(objects)?; let style = dictionary .get("S") - .map(|value| { - value - .try_str(objects) - .map(|name| BorderEffectStyle::from(name.as_ref())) - }) + .map(|value| value.try_str(objects).map(BorderEffectStyle::from)) .transpose()?; let intensity = dictionary.optional_number::("I", objects)?; diff --git a/crates/pdf-annotation-types/src/border_style.rs b/crates/pdf-annotation-types/src/border_style.rs index a150bf44..b66b9523 100644 --- a/crates/pdf-annotation-types/src/border_style.rs +++ b/crates/pdf-annotation-types/src/border_style.rs @@ -28,11 +28,7 @@ impl BorderStyle { let width = dictionary.optional_number::("W", objects)?; let style = dictionary .get("S") - .map(|value| { - value - .try_str(objects) - .map(|name| BorderStyleName::from(name.as_ref())) - }) + .map(|value| value.try_str(objects).map(BorderStyleName::from)) .transpose()?; let dash_pattern = dictionary.optional_vec_of::("D", objects)?; diff --git a/crates/pdf-annotation-types/src/destination.rs b/crates/pdf-annotation-types/src/destination.rs index 5750f56d..e400ee1f 100644 --- a/crates/pdf-annotation-types/src/destination.rs +++ b/crates/pdf-annotation-types/src/destination.rs @@ -144,7 +144,7 @@ fn explicit_destination( let page = destination_target(page_item, objects)?; - match mode_item.try_str(objects)?.as_ref() { + match mode_item.try_str(objects)? { "XYZ" => Ok(ExplicitDestination::Xyz { page, left: items.optional_number(2, objects)?, diff --git a/crates/pdf-annotation-types/src/subtypes/caret.rs b/crates/pdf-annotation-types/src/subtypes/caret.rs index d2064eb2..13bf2743 100644 --- a/crates/pdf-annotation-types/src/subtypes/caret.rs +++ b/crates/pdf-annotation-types/src/subtypes/caret.rs @@ -20,11 +20,7 @@ impl CaretAnnotation { let difference_rect = dictionary.optional_array_of::("RD", objects)?; let style = dictionary .get("Sy") - .map(|value| { - value - .try_str(objects) - .map(|name| CaretSymbolStyle::from(name.as_ref())) - }) + .map(|value| value.try_str(objects).map(CaretSymbolStyle::from)) .transpose()?; Ok(Self { diff --git a/crates/pdf-annotation-types/src/subtypes/link.rs b/crates/pdf-annotation-types/src/subtypes/link.rs index 8ef3281a..549cd712 100644 --- a/crates/pdf-annotation-types/src/subtypes/link.rs +++ b/crates/pdf-annotation-types/src/subtypes/link.rs @@ -28,11 +28,7 @@ impl LinkAnnotation { ) -> Result { let highlight_mode = dictionary .get("H") - .map(|value| { - value - .try_str(objects) - .map(|name| LinkHighlightMode::from(name.as_ref())) - }) + .map(|value| value.try_str(objects).map(LinkHighlightMode::from)) .transpose()?; let destination = AnnotationDestination::from_dictionary(dictionary, "Dest", objects)?; let action = AnnotationAction::from_dictionary(dictionary, "A", objects)?; diff --git a/crates/pdf-annotation-types/src/subtypes/mod.rs b/crates/pdf-annotation-types/src/subtypes/mod.rs index e5ee19fa..4e46024e 100644 --- a/crates/pdf-annotation-types/src/subtypes/mod.rs +++ b/crates/pdf-annotation-types/src/subtypes/mod.rs @@ -70,7 +70,7 @@ pub(crate) fn line_endings( let mut parsed = [LineEndingStyle::None, LineEndingStyle::None]; for (slot, item) in parsed.iter_mut().zip(endings.iter()) { - *slot = LineEndingStyle::from(item.try_str(objects)?.as_ref()); + *slot = LineEndingStyle::from(item.try_str(objects)?); } Ok(Some(parsed)) diff --git a/crates/pdf-color-space/src/color_space_reader.rs b/crates/pdf-color-space/src/color_space_reader.rs index b423e41f..a06b3f6b 100644 --- a/crates/pdf-color-space/src/color_space_reader.rs +++ b/crates/pdf-color-space/src/color_space_reader.rs @@ -65,7 +65,7 @@ pub(crate) fn parse_color_space_object( ObjectVariant::Array(arr) => { parse_color_space_array(objects, arr.as_slice(), depth.saturating_add(1)) } - other => parse_color_space_name(other.try_str(objects)?.as_ref()), + other => parse_color_space_name(other.try_str(objects)?), } } @@ -78,7 +78,7 @@ fn parse_color_space_array( depth: usize, ) -> Result { if let [single] = arr { - return parse_color_space_name(single.try_str(objects)?.as_ref()); + return parse_color_space_name(single.try_str(objects)?); } // Get the color space type (first element) @@ -88,7 +88,7 @@ fn parse_color_space_array( description: "empty color space array".into(), })?; - match cs_type.try_str(objects)?.as_ref() { + match cs_type.try_str(objects)? { "Indexed" => parse_indexed_color_space(objects, arr, depth), "ICCBased" => parse_icc_based_color_space(objects, arr, depth), "Separation" => parse_separation_color_space(objects, arr, depth), diff --git a/crates/pdf-color-space/src/device_n_color_space.rs b/crates/pdf-color-space/src/device_n_color_space.rs index c6c06e1a..e2bb6f14 100644 --- a/crates/pdf-color-space/src/device_n_color_space.rs +++ b/crates/pdf-color-space/src/device_n_color_space.rs @@ -46,7 +46,7 @@ pub(crate) fn parse_device_n_color_space( let names = names_obj .try_array(objects)? .iter() - .map(|n| n.try_str(objects).map(|s| s.into_owned())) + .map(|n| n.try_str(objects).map(|s| s.to_owned())) .collect::, _>>()?; let alternate_space = parse_color_space_object(objects, alt_obj, depth)?; diff --git a/crates/pdf-color-space/src/separation_color_space.rs b/crates/pdf-color-space/src/separation_color_space.rs index 4cc47c58..e2d83cbb 100644 --- a/crates/pdf-color-space/src/separation_color_space.rs +++ b/crates/pdf-color-space/src/separation_color_space.rs @@ -34,7 +34,7 @@ pub(crate) fn parse_separation_color_space( }); }; - let name = name.try_str(objects)?.to_string(); + let name = name.try_str(objects)?.to_owned(); let alternate_space = parse_color_space_object(objects, alternate_space, depth)?; let tint_transform = Function::parse(objects.resolve_object(tint_transform)?, objects)?; diff --git a/crates/pdf-content-stream-operators/src/operands.rs b/crates/pdf-content-stream-operators/src/operands.rs index 59c06a34..bd334b1b 100644 --- a/crates/pdf-content-stream-operators/src/operands.rs +++ b/crates/pdf-content-stream-operators/src/operands.rs @@ -32,13 +32,12 @@ impl Operands { } pub fn get_str(&mut self) -> Result { - match self.take_next()? { - ObjectVariant::HexString(s) - | ObjectVariant::Name(s) - | ObjectVariant::LiteralString(s) => Ok(String::from_utf8_lossy(&s).into_owned()), - other => Err(PdfOperatorError::OperandTypeMismatch { + let object = self.take_next()?; + match object.try_str(&PassthroughResolver) { + Ok(value) => Ok(value.to_owned()), + Err(_) => Err(PdfOperatorError::OperandTypeMismatch { expected: "a string operand (HexString, Name, or LiteralString)", - found: other.name(), + found: object.name(), }), } } diff --git a/crates/pdf-document/src/encryption.rs b/crates/pdf-document/src/encryption.rs index c868ead4..403b3699 100644 --- a/crates/pdf-document/src/encryption.rs +++ b/crates/pdf-document/src/encryption.rs @@ -151,7 +151,7 @@ impl EncryptDictionary { ) -> Result { let filter = dict .required_str("Filter", objects) - .map(|obj| EncryptionFilter::from(obj.as_ref()))?; + .map(EncryptionFilter::from)?; let version_num = dict.required_number::("V", objects)?; let version = EncryptionVersion::try_from(version_num)?; diff --git a/crates/pdf-document/src/pages.rs b/crates/pdf-document/src/pages.rs index 281f3534..ea70a357 100644 --- a/crates/pdf-document/src/pages.rs +++ b/crates/pdf-document/src/pages.rs @@ -48,7 +48,7 @@ impl ReadFromDictionary for PdfPages { let dictionary = value.try_dictionary(objects)?; // Determine the type of the child object by reading its `/Type` entry. - match dictionary.required_str("Type", objects)?.as_ref() { + match dictionary.required_str("Type", objects)? { PdfPage::KEY => { // If the child is a leaf node (`/Type /Page`), parse it as a `PdfPage`. let page = PdfPage::from_dictionary( diff --git a/crates/pdf-filter/src/filter.rs b/crates/pdf-filter/src/filter.rs index bc48f893..661f51d2 100644 --- a/crates/pdf-filter/src/filter.rs +++ b/crates/pdf-filter/src/filter.rs @@ -73,9 +73,9 @@ pub enum Filter { Unsupported(String), } -impl From> for Filter { - fn from(name: Cow<'_, str>) -> Self { - match name.as_ref() { +impl From<&str> for Filter { + fn from(name: &str) -> Self { + match name { "DCTDecode" => Self::DCTDecode, "DCT" => Self::DCTDecode, "FlateDecode" => Self::FlateDecode, @@ -90,17 +90,11 @@ impl From> for Filter { "RunLengthDecode" => Self::RunLengthDecode, "RL" => Self::RunLengthDecode, "JBIG2Decode" => Self::JBIG2Decode, - _ => Self::Unsupported(name.into_owned()), + _ => Self::Unsupported(name.to_owned()), } } } -impl From<&str> for Filter { - fn from(name: &str) -> Self { - Self::from(Cow::Borrowed(name)) - } -} - impl fmt::Display for Filter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -479,12 +473,17 @@ fn resolve_jbig2_dimensions( dict: &Dictionary, objects: &dyn ObjectResolver, ) -> Result<(u16, u16), FilterError> { - let width = dict.required_number::("Width", objects)?; - let height = dict.required_number::("Height", objects)?; + let missing_dimensions_error = + || FilterError::Decompression("JBIG2Decode requires positive Width and Height".into()); + + let width = dict + .optional_number::("Width", objects)? + .ok_or_else(missing_dimensions_error)?; + let height = dict + .optional_number::("Height", objects)? + .ok_or_else(missing_dimensions_error)?; if width == 0 || height == 0 { - return Err(FilterError::Decompression( - "JBIG2Decode requires positive Width and Height".into(), - )); + return Err(missing_dimensions_error()); } Ok((width, height)) @@ -503,7 +502,7 @@ fn resolve_jbig2_globals( #[cfg(test)] mod tests { - use std::{borrow::Cow, collections::BTreeMap}; + use std::collections::BTreeMap; use pdf_object::{ dictionary::Dictionary, object_resolver::ObjectResolver, object_variant::ObjectVariant, @@ -514,21 +513,21 @@ mod tests { #[test] fn test_filter_name_round_trip_ascii_hex() { - let filter = Filter::from(Cow::Borrowed("ASCIIHexDecode")); + let filter = Filter::from("ASCIIHexDecode"); assert_eq!(filter, Filter::ASCIIHexDecode); assert_eq!(filter.to_string(), "ASCIIHexDecode"); } #[test] fn test_filter_name_round_trip_run_length() { - let filter = Filter::from(Cow::Borrowed("RunLengthDecode")); + let filter = Filter::from("RunLengthDecode"); assert_eq!(filter, Filter::RunLengthDecode); assert_eq!(filter.to_string(), "RunLengthDecode"); } #[test] fn test_filter_name_round_trip_jbig2() { - let filter = Filter::from(Cow::Borrowed("JBIG2Decode")); + let filter = Filter::from("JBIG2Decode"); assert_eq!(filter, Filter::JBIG2Decode); assert_eq!(filter.to_string(), "JBIG2Decode"); } diff --git a/crates/pdf-font/src/cid_system_info.rs b/crates/pdf-font/src/cid_system_info.rs index 200206cb..fd8ae566 100644 --- a/crates/pdf-font/src/cid_system_info.rs +++ b/crates/pdf-font/src/cid_system_info.rs @@ -28,5 +28,5 @@ pub(crate) fn cid_ordering_from_dictionary( return Ok(None); }; - Ok(CidOrdering::from_name(ordering.as_ref())) + Ok(CidOrdering::from_name(ordering)) } diff --git a/crates/pdf-font/src/encoding.rs b/crates/pdf-font/src/encoding.rs index 2aef2301..5a07199d 100644 --- a/crates/pdf-font/src/encoding.rs +++ b/crates/pdf-font/src/encoding.rs @@ -21,14 +21,14 @@ pub enum FontEncoding { Unknown(String), } -impl From> for FontEncoding { - fn from(name: Cow<'_, str>) -> Self { - match name.as_ref() { +impl From<&str> for FontEncoding { + fn from(name: &str) -> Self { + match name { "MacRomanEncoding" => Self::MacRoman, "MacExpertEncoding" => Self::MacExpert, "StandardEncoding" => Self::Standard, "WinAnsiEncoding" => Self::WinAnsi, - _ => Self::Unknown(name.to_string()), + _ => Self::Unknown(name.to_owned()), } } } @@ -74,7 +74,7 @@ impl Encoding { } _ => { if let Some(slot) = self.names.get_mut(current_range_start) { - *slot = Cow::Owned(chunk.try_str(objects)?.to_string()); + *slot = Cow::Owned(chunk.try_str(objects)?.to_owned()); } current_range_start = current_range_start.saturating_add(1); } diff --git a/crates/pdf-font/src/fallback.rs b/crates/pdf-font/src/fallback.rs index f48c2ffc..b5924b43 100644 --- a/crates/pdf-font/src/fallback.rs +++ b/crates/pdf-font/src/fallback.rs @@ -106,7 +106,7 @@ fn standard14_from_dictionary( dictionary .get("BaseFont") .and_then(|value| value.try_str(objects).ok()) - .and_then(|name| Standard14Font::from_base_font_name(name.as_ref())) + .and_then(Standard14Font::from_base_font_name) .unwrap_or_else(|| Standard14Font::from(flags)) } diff --git a/crates/pdf-font/src/font.rs b/crates/pdf-font/src/font.rs index 784af6f0..7f0e78cd 100644 --- a/crates/pdf-font/src/font.rs +++ b/crates/pdf-font/src/font.rs @@ -42,7 +42,7 @@ impl Font { ) -> Result { // Determine the font subtype from the dictionary. let subtype = dictionary.required_str("Subtype", objects)?; - match subtype.as_ref() { + match subtype { "Type0" => { let type0_font = Type0Font::from_dictionary(dictionary, objects)?; Ok(Font::Type0(type0_font)) diff --git a/crates/pdf-font/src/type0_font.rs b/crates/pdf-font/src/type0_font.rs index 3822dd37..9d9c752f 100644 --- a/crates/pdf-font/src/type0_font.rs +++ b/crates/pdf-font/src/type0_font.rs @@ -208,9 +208,7 @@ fn parse_encoding( ObjectVariant::Stream(stream) => { Ok(Type0EncodingCMap::from_bytes(&stream.data()?)?) } - _ => Ok(Type0EncodingCMap::from_name( - value.try_str(objects)?.as_ref(), - )?), + _ => Ok(Type0EncodingCMap::from_name(value.try_str(objects)?)?), } }) .transpose() @@ -283,7 +281,7 @@ fn cid_font_subtype( dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> Result { - match dictionary.required_str("Subtype", objects)?.as_ref() { + match dictionary.required_str("Subtype", objects)? { "CIDFontType0" => Ok(CidFontSubType::Type0), "CIDFontType2" => Ok(CidFontSubType::Type2), other => Err(FontError::UnsupportedCidFontSubtype { diff --git a/crates/pdf-font/src/type1_font.rs b/crates/pdf-font/src/type1_font.rs index 77d08ee3..b47b8bcb 100644 --- a/crates/pdf-font/src/type1_font.rs +++ b/crates/pdf-font/src/type1_font.rs @@ -119,10 +119,10 @@ impl Type1Font { let data = stream.data()?; - return match subtype.as_deref() { + return match subtype { Some("Type1C") | Some("CIDFontType0C") => build_cff_font(data.as_ref()) .map(|font| (font, Type1FontProgramFormat::OpenTypeCff)), - Some("OpenType") => Ok((data.into_owned(), Type1FontProgramFormat::OpenTypeCff)), + Some("OpenType") => Ok((data.to_vec(), Type1FontProgramFormat::OpenTypeCff)), Some(other) => Err(FontError::UnsupportedFontSubtype { subtype: other.to_string(), }), diff --git a/crates/pdf-object/src/error.rs b/crates/pdf-object/src/error.rs index a4030f04..78fe2450 100644 --- a/crates/pdf-object/src/error.rs +++ b/crates/pdf-object/src/error.rs @@ -13,6 +13,12 @@ pub enum ObjectError { TypeMismatch(&'static str, &'static str), #[error("Failed to convert number to the requested type")] NumberConversionError, + #[error("Invalid UTF-8 in {object_type}")] + InvalidUtf8String { + object_type: &'static str, + #[source] + source: std::str::Utf8Error, + }, #[error("Detected a cyclic dependency while reading object {obj_num}")] CyclicDependency { obj_num: usize }, #[error("Failed to resolve an object reference {obj_num}")] diff --git a/crates/pdf-object/src/object_lookup.rs b/crates/pdf-object/src/object_lookup.rs index 2bb17baa..b07fd8cd 100644 --- a/crates/pdf-object/src/object_lookup.rs +++ b/crates/pdf-object/src/object_lookup.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use num_traits::FromPrimitive; use crate::{ @@ -152,7 +150,7 @@ pub trait ObjectLookupExt { objects: &'a dyn ObjectResolver, ) -> Result<&'a [ObjectVariant], ObjectError>; - /// Returns an optional string-like value from this container. + /// Returns an optional UTF-8 string value from this container. /// /// This method looks up a value by key or index and converts it using /// [`ObjectVariant::try_str`]. Missing entries and explicit PDF `null` @@ -166,16 +164,16 @@ pub trait ObjectLookupExt { /// /// # Returns /// - /// Returns `Ok(Some(Cow))` when the value exists and converts + /// Returns `Ok(Some(&str))` when the value exists and converts /// successfully, `Ok(None)` when the value is missing or resolves to /// `null`, or `Err` if reference resolution or conversion fails. fn optional_str<'a>( &'a self, key: K, objects: &'a dyn ObjectResolver, - ) -> Result>, ObjectError>; + ) -> Result, ObjectError>; - /// Returns a required string-like value from this container. + /// Returns a required UTF-8 string value from this container. /// /// This method looks up a value by key or index and converts it using /// [`ObjectVariant::try_str`]. @@ -188,14 +186,14 @@ pub trait ObjectLookupExt { /// /// # Returns /// - /// Returns `Ok(Cow)` when the value exists and converts successfully, - /// or `Err` if the value is missing, reference resolution fails, or + /// Returns `Ok(&str)` when the value exists and converts successfully, or + /// `Err` if the value is missing, reference resolution fails, or /// conversion fails. fn required_str<'a>( &'a self, key: K, objects: &'a dyn ObjectResolver, - ) -> Result, ObjectError>; + ) -> Result<&'a str, ObjectError>; /// Returns an optional byte string value from this container. /// @@ -589,7 +587,7 @@ impl ObjectLookupExt for [ObjectVariant] { &'a self, index: usize, objects: &'a dyn ObjectResolver, - ) -> Result>, ObjectError> { + ) -> Result, ObjectError> { optional_resolved_value(self.get(index), objects)? .map(|value| value.try_str(objects)) .transpose() @@ -599,7 +597,7 @@ impl ObjectLookupExt for [ObjectVariant] { &'a self, index: usize, objects: &'a dyn ObjectResolver, - ) -> Result, ObjectError> { + ) -> Result<&'a str, ObjectError> { required_slice_value(self, index)?.try_str(objects) } @@ -799,7 +797,7 @@ impl ObjectLookupExt<&str> for Dictionary { &'a self, key: &str, objects: &'a dyn ObjectResolver, - ) -> Result>, ObjectError> { + ) -> Result, ObjectError> { optional_resolved_value(self.get(key), objects)? .map(|value| value.try_str(objects)) .transpose() @@ -809,7 +807,7 @@ impl ObjectLookupExt<&str> for Dictionary { &'a self, key: &str, objects: &'a dyn ObjectResolver, - ) -> Result, ObjectError> { + ) -> Result<&'a str, ObjectError> { self.get_or_err(key)?.try_str(objects) } @@ -1154,8 +1152,7 @@ mod tests { assert_eq!( dictionary .required_str("String", &PassthroughResolver) - .expect("string exists") - .as_ref(), + .expect("string exists"), "Name" ); assert_eq!( @@ -1235,8 +1232,7 @@ mod tests { assert_eq!( values .required_str(2, &PassthroughResolver) - .expect("string exists") - .as_ref(), + .expect("string exists"), "text" ); assert!( @@ -1273,4 +1269,38 @@ mod tests { assert_eq!(error, ObjectError::TypeMismatch("String", "Boolean")); } + + #[test] + fn required_str_propagates_invalid_utf8() { + let dictionary = dictionary_with("Name", ObjectVariant::Name(vec![0xFF])); + + let error = dictionary + .required_str("Name", &PassthroughResolver) + .expect_err("invalid utf-8 should fail"); + + assert!(matches!( + error, + ObjectError::InvalidUtf8String { + object_type: "Name", + .. + } + )); + } + + #[test] + fn optional_str_propagates_invalid_utf8() { + let dictionary = dictionary_with("Name", ObjectVariant::LiteralString(vec![0xFF])); + + let error = dictionary + .optional_str("Name", &PassthroughResolver) + .expect_err("invalid utf-8 should fail"); + + assert!(matches!( + error, + ObjectError::InvalidUtf8String { + object_type: "LiteralString", + .. + } + )); + } } diff --git a/crates/pdf-object/src/object_variant.rs b/crates/pdf-object/src/object_variant.rs index c577d00d..979976fd 100644 --- a/crates/pdf-object/src/object_variant.rs +++ b/crates/pdf-object/src/object_variant.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use num_traits::FromPrimitive; use crate::cross_reference_table::CrossReferenceTable; @@ -137,7 +135,7 @@ impl ObjectVariant { } } - /// Resolves an `ObjectVariant` into a `String`. + /// Resolves an `ObjectVariant` into UTF-8 text. /// /// This function takes a reference to an `ObjectVariant` and attempts to resolve it /// into a `String`. @@ -148,12 +146,9 @@ impl ObjectVariant { /// /// # Returns /// - /// `String` or `Err` if the object is not a string or if a reference cannot be + /// `&str` or `Err` if the object is not a string or if a reference cannot be /// resolved. - pub fn try_str<'a>( - &'a self, - objects: &'a dyn ObjectResolver, - ) -> Result, ObjectError> { + pub fn try_str<'a>(&'a self, objects: &'a dyn ObjectResolver) -> Result<&'a str, ObjectError> { let object = if let ObjectVariant::Reference(_) = self { objects.resolve_object(self)? } else { @@ -161,12 +156,13 @@ impl ObjectVariant { }; match object { - ObjectVariant::HexString(s) => { - let s = String::from_utf8_lossy(s); - Ok(s) - } - ObjectVariant::LiteralString(s) | ObjectVariant::Name(s) => { - Ok(String::from_utf8_lossy(s)) + ObjectVariant::HexString(s) + | ObjectVariant::LiteralString(s) + | ObjectVariant::Name(s) => { + std::str::from_utf8(s).map_err(|source| ObjectError::InvalidUtf8String { + object_type: object.name(), + source, + }) } _ => Err(ObjectError::TypeMismatch("String", object.name())), } @@ -415,6 +411,22 @@ mod tests { assert_eq!(err, ObjectError::TypeMismatch("String", "Integer")); } + #[test] + fn try_str_returns_invalid_utf8_error() { + let object = ObjectVariant::Name(vec![0xFF]); + let err = object + .try_str(&PassthroughResolver) + .expect_err("invalid utf-8 should fail"); + + assert!(matches!( + err, + ObjectError::InvalidUtf8String { + object_type: "Name", + .. + } + )); + } + #[test] fn try_number_returns_type_mismatch_for_non_number() { let object = ObjectVariant::Array(vec![]); diff --git a/crates/pdf-parser/src/cross_reference_stream.rs b/crates/pdf-parser/src/cross_reference_stream.rs index 3ea6eac0..6ec06ba1 100644 --- a/crates/pdf-parser/src/cross_reference_stream.rs +++ b/crates/pdf-parser/src/cross_reference_stream.rs @@ -67,12 +67,12 @@ fn validate_stream_type( return Ok(()); }; - if type_name.as_ref() == "XRef" { + if type_name == "XRef" { Ok(()) } else { Err(ParserError::InvalidKeyword( "XRef".to_string(), - type_name.into_owned(), + type_name.to_owned(), )) } } diff --git a/crates/pdf-parser/src/inline_image.rs b/crates/pdf-parser/src/inline_image.rs index 7c3728ff..abe0a84f 100644 --- a/crates/pdf-parser/src/inline_image.rs +++ b/crates/pdf-parser/src/inline_image.rs @@ -190,9 +190,8 @@ impl PdfParser<'_> { let color_space = dictionary .get("CS") .or_else(|| dictionary.get("ColorSpace"))?; - let color_space_name = color_space.try_str(objects).ok()?; - match color_space_name.as_ref() { + match color_space.try_str(objects).ok()? { "G" | "DeviceGray" => Some(1), "RGB" | "DeviceRGB" => Some(3), "CMYK" | "DeviceCMYK" => Some(4), diff --git a/crates/pdf-resources/src/external_graphics_state.rs b/crates/pdf-resources/src/external_graphics_state.rs index 30c70930..5af4675b 100644 --- a/crates/pdf-resources/src/external_graphics_state.rs +++ b/crates/pdf-resources/src/external_graphics_state.rs @@ -244,10 +244,10 @@ fn parse_blend_mode( value .try_array(objects)? .iter() - .map(|obj| to_blend_mode(obj.try_str(objects)?.as_ref())) + .map(|obj| to_blend_mode(obj.try_str(objects)?)) .collect::, _>>()? } else { - let mode = to_blend_mode(value.try_str(objects)?.as_ref())?; + let mode = to_blend_mode(value.try_str(objects)?)?; vec![mode] }; @@ -264,7 +264,7 @@ fn parse_soft_mask( ) -> Result { let smask = match value { ObjectVariant::Dictionary(dict) => { - let mask_type = parse_mask_mode(dict.required_str("S", objects)?.as_ref())?; + let mask_type = parse_mask_mode(dict.required_str("S", objects)?)?; // Parse the "G" key for the `XObject` let stream = dict.required_stream("G", objects)?; @@ -287,7 +287,7 @@ fn parse_soft_mask( Some(Box::new(SoftMask { mask_type, shape })) } - other => match other.try_str(objects)?.as_ref() { + other => match other.try_str(objects)? { "None" => None, _ => { return Err(invalid_ext_gstate_entry_value( diff --git a/crates/pdf-resources/src/resources.rs b/crates/pdf-resources/src/resources.rs index ed4251bb..01a7cd74 100644 --- a/crates/pdf-resources/src/resources.rs +++ b/crates/pdf-resources/src/resources.rs @@ -230,9 +230,9 @@ fn read_xobject_resource( } ObjectVariant::Dictionary(dictionary) => { let subtype = dictionary.required_str("Subtype", objects)?; - if subtype.as_ref() != "Form" { + if subtype != "Form" { return Err(crate::error::PdfPagesError::UnsupportedXObjectSubtype { - subtype: subtype.into_owned(), + subtype: subtype.to_owned(), }); } diff --git a/crates/pdf-resources/src/xobject.rs b/crates/pdf-resources/src/xobject.rs index 4b16d3c1..719dd8cc 100644 --- a/crates/pdf-resources/src/xobject.rs +++ b/crates/pdf-resources/src/xobject.rs @@ -34,8 +34,7 @@ impl ReadXObject for XObject { cycle_tracker: &mut ReadCycleTracker, id_allocator: &mut ContentStreamIdAllocator, ) -> Result { - let subtype = dictionary.required_str("Subtype", objects)?; - match subtype.as_ref() { + match dictionary.required_str("Subtype", objects)? { "Image" => { let soft_mask = resolve_image_soft_mask( dictionary,