diff --git a/crates/pdf-annotation-types/src/action.rs b/crates/pdf-annotation-types/src/action.rs index 2a9f4afd..e9254501 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::{ @@ -108,17 +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 - .get_or_err("S")? - .try_str(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")?, @@ -135,23 +129,15 @@ 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( - &action_dictionary, + action_dictionary, "F", objects, )?, @@ -161,26 +147,20 @@ 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( - &action_dictionary, + action_dictionary, "F", objects, )?, - fields: name_list(&action_dictionary, "Fields", objects)?, - flags: action_dictionary - .get("Flags") - .map(|value| value.try_number::(objects)) - .transpose()?, + fields: name_list(action_dictionary, "Fields", objects)?, + 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()?, + fields: name_list(action_dictionary, "Fields", objects)?, + flags: action_dictionary.optional_number::("Flags", objects)?, }, "ImportData" => Self::ImportData { file_specification: FileSpecification::from_object( @@ -192,31 +172,22 @@ 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(), - preserve_rb: action_dictionary - .get("PreserveRB") - .map(|value| value.try_boolean(objects)) - .transpose()?, + state: name_list(action_dictionary, "State", objects)?.unwrap_or_default(), + preserve_rb: action_dictionary.optional_boolean("PreserveRB", objects)?, }, "Rendition" => Self::Rendition { - operation: action_dictionary - .get("OP") - .map(|value| value.try_number::(objects)) - .transpose()?, - rendition: Rendition::from_dictionary(&action_dictionary, objects)?, + operation: action_dictionary.optional_number::("OP", objects)?, + rendition: Rendition::from_dictionary(action_dictionary, objects)?, }, "Trans" => Self::Trans { transition: action_dictionary .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)?, + 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 ea84e02f..d97ea384 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::{ @@ -82,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 { @@ -96,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") @@ -118,26 +117,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.to_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..0cc8b1c1 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}; @@ -23,16 +25,9 @@ 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())) - }) - .transpose()?; - let intensity = dictionary - .get("I") - .map(|value| value.try_number::(objects)) + .map(|value| value.try_str(objects).map(BorderEffectStyle::from)) .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..b66b9523 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,22 +25,12 @@ 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| { - value - .try_str(objects) - .map(|name| BorderStyleName::from(name.as_ref())) - }) - .transpose()?; - let dash_pattern = dictionary - .get("D") - .map(|value| value.try_vec_of::(objects)) + .map(|value| value.try_str(objects).map(BorderStyleName::from)) .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..e400ee1f 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; @@ -144,37 +144,37 @@ 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: 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..13bf2743 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,17 +17,10 @@ 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| { - 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/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/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-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/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/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-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 baaa38d8..403b3699 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) - .map(|obj| EncryptionFilter::from(obj.as_ref()))?; + .required_str("Filter", objects) + .map(EncryptionFilter::from)?; - 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..ea70a357 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)? { 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..661f51d2 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, @@ -72,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, @@ -89,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 { @@ -446,12 +441,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,19 +473,17 @@ fn resolve_jbig2_dimensions( dict: &Dictionary, objects: &dyn ObjectResolver, ) -> Result<(u16, u16), FilterError> { + let missing_dimensions_error = + || FilterError::Decompression("JBIG2Decode requires positive Width and Height".into()); + let width = dict - .get("Width") - .ok_or_else(|| FilterError::Decompression("JBIG2Decode requires Width".into()))? - .try_number::(objects)?; + .optional_number::("Width", objects)? + .ok_or_else(missing_dimensions_error)?; let height = dict - .get("Height") - .ok_or_else(|| FilterError::Decompression("JBIG2Decode requires Height".into()))? - .try_number::(objects)?; - + .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)) @@ -505,17 +493,16 @@ 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())) } #[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, @@ -526,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-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..fd8ae566 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,20 +20,13 @@ 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); }; - 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 f374c471..b5924b43 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::{ @@ -105,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)) } @@ -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..7f0e78cd 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,9 +41,8 @@ impl Font { id_allocator: &mut ContentStreamIdAllocator, ) -> Result { // Determine the font subtype from the dictionary. - let subtype = dictionary.get_or_err("Subtype")?.try_str(objects)?; - - match subtype.as_ref() { + let subtype = dictionary.required_str("Subtype", objects)?; + match subtype { "Type0" => { let type0_font = Type0Font::from_dictionary(dictionary, objects)?; Ok(Font::Type0(type0_font)) 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..9d9c752f 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}; @@ -207,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() @@ -253,9 +252,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 +281,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)? { "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..b47b8bcb 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,30 +110,19 @@ 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()?; - 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(), }), @@ -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/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/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..b07fd8cd --- /dev/null +++ b/crates/pdf-object/src/object_lookup.rs @@ -0,0 +1,1306 @@ +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 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` + /// 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(&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>; + + /// 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`]. + /// + /// # 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(&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<&'a str, 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<&'a str, 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<&'a str, 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"), + "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"), + "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")); + } + + #[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 211f6baf..6ec06ba1 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,17 +63,16 @@ 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" { + if type_name == "XRef" { Ok(()) } else { Err(ParserError::InvalidKeyword( "XRef".to_string(), - type_name.into_owned(), + type_name.to_owned(), )) } } @@ -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/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-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..5af4675b 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::{ @@ -243,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] }; @@ -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)?)?; // 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()), @@ -286,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/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..01a7cd74 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,10 +229,10 @@ fn read_xobject_resource( Ok(Some(resource)) } ObjectVariant::Dictionary(dictionary) => { - let subtype = dictionary.get_or_err("Subtype")?.try_str(objects)?; - if subtype.as_ref() != "Form" { + let subtype = dictionary.required_str("Subtype", objects)?; + 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 4fba74dc..719dd8cc 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,9 +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)?; - - match subtype.as_ref() { + match dictionary.required_str("Subtype", objects)? { "Image" => { let soft_mask = resolve_image_soft_mask( dictionary, 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 {