From a6ad3f0ff4127fba1c1321ba6a6aae07b5428b07 Mon Sep 17 00:00:00 2001 From: Velli20 Date: Mon, 19 Oct 2026 01:24:38 +0300 Subject: [PATCH] Read content-stream operands through ArrayContext instead of Operands Operators now decode their operands with the pdf-object-reader decoders, reading in place from the parser's reused operand buffer through ArrayContext::new, so the Operands type and its parallel accessors are gone. To keep this path faster than before, ReadSession::read decodes direct values without copying them into a PdfObject or cloning the traversal state, and ObjectContext borrows the value. ArrayContext gains read_array for fixed numeric runs, and read_all presizes its vector. Co-Authored-By: Claude Opus 5.5 --- crates/pdf-cmap/src/to_unicode.rs | 2 +- crates/pdf-cmap/src/type0/mod.rs | 2 +- .../pdf-color-space/src/color_space_reader.rs | 2 +- .../src/device_n_color_space.rs | 2 +- .../src/indexed_color_space.rs | 2 +- .../src/separation_color_space.rs | 2 +- .../src/clipping_path_operators.rs | 9 +- .../src/color_operators.rs | 131 ++++------- .../src/compatibility_operators.rs | 9 +- .../pdf-content-stream-operators/src/error.rs | 7 - .../src/graphics_state_operators.rs | 60 +++-- .../pdf-content-stream-operators/src/lib.rs | 16 +- .../src/marked_content_operators.rs | 20 +- .../src/operands.rs | 208 ------------------ .../src/operation_map.rs | 6 +- .../src/operator_trait.rs | 17 +- .../src/path_operators.rs | 31 ++- .../src/path_paint_operators.rs | 23 +- .../src/shadings_operators.rs | 9 +- .../src/text_object_operators.rs | 9 +- .../src/text_positioning_operators.rs | 24 +- .../src/text_showing_operators.rs | 81 +++---- .../src/text_state_operators.rs | 47 ++-- .../src/type3_font_operators.rs | 13 +- .../src/xobject_and_image_operators.rs | 15 +- .../pdf-content-stream/src/content_stream.rs | 8 +- crates/pdf-content-stream/src/lib.rs | 2 +- .../src/operator_stream_parser.rs | 123 +++++++---- crates/pdf-filter/src/filter.rs | 6 +- crates/pdf-filter/src/image_payload.rs | 2 +- crates/pdf-font/src/encoding.rs | 2 +- crates/pdf-font/src/pdf_font_descriptor.rs | 2 +- crates/pdf-font/src/pdf_font_metrics.rs | 2 +- crates/pdf-font/src/pdf_font_spec.rs | 2 +- crates/pdf-font/src/simple_font_spec.rs | 2 +- crates/pdf-font/src/type0_font_spec.rs | 6 +- crates/pdf-font/src/type3_font_spec.rs | 2 +- crates/pdf-object-reader/src/context.rs | 98 +++++++-- crates/pdf-object-reader/src/decode.rs | 24 +- .../pdf-object-reader/src/object_variant.rs | 36 +++ crates/pdf-object-reader/src/pdf_object.rs | 20 +- crates/pdf-object-reader/src/reader.rs | 75 ++++--- .../pdf-object-reader/src/resolved_object.rs | 9 +- crates/pdf-optional-content/src/decode.rs | 4 +- .../src/external_graphics_state.rs | 2 +- crates/pdf-resources/src/form.rs | 4 +- crates/pdf-resources/src/pattern.rs | 2 +- crates/pdf-resources/src/resources.rs | 2 +- crates/pdf-resources/src/xobject.rs | 14 +- crates/pdf-shading/src/parse.rs | 1 - 50 files changed, 517 insertions(+), 680 deletions(-) delete mode 100644 crates/pdf-content-stream-operators/src/operands.rs diff --git a/crates/pdf-cmap/src/to_unicode.rs b/crates/pdf-cmap/src/to_unicode.rs index fecbea3d..f092b4c8 100644 --- a/crates/pdf-cmap/src/to_unicode.rs +++ b/crates/pdf-cmap/src/to_unicode.rs @@ -40,7 +40,7 @@ impl FromPdfObject for ToUnicodeMap { /// Propagates malformed CMap streams. Unsupported names and other values decode to an /// empty map, which falls back the same way an absent entry does. fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - let value = context.object().value(); + let value = context.object(); if matches!(value, ObjectVariant::Stream(_)) { return ToUnicodeCMap::from_pdf_object(context).map(Self::CMap); } diff --git a/crates/pdf-cmap/src/type0/mod.rs b/crates/pdf-cmap/src/type0/mod.rs index cd6488bf..0430f617 100644 --- a/crates/pdf-cmap/src/type0/mod.rs +++ b/crates/pdf-cmap/src/type0/mod.rs @@ -91,7 +91,7 @@ impl PdfCMap for Type0EncodingCMap { impl FromPdfObject for Type0EncodingCMap { /// Decodes an embedded stream or predefined name using the active object traversal. fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - match context.object().value() { + match context.object() { ObjectVariant::Stream(_) => { let context = context.stream()?; Ok(Self::from_bytes(context.stream().raw_data())?) diff --git a/crates/pdf-color-space/src/color_space_reader.rs b/crates/pdf-color-space/src/color_space_reader.rs index 1f87f208..e6915309 100644 --- a/crates/pdf-color-space/src/color_space_reader.rs +++ b/crates/pdf-color-space/src/color_space_reader.rs @@ -14,7 +14,7 @@ use crate::{ /// decode-depth limit and cycle detection bound maliciously nested definitions. impl FromPdfObject for ColorSpace { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - let value = context.object().value(); + let value = context.object(); let ObjectVariant::Array(array) = value else { return Ok(Self::try_from(value.try_bytes(context.source())?)?); }; 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 a61191ed..5998cc8a 100644 --- a/crates/pdf-color-space/src/device_n_color_space.rs +++ b/crates/pdf-color-space/src/device_n_color_space.rs @@ -30,7 +30,7 @@ pub struct DeviceNColorSpace { impl FromPdfObject for DeviceNColorSpace { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { let mut array = context.array()?; - let [_, names, _, tint_transform, ..] = array.array().as_slice() else { + let [_, names, _, tint_transform, ..] = array.array() else { return Err(ColorSpaceError::InvalidColorSpace { description: format!( "/DeviceN requires [/DeviceN names alternateSpace tintTransform] with an optional attributes dictionary; found {} element(s)", diff --git a/crates/pdf-color-space/src/indexed_color_space.rs b/crates/pdf-color-space/src/indexed_color_space.rs index e3839a85..edd4abcf 100644 --- a/crates/pdf-color-space/src/indexed_color_space.rs +++ b/crates/pdf-color-space/src/indexed_color_space.rs @@ -29,7 +29,7 @@ pub struct IndexedColorSpace { impl FromPdfObject for IndexedColorSpace { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { let mut array = context.array()?; - let [_, _, _, lookup] = array.array().as_slice() else { + let [_, _, _, lookup] = array.array() else { return Err(ColorSpaceError::InvalidColorSpace { description: format!( "/Indexed requires 4 elements, found {}", diff --git a/crates/pdf-color-space/src/separation_color_space.rs b/crates/pdf-color-space/src/separation_color_space.rs index 2d2f69cd..2ecbe225 100644 --- a/crates/pdf-color-space/src/separation_color_space.rs +++ b/crates/pdf-color-space/src/separation_color_space.rs @@ -23,7 +23,7 @@ pub struct SeparationColorSpace { impl FromPdfObject for SeparationColorSpace { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { let mut array = context.array()?; - let [_, name, _, tint_transform] = array.array().as_slice() else { + let [_, name, _, tint_transform] = array.array() else { return Err(ColorSpaceError::InvalidColorSpace { description: format!( "/Separation requires 4 elements, found {}", diff --git a/crates/pdf-content-stream-operators/src/clipping_path_operators.rs b/crates/pdf-content-stream-operators/src/clipping_path_operators.rs index 4b86be7c..70bc9878 100644 --- a/crates/pdf-content-stream-operators/src/clipping_path_operators.rs +++ b/crates/pdf-content-stream-operators/src/clipping_path_operators.rs @@ -1,10 +1,9 @@ use crate::{ - error::PdfOperatorError, - operands::Operands, - operator_trait::PdfOperator, + operator_trait::{OperandContext, PdfOperator}, pdf_operator_backend::{BackendError, PdfOperatorBackend}, variants::PdfOperatorVariant, }; +use pdf_object_reader::ReadResult; /// Modifies the current clipping path by intersecting it with the current path, using the non-zero winding number rule to determine the region to clip. #[derive(Debug, Clone, PartialEq, Default)] @@ -15,7 +14,7 @@ impl PdfOperator for ClipNonZero { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::ClipNonZero(Self)) } @@ -33,7 +32,7 @@ impl PdfOperator for ClipEvenOdd { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::ClipEvenOdd(Self)) } diff --git a/crates/pdf-content-stream-operators/src/color_operators.rs b/crates/pdf-content-stream-operators/src/color_operators.rs index 5134d3ac..d7bb97d0 100644 --- a/crates/pdf-content-stream-operators/src/color_operators.rs +++ b/crates/pdf-content-stream-operators/src/color_operators.rs @@ -2,12 +2,11 @@ use std::sync::Arc; use crate::{ error::PdfOperatorError, - operands::Operands, - operator_trait::PdfOperator, + operator_trait::{OperandContext, PdfOperator}, pdf_operator_backend::{BackendError, PdfOperatorBackend}, variants::PdfOperatorVariant, }; -use pdf_object_reader::object_resolver::PassthroughResolver; +use pdf_object_reader::{ReadResult, pdf_string::PdfString}; /// Sets the fill color to a grayscale value. /// The gray level applies to subsequent fill operations. @@ -33,10 +32,8 @@ impl PdfOperator for SetGrayFill { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let gray = operands - .take_next()? - .try_number::(&PassthroughResolver)?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let gray = operands.at(0)?; Ok(PdfOperatorVariant::SetGrayFill(Self::new(gray))) } @@ -64,10 +61,8 @@ impl PdfOperator for SetGrayStroke { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let gray = operands - .take_next()? - .try_number::(&PassthroughResolver)?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let gray = operands.at(0)?; Ok(PdfOperatorVariant::SetGrayStroke(Self::new(gray))) } @@ -104,8 +99,8 @@ impl PdfOperator for SetRGBFill { const OPERAND_COUNT: Option = Some(3); - fn read(operands: &mut Operands) -> Result { - let [r, g, b] = operands.try_array_of::()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let [r, g, b] = operands.read_array()?; Ok(PdfOperatorVariant::SetRGBFill(Self::new(r, g, b))) } @@ -137,8 +132,8 @@ impl PdfOperator for SetRGBStroke { const OPERAND_COUNT: Option = Some(3); - fn read(operands: &mut Operands) -> Result { - let [r, g, b] = operands.try_array_of::()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let [r, g, b] = operands.read_array()?; Ok(PdfOperatorVariant::SetRGBStroke(Self::new(r, g, b))) } @@ -177,8 +172,8 @@ impl PdfOperator for SetCMYKFill { const OPERAND_COUNT: Option = Some(4); - fn read(operands: &mut Operands) -> Result { - let [c, m, y, k] = operands.try_array_of::()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let [c, m, y, k] = operands.read_array()?; Ok(PdfOperatorVariant::SetCMYKFill(Self::new(c, m, y, k))) } @@ -212,8 +207,8 @@ impl PdfOperator for SetCMYKStroke { const OPERAND_COUNT: Option = Some(4); - fn read(operands: &mut Operands) -> Result { - let [c, m, y, k] = operands.try_array_of::()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let [c, m, y, k] = operands.read_array()?; Ok(PdfOperatorVariant::SetCMYKStroke(Self::new(c, m, y, k))) } @@ -239,8 +234,8 @@ impl PdfOperator for SetStrokeColorSpace { const NAME: &'static [u8] = b"CS"; const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let name = operands.get_string_bytes()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let name = operands.at::(0)?.bytes; Ok(PdfOperatorVariant::SetStrokeColorSpace(Self::new(name))) } @@ -266,8 +261,8 @@ impl PdfOperator for SetNonStrokingColorSpace { const NAME: &'static [u8] = b"cs"; const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let name = operands.get_string_bytes()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let name = operands.at::(0)?.bytes; Ok(PdfOperatorVariant::SetNonStrokingColorSpace(Self::new( name, ))) @@ -301,28 +296,17 @@ impl PdfOperator for SetStrokingColor { const NAME: &'static [u8] = b"SCN"; const OPERAND_COUNT: Option = None; - fn read(operands: &mut Operands) -> Result { - let mut values = vec![]; - while let Some(value) = operands.peek_next() { - if value.is_name() { - break; - } - let v = operands - .take_next()? - .try_number::(&PassthroughResolver)?; - values.push(v); - } - + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { // The pattern name should come last, after the numeric color components - let pattern = if operands - .peek_next() - .map(|obj| obj.is_name()) - .unwrap_or(false) - { - operands.get_string_bytes().ok() - } else { - None - }; + let count = operands + .array() + .iter() + .take_while(|value| !value.is_name()) + .count(); + let values = (0..count) + .map(|index| operands.at(index)) + .collect::>()?; + let pattern = operands.at::(count).ok().map(|name| name.bytes); Ok(PdfOperatorVariant::SetStrokingColor(Self::new( values, pattern, @@ -361,28 +345,17 @@ impl PdfOperator for SetNonStrokingColor { const NAME: &'static [u8] = b"scn"; const OPERAND_COUNT: Option = None; - fn read(operands: &mut Operands) -> Result { - let mut values = vec![]; - while let Some(value) = operands.peek_next() { - if value.is_name() { - break; - } - let v = operands - .take_next()? - .try_number::(&PassthroughResolver)?; - values.push(v); - } - + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { // The pattern name should come last, after the numeric color components - let pattern = if operands - .peek_next() - .map(|obj| obj.is_name()) - .unwrap_or(false) - { - operands.get_string_bytes().ok() - } else { - None - }; + let count = operands + .array() + .iter() + .take_while(|value| !value.is_name()) + .count(); + let values = (0..count) + .map(|index| operands.at(index)) + .collect::>()?; + let pattern = operands.at::(count).ok().map(|name| name.bytes); Ok(PdfOperatorVariant::SetNonStrokingColor(Self::new( values, pattern, @@ -414,22 +387,16 @@ impl PdfOperator for SetNonStrokingColorSc { Ok(()) } - fn read(operands: &mut Operands) -> Result { - let mut values = vec![]; - while operands.peek_next().is_some() { - values.push( - operands - .take_next()? - .try_number::(&PassthroughResolver)?, - ); - } + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let values: Vec = operands.read_all()?; if values.is_empty() { return Err(PdfOperatorError::OperandCountMismatch { operator: "sc".to_string(), expected: 1, actual: 0, - }); + } + .into()); } Ok(PdfOperatorVariant::SetNonStrokingColor( @@ -454,22 +421,16 @@ impl PdfOperator for SetStrokingColorSc { Ok(()) } - fn read(operands: &mut Operands) -> Result { - let mut values = vec![]; - while operands.peek_next().is_some() { - values.push( - operands - .take_next()? - .try_number::(&PassthroughResolver)?, - ); - } + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let values: Vec = operands.read_all()?; if values.is_empty() { return Err(PdfOperatorError::OperandCountMismatch { operator: "SC".to_string(), expected: 1, actual: 0, - }); + } + .into()); } Ok(PdfOperatorVariant::SetStrokingColor(SetStrokingColor::new( diff --git a/crates/pdf-content-stream-operators/src/compatibility_operators.rs b/crates/pdf-content-stream-operators/src/compatibility_operators.rs index 7b65d15e..19641441 100644 --- a/crates/pdf-content-stream-operators/src/compatibility_operators.rs +++ b/crates/pdf-content-stream-operators/src/compatibility_operators.rs @@ -1,10 +1,9 @@ use crate::{ - error::PdfOperatorError, - operands::Operands, - operator_trait::PdfOperator, + operator_trait::{OperandContext, PdfOperator}, pdf_operator_backend::{BackendError, PdfOperatorBackend}, variants::PdfOperatorVariant, }; +use pdf_object_reader::ReadResult; /// Begins a compatibility section (BX). /// @@ -22,7 +21,7 @@ impl PdfOperator for BeginCompatibility { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::BeginCompatibility(BeginCompatibility)) } @@ -40,7 +39,7 @@ impl PdfOperator for EndCompatibility { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::EndCompatibility(EndCompatibility)) } diff --git a/crates/pdf-content-stream-operators/src/error.rs b/crates/pdf-content-stream-operators/src/error.rs index 058133b6..1045e22e 100644 --- a/crates/pdf-content-stream-operators/src/error.rs +++ b/crates/pdf-content-stream-operators/src/error.rs @@ -12,13 +12,6 @@ pub enum PdfOperatorError { UnsupportedOperator(&'static str), #[error("Unknown PDF operator '{0}'")] UnknownOperator(String), - #[error("Missing operand; expected {expected}")] - OperandMissing { expected: &'static str }, - #[error("Invalid operand type; expected {expected}, found {found}")] - OperandTypeMismatch { - expected: &'static str, - found: &'static str, - }, #[error("Invalid operand value; expected {expected}, found '{found}'")] InvalidOperandValue { expected: &'static str, diff --git a/crates/pdf-content-stream-operators/src/graphics_state_operators.rs b/crates/pdf-content-stream-operators/src/graphics_state_operators.rs index aa50baae..02ceeaff 100644 --- a/crates/pdf-content-stream-operators/src/graphics_state_operators.rs +++ b/crates/pdf-content-stream-operators/src/graphics_state_operators.rs @@ -2,12 +2,11 @@ use std::sync::Arc; use num_traits::FromPrimitive; use pdf_graphics::{LineCap, LineJoin, transform::Transform}; -use pdf_object_reader::object_resolver::PassthroughResolver; +use pdf_object_reader::{ReadResult, pdf_string::PdfString}; use crate::{ error::PdfOperatorError, - operands::Operands, - operator_trait::PdfOperator, + operator_trait::{OperandContext, PdfOperator}, pdf_operator_backend::{BackendError, PdfOperatorBackend}, variants::PdfOperatorVariant, }; @@ -30,10 +29,8 @@ impl PdfOperator for SetLineWidth { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let width = operands - .take_next()? - .try_number::(&PassthroughResolver)?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let width = operands.at(0)?; Ok(PdfOperatorVariant::SetLineWidth(Self::new(width))) } @@ -66,8 +63,8 @@ impl PdfOperator for SetLineCapStyle { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let style = operands.get_u8()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let style = operands.at(0)?; Ok(PdfOperatorVariant::SetLineCapStyle(Self::new(style)?)) } @@ -100,8 +97,8 @@ impl PdfOperator for SetLineJoinStyle { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let style = operands.get_u8()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let style = operands.at(0)?; Ok(PdfOperatorVariant::SetLineJoinStyle(Self::new(style)?)) } @@ -129,10 +126,8 @@ impl PdfOperator for SetMiterLimit { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let limit = operands - .take_next()? - .try_number::(&PassthroughResolver)?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let limit = operands.at(0)?; Ok(PdfOperatorVariant::SetMiterLimit(Self::new(limit))) } @@ -161,11 +156,9 @@ impl PdfOperator for SetDashPattern { const OPERAND_COUNT: Option = Some(2); - fn read(operands: &mut Operands) -> Result { - let array = operands.get_f32_array()?; - let phase = operands - .take_next()? - .try_number::(&PassthroughResolver)?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let array = operands.at(0)?; + let phase = operands.at(1)?; Ok(PdfOperatorVariant::SetDashPattern(Self::new(array, phase))) } @@ -193,10 +186,8 @@ impl PdfOperator for SetFlatnessTolerance { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let tolerance = operands - .take_next()? - .try_number::(&PassthroughResolver)?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let tolerance = operands.at(0)?; Ok(PdfOperatorVariant::SetFlatnessTolerance(Self::new( tolerance, ))) @@ -227,8 +218,8 @@ impl PdfOperator for SetRenderingIntent { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let intent = operands.get_string_bytes()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let intent = operands.at::(0)?.bytes; Ok(PdfOperatorVariant::SetRenderingIntent(Self::new(intent))) } @@ -246,7 +237,7 @@ impl PdfOperator for SaveGraphicsState { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::SaveGraphicsState(Self)) } @@ -264,7 +255,7 @@ impl PdfOperator for RestoreGraphicsState { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::RestoreGraphicsState(Self)) } @@ -293,11 +284,10 @@ impl PdfOperator for ConcatMatrix { const OPERAND_COUNT: Option = Some(6); - fn read(operands: &mut Operands) -> Result { - let [a, b, c, d, e, f] = operands.try_array_of::()?; - Ok(PdfOperatorVariant::ConcatMatrix(Self::new([ - a, b, c, d, e, f, - ]))) + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + Ok(PdfOperatorVariant::ConcatMatrix(Self::new( + operands.read_array()?, + ))) } fn call(&self, backend: &mut T) -> Result<(), BackendError> { @@ -324,8 +314,8 @@ impl PdfOperator for SetGraphicsStateFromDict { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let dict_name = operands.get_string_bytes()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let dict_name = operands.at::(0)?.bytes; Ok(PdfOperatorVariant::SetGraphicsStateFromDict(Self::new( dict_name, ))) diff --git a/crates/pdf-content-stream-operators/src/lib.rs b/crates/pdf-content-stream-operators/src/lib.rs index f8bb033b..ad101c8d 100644 --- a/crates/pdf-content-stream-operators/src/lib.rs +++ b/crates/pdf-content-stream-operators/src/lib.rs @@ -4,7 +4,6 @@ pub mod compatibility_operators; pub mod error; pub mod graphics_state_operators; pub mod marked_content_operators; -pub mod operands; pub mod operation_map; pub mod variants; @@ -24,6 +23,10 @@ pub mod xobject_and_image_operators; extern crate alloc; use std::sync::Arc; +use pdf_object_reader::{ + FromPdfObject, ObjectAccess, ObjectContext, ReadResult, object_variant::ObjectVariant, +}; + /// One operand from a PDF `Tj` or `TJ` text-showing operation. #[derive(Debug, Clone, PartialEq)] pub enum PdfTextItem { @@ -32,3 +35,14 @@ pub enum PdfTextItem { /// Numeric text-position adjustment in thousandths of a text-space unit. Adjustment(f32), } + +/// Decodes a `TJ` array element: strings are shown, any other value is an adjustment. +impl FromPdfObject for PdfTextItem { + #[inline] + fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { + match context.object() { + ObjectVariant::String(value) => Ok(Self::Text(Arc::clone(&value.bytes))), + value => Ok(Self::Adjustment(value.try_number(context.source())?)), + } + } +} diff --git a/crates/pdf-content-stream-operators/src/marked_content_operators.rs b/crates/pdf-content-stream-operators/src/marked_content_operators.rs index 3cb3ce39..0d9f63a6 100644 --- a/crates/pdf-content-stream-operators/src/marked_content_operators.rs +++ b/crates/pdf-content-stream-operators/src/marked_content_operators.rs @@ -1,11 +1,9 @@ use std::sync::Arc; -use pdf_object_reader::object_variant::ObjectVariant; +use pdf_object_reader::{ReadResult, object_variant::ObjectVariant, pdf_string::PdfString}; use crate::{ - error::PdfOperatorError, - operands::Operands, - operator_trait::PdfOperator, + operator_trait::{OperandContext, PdfOperator}, pdf_operator_backend::{BackendError, PdfOperatorBackend}, variants::PdfOperatorVariant, }; @@ -29,8 +27,8 @@ impl PdfOperator for BeginMarkedContent { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let tag = operands.get_string_bytes()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let tag = operands.at::(0)?.bytes; Ok(PdfOperatorVariant::BeginMarkedContent(Self::new(tag))) } @@ -62,10 +60,10 @@ impl PdfOperator for BeginMarkedContentWithProps { const OPERAND_COUNT: Option = Some(2); - fn read(operands: &mut Operands) -> Result { - let tag = operands.get_string_bytes()?; - let properties = match operands.peek_next() { - Some(ObjectVariant::String(_)) => Some(operands.get_string_bytes()?), + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let tag = operands.at::(0)?.bytes; + let properties = match operands.array().get(1) { + Some(ObjectVariant::String(_)) => Some(operands.at::(1)?.bytes), _ => None, }; Ok(PdfOperatorVariant::BeginMarkedContentWithProps(Self::new( @@ -87,7 +85,7 @@ impl PdfOperator for EndMarkedContent { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::EndMarkedContent(Self)) } diff --git a/crates/pdf-content-stream-operators/src/operands.rs b/crates/pdf-content-stream-operators/src/operands.rs deleted file mode 100644 index a70d6158..00000000 --- a/crates/pdf-content-stream-operators/src/operands.rs +++ /dev/null @@ -1,208 +0,0 @@ -use std::sync::Arc; - -use crate::error::PdfOperatorError; -use num_traits::FromPrimitive; -use pdf_object_reader::{ - object_error::ObjectError, object_resolver::PassthroughResolver, object_variant::ObjectVariant, -}; - -/// Reusable storage for the operands belonging to one PDF operator. -/// -/// Values are consumed through a cursor instead of being removed from the front -/// of the backing vector. This keeps reads constant-time and lets the content -/// stream parser reuse the allocation for subsequent operators. -pub struct Operands { - values: Vec, - position: usize, -} - -impl Operands { - /// Creates an empty operand buffer with the requested capacity. - pub fn with_capacity(capacity: usize) -> Self { - Self { - values: Vec::with_capacity(capacity), - position: 0, - } - } - - /// Appends an operand to the buffer. - pub fn push(&mut self, value: ObjectVariant) { - self.values.push(value); - } - - /// Returns the number of operands that have not yet been consumed. - pub fn len(&self) -> usize { - self.values.len().saturating_sub(self.position) - } - - /// Returns whether every operand has been consumed. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Returns the current backing allocation capacity. - pub fn capacity(&self) -> usize { - self.values.capacity() - } - - /// Clears all operands and resets the read cursor while retaining capacity. - pub fn clear(&mut self) { - self.values.clear(); - self.position = 0; - } - - /// Peeks at the next operand without consuming it. - pub fn peek_next(&self) -> Option<&ObjectVariant> { - self.values.get(self.position) - } - - /// Removes and returns the next operand in constant time. - pub(crate) fn take_next(&mut self) -> Result { - let Some(value) = self.values.get_mut(self.position) else { - return Err(PdfOperatorError::OperandMissing { - expected: "an operand", - }); - }; - - self.position = self.position.saturating_add(1); - Ok(std::mem::replace(value, ObjectVariant::Null)) - } - - /// Converts and consumes the next `N` numeric operands. - pub(crate) fn try_array_of(&mut self) -> Result<[T; N], ObjectError> - where - T: FromPrimitive + Copy + Default, - { - if self.len() < N { - return Err(ObjectError::InvalidArrayLength { - expected: N, - found: self.len(), - }); - } - - let mut result = [T::default(); N]; - for output in &mut result { - *output = self - .take_next() - .map_err(|_| ObjectError::InvalidArrayLength { - expected: N, - found: self.len(), - })? - .try_number(&PassthroughResolver)?; - } - Ok(result) - } - - /// Reads the next operand as an `f32`. - pub fn get_f32(&mut self) -> Result { - Ok(self.take_next()?.try_number::(&PassthroughResolver)?) - } - - /// Reads the next PDF string operand as owned bytes. - pub fn get_string_bytes(&mut self) -> Result, PdfOperatorError> { - let object = self.take_next()?; - match object { - ObjectVariant::String(value) => Ok(Arc::clone(&value.bytes)), - other => Err(PdfOperatorError::OperandTypeMismatch { - expected: "a PDF string operand (HexString or LiteralString)", - found: other.name(), - }), - } - } - - /// Reads the next operand as a `u8`. - pub fn get_u8(&mut self) -> Result { - Ok(self.take_next()?.try_number::(&PassthroughResolver)?) - } - - /// Reads the next array operand as numeric `f32` values. - pub fn get_f32_array(&mut self) -> Result, PdfOperatorError> { - let array = self.take_next()?; - Ok(array.try_vec_of::(&PassthroughResolver)?) - } -} - -impl From> for Operands { - fn from(values: Vec) -> Self { - Self { - values, - position: 0, - } - } -} - -#[cfg(test)] -#[allow(clippy::unwrap_used, clippy::expect_used)] -mod tests { - use super::*; - - #[test] - fn operands_are_consumed_in_order_without_reducing_capacity() { - let values = vec![ - ObjectVariant::Integer(1), - ObjectVariant::Real(2.5), - ObjectVariant::Integer(3), - ]; - let original_capacity = values.capacity(); - let mut operands = Operands::from(values); - - assert_eq!(operands.get_f32().expect("first number should parse"), 1.0); - assert_eq!(operands.peek_next(), Some(&ObjectVariant::Real(2.5))); - assert_eq!( - operands - .try_array_of::() - .expect("remaining numbers should parse"), - [2.5, 3.0] - ); - assert!(operands.is_empty()); - assert_eq!(operands.capacity(), original_capacity); - } - - #[test] - fn clear_resets_the_cursor_and_reuses_allocation() { - let mut operands = Operands::with_capacity(6); - operands.push(ObjectVariant::Integer(7)); - let capacity = operands.capacity(); - - assert_eq!(operands.get_u8().expect("number should parse"), 7); - operands.clear(); - operands.push(ObjectVariant::Integer(8)); - - assert_eq!(operands.get_u8().expect("reused number should parse"), 8); - assert!(operands.capacity() >= capacity); - } - - #[test] - fn grouped_conversion_consumes_through_the_invalid_operand() { - let mut operands = Operands::from(vec![ - ObjectVariant::Integer(1), - pdf_object_reader::pdf_string::PdfString::from( - b"not a number", - pdf_object_reader::string_kind::StringKind::Literal, - ), - ObjectVariant::Integer(3), - ]); - - let result = operands.try_array_of::(); - - assert!(matches!( - result, - Err(ObjectError::TypeMismatch("Number", _)) - )); - assert_eq!(operands.peek_next(), Some(&ObjectVariant::Integer(3))); - } - - #[test] - fn grouped_conversion_rejects_too_few_operands_without_consuming() { - let mut operands = Operands::from(vec![ObjectVariant::Integer(1)]); - - assert!(matches!( - operands.try_array_of::(), - Err(ObjectError::InvalidArrayLength { - expected: 2, - found: 1 - }) - )); - assert_eq!(operands.len(), 1); - } -} diff --git a/crates/pdf-content-stream-operators/src/operation_map.rs b/crates/pdf-content-stream-operators/src/operation_map.rs index b95a6942..d436b74e 100644 --- a/crates/pdf-content-stream-operators/src/operation_map.rs +++ b/crates/pdf-content-stream-operators/src/operation_map.rs @@ -5,8 +5,7 @@ use crate::{ error::PdfOperatorError, graphics_state_operators::*, marked_content_operators::*, - operands::Operands, - operator_trait::PdfOperator, + operator_trait::{OperandContext, PdfOperator}, path_operators::*, path_paint_operators::*, shadings_operators::PaintShading, @@ -20,6 +19,7 @@ use crate::{ }; use pdf_image::InlineImage; +use pdf_object_reader::ReadResult; use pdf_parser::parser::PdfParser; /// Custom parser used by operators whose bytes are not represented by the @@ -34,7 +34,7 @@ pub type OperatorParseHook = #[derive(Clone, Copy)] pub struct OpDescriptor { pub operand_count: Option, - pub parser: fn(operands: &mut Operands) -> Result, + pub parser: fn(operands: &mut OperandContext<'_, '_>) -> ReadResult, pub parse_hook: Option, } diff --git a/crates/pdf-content-stream-operators/src/operator_trait.rs b/crates/pdf-content-stream-operators/src/operator_trait.rs index 21868e46..fb4bcce1 100644 --- a/crates/pdf-content-stream-operators/src/operator_trait.rs +++ b/crates/pdf-content-stream-operators/src/operator_trait.rs @@ -1,10 +1,18 @@ +use pdf_object_reader::{ + ArrayContext, ReadResult, ReadSession, object_resolver::PassthroughResolver, +}; + use crate::{ error::PdfOperatorError, - operands::Operands, pdf_operator_backend::{BackendError, PdfOperatorBackend}, variants::PdfOperatorVariant, }; +/// The operands collected before an operator token, read in place through a +/// session that does not resolve references. +pub type OperandContext<'read, 'reader> = + ArrayContext<'read, ReadSession<'reader, PassthroughResolver>>; + /// Represents a PDF content stream operator. /// /// This trait provides metadata about a PDF operator, such as its name @@ -20,13 +28,12 @@ pub trait PdfOperator { /// The number of operands this operator consumes from the operand stack. const OPERAND_COUNT: Option; - /// Reads and consumes the necessary operands from the provided `Operands` - /// slice and constructs the specific `PdfOperatorVariant`. - fn read(operands: &mut Operands) -> Result; + /// Decodes the operator's operands and constructs the specific `PdfOperatorVariant`. + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult; /// Optional custom parsing hook. /// - /// Operators that cannot be parsed solely from the pre-collected `Operands` + /// Operators that cannot be parsed solely from the pre-collected operands /// (for example, operators that need to read additional bytes from the /// content stream or use a non-standard grammar) should implement this /// method so they can perform custom parsing using the low-level diff --git a/crates/pdf-content-stream-operators/src/path_operators.rs b/crates/pdf-content-stream-operators/src/path_operators.rs index 7153ad23..d694dc40 100644 --- a/crates/pdf-content-stream-operators/src/path_operators.rs +++ b/crates/pdf-content-stream-operators/src/path_operators.rs @@ -1,10 +1,9 @@ use crate::{ - error::PdfOperatorError, - operands::Operands, - operator_trait::PdfOperator, + operator_trait::{OperandContext, PdfOperator}, pdf_operator_backend::{BackendError, PdfOperatorBackend}, variants::PdfOperatorVariant, }; +use pdf_object_reader::ReadResult; /// Begins a new subpath by moving the current point to coordinates (x, y), omitting any connecting line segment. /// If the `m` operator is the first operator in a path, it sets the current point. @@ -27,8 +26,8 @@ impl PdfOperator for MoveTo { const OPERAND_COUNT: Option = Some(2); - fn read(operands: &mut Operands) -> Result { - let [x, y] = operands.try_array_of::()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let [x, y] = operands.read_array()?; Ok(PdfOperatorVariant::MoveTo(Self::new(x, y))) } @@ -58,8 +57,8 @@ impl PdfOperator for LineTo { const OPERAND_COUNT: Option = Some(2); - fn read(operands: &mut Operands) -> Result { - let [x, y] = operands.try_array_of::()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let [x, y] = operands.read_array()?; Ok(PdfOperatorVariant::LineTo(Self::new(x, y))) } @@ -105,8 +104,8 @@ impl PdfOperator for CurveTo { const OPERAND_COUNT: Option = Some(6); - fn read(operands: &mut Operands) -> Result { - let [x1, y1, x2, y2, x3, y3] = operands.try_array_of::()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let [x1, y1, x2, y2, x3, y3] = operands.read_array()?; Ok(PdfOperatorVariant::CurveTo(Self::new( x1, y1, x2, y2, x3, y3, ))) @@ -144,8 +143,8 @@ impl PdfOperator for CurveToV { const OPERAND_COUNT: Option = Some(4); - fn read(operands: &mut Operands) -> Result { - let [x2, y2, x3, y3] = operands.try_array_of::()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let [x2, y2, x3, y3] = operands.read_array()?; Ok(PdfOperatorVariant::CurveToV(Self::new(x2, y2, x3, y3))) } @@ -180,8 +179,8 @@ impl PdfOperator for CurveToY { const OPERAND_COUNT: Option = Some(4); - fn read(operands: &mut Operands) -> Result { - let [x1, y1, x3, y3] = operands.try_array_of::()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let [x1, y1, x3, y3] = operands.read_array()?; Ok(PdfOperatorVariant::CurveToY(Self::new(x1, y1, x3, y3))) } @@ -200,7 +199,7 @@ impl PdfOperator for ClosePath { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::ClosePath(Self)) } @@ -240,8 +239,8 @@ impl PdfOperator for Rectangle { const OPERAND_COUNT: Option = Some(4); - fn read(operands: &mut Operands) -> Result { - let [x, y, width, height] = operands.try_array_of::()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let [x, y, width, height] = operands.read_array()?; Ok(PdfOperatorVariant::Rectangle(Self::new( x, y, width, height, ))) diff --git a/crates/pdf-content-stream-operators/src/path_paint_operators.rs b/crates/pdf-content-stream-operators/src/path_paint_operators.rs index 9cfd9529..844e472a 100644 --- a/crates/pdf-content-stream-operators/src/path_paint_operators.rs +++ b/crates/pdf-content-stream-operators/src/path_paint_operators.rs @@ -1,10 +1,9 @@ use crate::{ - error::PdfOperatorError, - operands::Operands, - operator_trait::PdfOperator, + operator_trait::{OperandContext, PdfOperator}, pdf_operator_backend::{BackendError, PdfOperatorBackend}, variants::PdfOperatorVariant, }; +use pdf_object_reader::ReadResult; /// Strokes the current path. #[derive(Debug, Clone, PartialEq, Default)] @@ -15,7 +14,7 @@ impl PdfOperator for StrokePath { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::StrokePath(Self)) } @@ -34,7 +33,7 @@ impl PdfOperator for CloseStrokePath { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::CloseStrokePath(Self)) } @@ -52,7 +51,7 @@ impl PdfOperator for FillPathNonZero { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::FillPathNonZero(Self)) } @@ -70,7 +69,7 @@ impl PdfOperator for FillPathEvenOdd { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::FillPathEvenOdd(Self)) } @@ -89,7 +88,7 @@ impl PdfOperator for FillAndStrokePathNonZero { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::FillAndStrokePathNonZero(Self)) } @@ -108,7 +107,7 @@ impl PdfOperator for FillAndStrokePathEvenOdd { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::FillAndStrokePathEvenOdd(Self)) } @@ -127,7 +126,7 @@ impl PdfOperator for CloseFillAndStrokePathNonZero { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::CloseFillAndStrokePathNonZero(Self)) } @@ -146,7 +145,7 @@ impl PdfOperator for CloseFillAndStrokePathEvenOdd { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::CloseFillAndStrokePathEvenOdd(Self)) } @@ -165,7 +164,7 @@ impl PdfOperator for EndPath { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::EndPath(Self)) } diff --git a/crates/pdf-content-stream-operators/src/shadings_operators.rs b/crates/pdf-content-stream-operators/src/shadings_operators.rs index a2264ef7..9e77019e 100644 --- a/crates/pdf-content-stream-operators/src/shadings_operators.rs +++ b/crates/pdf-content-stream-operators/src/shadings_operators.rs @@ -1,12 +1,11 @@ use std::sync::Arc; use crate::{ - error::PdfOperatorError, - operands::Operands, - operator_trait::PdfOperator, + operator_trait::{OperandContext, PdfOperator}, pdf_operator_backend::{BackendError, PdfOperatorBackend}, variants::PdfOperatorVariant, }; +use pdf_object_reader::{ReadResult, pdf_string::PdfString}; /// Paints the shape and color shading defined by a shading dictionary resource. /// The `sh` operator takes one operand, the name of a shading dictionary. @@ -28,8 +27,8 @@ impl PdfOperator for PaintShading { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let name = operands.get_string_bytes()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let name = operands.at::(0)?.bytes; Ok(PdfOperatorVariant::PaintShading(Self::new(name))) } diff --git a/crates/pdf-content-stream-operators/src/text_object_operators.rs b/crates/pdf-content-stream-operators/src/text_object_operators.rs index 1675d082..e93b4622 100644 --- a/crates/pdf-content-stream-operators/src/text_object_operators.rs +++ b/crates/pdf-content-stream-operators/src/text_object_operators.rs @@ -1,10 +1,9 @@ use crate::{ - error::PdfOperatorError, - operands::Operands, - operator_trait::PdfOperator, + operator_trait::{OperandContext, PdfOperator}, pdf_operator_backend::{BackendError, PdfOperatorBackend}, variants::PdfOperatorVariant, }; +use pdf_object_reader::ReadResult; /// Begins a text object, initializing the text matrix and text line matrix to /// the identity matrix. @@ -16,7 +15,7 @@ impl PdfOperator for BeginText { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::BeginText(Self)) } @@ -34,7 +33,7 @@ impl PdfOperator for EndText { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::EndText(Self)) } diff --git a/crates/pdf-content-stream-operators/src/text_positioning_operators.rs b/crates/pdf-content-stream-operators/src/text_positioning_operators.rs index fb785719..fe84f3b1 100644 --- a/crates/pdf-content-stream-operators/src/text_positioning_operators.rs +++ b/crates/pdf-content-stream-operators/src/text_positioning_operators.rs @@ -1,12 +1,11 @@ use pdf_graphics::transform::Transform; use crate::{ - error::PdfOperatorError, - operands::Operands, - operator_trait::PdfOperator, + operator_trait::{OperandContext, PdfOperator}, pdf_operator_backend::{BackendError, PdfOperatorBackend}, variants::PdfOperatorVariant, }; +use pdf_object_reader::ReadResult; /// Moves to the start of the next line, offset from the start of the current line by (`tx`, `ty`). /// `tx` and `ty` are numbers expressed in unscaled text space units. @@ -31,8 +30,8 @@ impl PdfOperator for MoveTextPosition { const OPERAND_COUNT: Option = Some(2); - fn read(operands: &mut Operands) -> Result { - let [tx, ty] = operands.try_array_of::()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let [tx, ty] = operands.read_array()?; Ok(PdfOperatorVariant::MoveTextPosition(Self::new(tx, ty))) } @@ -63,8 +62,8 @@ impl PdfOperator for MoveTextPositionAndSetLeading { const OPERAND_COUNT: Option = Some(2); - fn read(operands: &mut Operands) -> Result { - let [tx, ty] = operands.try_array_of::()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let [tx, ty] = operands.read_array()?; Ok(PdfOperatorVariant::MoveTextPositionAndSetLeading( Self::new(tx, ty), )) @@ -100,11 +99,10 @@ impl PdfOperator for SetTextMatrix { const OPERAND_COUNT: Option = Some(6); - fn read(operands: &mut Operands) -> Result { - let [a, b, c, d, e, f] = operands.try_array_of::()?; - Ok(PdfOperatorVariant::SetTextMatrix(Self::new([ - a, b, c, d, e, f, - ]))) + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + Ok(PdfOperatorVariant::SetTextMatrix(Self::new( + operands.read_array()?, + ))) } fn call(&self, backend: &mut T) -> Result<(), BackendError> { @@ -123,7 +121,7 @@ impl PdfOperator for MoveToNextLine { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { Ok(PdfOperatorVariant::MoveToNextLine(Self)) } diff --git a/crates/pdf-content-stream-operators/src/text_showing_operators.rs b/crates/pdf-content-stream-operators/src/text_showing_operators.rs index 059ceaec..4f62bb19 100644 --- a/crates/pdf-content-stream-operators/src/text_showing_operators.rs +++ b/crates/pdf-content-stream-operators/src/text_showing_operators.rs @@ -1,16 +1,10 @@ -use std::sync::Arc; - use crate::PdfTextItem; -use crate::operands::Operands; -use crate::operator_trait::PdfOperator; +use crate::operator_trait::{OperandContext, PdfOperator}; use crate::{ - error::PdfOperatorError, pdf_operator_backend::{BackendError, PdfOperatorBackend}, variants::PdfOperatorVariant, }; -use pdf_object_reader::{ - object_error::ObjectError, object_resolver::PassthroughResolver, object_variant::ObjectVariant, -}; +use pdf_object_reader::{ReadResult, pdf_string::PdfString}; /// Shows a text string. #[derive(Debug, Clone, PartialEq)] @@ -31,8 +25,8 @@ impl PdfOperator for ShowText { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let text = operands.get_string_bytes()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let text = operands.at::(0)?.bytes; Ok(PdfOperatorVariant::ShowText(Self::new(PdfTextItem::Text( text, )))) @@ -61,8 +55,8 @@ impl PdfOperator for MoveNextLineShowText { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let text = operands.get_string_bytes()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let text = operands.at::(0)?.bytes; Ok(PdfOperatorVariant::MoveNextLineShowText(Self::new( PdfTextItem::Text(text), ))) @@ -100,9 +94,9 @@ impl PdfOperator for SetSpacingMoveShowText { const OPERAND_COUNT: Option = Some(3); - fn read(operands: &mut Operands) -> Result { - let [word_spacing, char_spacing] = operands.try_array_of::()?; - let text = operands.get_string_bytes()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let [word_spacing, char_spacing] = operands.read_array()?; + let text = operands.at::(2)?.bytes; Ok(PdfOperatorVariant::SetSpacingMoveShowText(Self::new( word_spacing, @@ -137,26 +131,10 @@ impl PdfOperator for ShowTextArray { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let object = operands.take_next()?; - let ObjectVariant::Array(values) = object else { - return Err(ObjectError::TypeMismatch("Array", object.name()).into()); - }; - - let mut elements = Vec::with_capacity(values.len()); - for value in values.iter() { - match value { - ObjectVariant::String(value) => { - elements.push(PdfTextItem::Text(Arc::clone(&value.bytes))); - } - other => { - let amount = other.try_number::(&PassthroughResolver)?; - elements.push(PdfTextItem::Adjustment(amount)); - } - } - } - - Ok(PdfOperatorVariant::ShowTextArray(Self::new(elements))) + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + Ok(PdfOperatorVariant::ShowTextArray(Self::new( + operands.at(0)?, + ))) } fn call(&self, backend: &mut T) -> Result<(), BackendError> { @@ -167,6 +145,13 @@ impl PdfOperator for ShowTextArray { #[cfg(test)] #[allow(clippy::expect_used, clippy::panic)] mod tests { + use std::sync::Arc; + + use pdf_object_reader::{ + ArrayContext, ObjectReader, object_resolver::PassthroughResolver, + object_variant::ObjectVariant, + }; + use super::*; #[test] @@ -175,14 +160,17 @@ mod tests { let original_pointer = text.as_ptr(); - let mut operands = Operands::from(vec![ObjectVariant::String( + let operands = [ObjectVariant::String( pdf_object_reader::pdf_string::PdfString { bytes: text, kind: pdf_object_reader::string_kind::StringKind::Literal, }, - )]); + )]; + let reader = ObjectReader::new(PassthroughResolver); + let mut session = reader.session(); - let operator = ShowText::read(&mut operands).expect("text should parse"); + let operator = ShowText::read(&mut ArrayContext::new(&operands, &mut session)) + .expect("text should parse"); let PdfOperatorVariant::ShowText(operator) = operator else { panic!("expected ShowText variant"); }; @@ -196,27 +184,26 @@ mod tests { #[test] fn spacing_move_show_text_groups_numeric_prefix_before_text() { - let mut operands = Operands::from(vec![ + let operands = [ ObjectVariant::Real(1.5), ObjectVariant::Integer(2), pdf_object_reader::pdf_string::PdfString::from( b"text", pdf_object_reader::string_kind::StringKind::Literal, ), - ]); + ]; + let reader = ObjectReader::new(PassthroughResolver); + let mut session = reader.session(); - let operator = SetSpacingMoveShowText::read(&mut operands) - .expect("mixed operands should parse successfully"); + let operator = + SetSpacingMoveShowText::read(&mut ArrayContext::new(&operands, &mut session)) + .expect("mixed operands should parse successfully"); let PdfOperatorVariant::SetSpacingMoveShowText(operator) = operator else { panic!("expected SetSpacingMoveShowText variant"); }; assert_eq!(operator.word_spacing, 1.5); assert_eq!(operator.char_spacing, 2.0); - assert_eq!( - operator.text, - PdfTextItem::Text(std::sync::Arc::from(&b"text"[..])) - ); - assert!(operands.is_empty()); + assert_eq!(operator.text, PdfTextItem::Text(Arc::from(&b"text"[..]))); } } diff --git a/crates/pdf-content-stream-operators/src/text_state_operators.rs b/crates/pdf-content-stream-operators/src/text_state_operators.rs index 8a50efea..ec9f6ab5 100644 --- a/crates/pdf-content-stream-operators/src/text_state_operators.rs +++ b/crates/pdf-content-stream-operators/src/text_state_operators.rs @@ -2,14 +2,13 @@ use std::sync::Arc; use crate::{ error::PdfOperatorError, - operands::Operands, - operator_trait::PdfOperator, + operator_trait::{OperandContext, PdfOperator}, pdf_operator_backend::{BackendError, PdfOperatorBackend}, variants::PdfOperatorVariant, }; use num_traits::FromPrimitive; use pdf_graphics::TextRenderingMode; -use pdf_object_reader::object_resolver::PassthroughResolver; +use pdf_object_reader::{ReadResult, pdf_string::PdfString}; /// Sets the character spacing, `Tc`, which is a number expressed in unscaled text space units. #[derive(Debug, Clone, PartialEq)] @@ -29,10 +28,8 @@ impl PdfOperator for SetCharacterSpacing { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let spacing = operands - .take_next()? - .try_number::(&PassthroughResolver)?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let spacing = operands.at(0)?; Ok(PdfOperatorVariant::SetCharacterSpacing(Self::new(spacing))) } @@ -60,10 +57,8 @@ impl PdfOperator for SetWordSpacing { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let spacing = operands - .take_next()? - .try_number::(&PassthroughResolver)?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let spacing = operands.at(0)?; Ok(PdfOperatorVariant::SetWordSpacing(Self::new(spacing))) } @@ -91,10 +86,8 @@ impl PdfOperator for SetHorizontalScaling { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let scale = operands - .take_next()? - .try_number::(&PassthroughResolver)?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let scale = operands.at(0)?; Ok(PdfOperatorVariant::SetHorizontalScaling(Self::new(scale))) } @@ -128,10 +121,8 @@ impl PdfOperator for SetLeading { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let leading = operands - .take_next()? - .try_number::(&PassthroughResolver)?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let leading = operands.at(0)?; Ok(PdfOperatorVariant::SetLeading(Self::new(leading))) } @@ -171,11 +162,9 @@ impl PdfOperator for SetFont { const OPERAND_COUNT: Option = Some(2); - fn read(operands: &mut Operands) -> Result { - let name = operands.get_string_bytes()?; - let size = operands - .take_next()? - .try_number::(&PassthroughResolver)?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let name = operands.at::(0)?.bytes; + let size = operands.at(1)?; Ok(PdfOperatorVariant::SetFont(Self::new(name, size))) } @@ -209,8 +198,8 @@ impl PdfOperator for SetRenderingMode { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let mode = operands.get_u8()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let mode = operands.at(0)?; Ok(PdfOperatorVariant::SetRenderingMode(Self::new(mode)?)) } @@ -238,10 +227,8 @@ impl PdfOperator for SetTextRise { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let rise = operands - .take_next()? - .try_number::(&PassthroughResolver)?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let rise = operands.at(0)?; Ok(PdfOperatorVariant::SetTextRise(Self::new(rise))) } diff --git a/crates/pdf-content-stream-operators/src/type3_font_operators.rs b/crates/pdf-content-stream-operators/src/type3_font_operators.rs index d24562a6..f7c77044 100644 --- a/crates/pdf-content-stream-operators/src/type3_font_operators.rs +++ b/crates/pdf-content-stream-operators/src/type3_font_operators.rs @@ -1,10 +1,9 @@ use crate::{ - error::PdfOperatorError, - operands::Operands, - operator_trait::PdfOperator, + operator_trait::{OperandContext, PdfOperator}, pdf_operator_backend::{BackendError, PdfOperatorBackend}, variants::PdfOperatorVariant, }; +use pdf_object_reader::ReadResult; #[derive(Debug, Clone, PartialEq, Default)] pub struct SetCharWidthAndBoundingBox { @@ -27,8 +26,8 @@ impl PdfOperator for SetCharWidthAndBoundingBox { const OPERAND_COUNT: Option = Some(6); - fn read(operands: &mut Operands) -> Result { - let [wx, wy, llx, lly, urx, ury] = operands.try_array_of::()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let [wx, wy, llx, lly, urx, ury] = operands.read_array()?; Ok(PdfOperatorVariant::SetCharWidthAndBoundingBox(Self { wx, @@ -58,8 +57,8 @@ impl PdfOperator for SetCharWidth { const OPERAND_COUNT: Option = Some(2); - fn read(operands: &mut Operands) -> Result { - let [wx, wy] = operands.try_array_of::()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let [wx, wy] = operands.read_array()?; Ok(PdfOperatorVariant::SetCharWidth(Self { wx, wy })) } diff --git a/crates/pdf-content-stream-operators/src/xobject_and_image_operators.rs b/crates/pdf-content-stream-operators/src/xobject_and_image_operators.rs index aa165882..4abc4f72 100644 --- a/crates/pdf-content-stream-operators/src/xobject_and_image_operators.rs +++ b/crates/pdf-content-stream-operators/src/xobject_and_image_operators.rs @@ -1,13 +1,14 @@ use std::sync::Arc; use pdf_image::InlineImage; -use pdf_object_reader::{ObjectReader, object_resolver::PassthroughResolver}; +use pdf_object_reader::{ + ObjectReader, ReadResult, object_resolver::PassthroughResolver, pdf_string::PdfString, +}; use pdf_parser::parser::PdfParser; use crate::{ error::PdfOperatorError, - operands::Operands, - operator_trait::PdfOperator, + operator_trait::{OperandContext, PdfOperator}, pdf_operator_backend::{BackendError, PdfOperatorBackend}, variants::PdfOperatorVariant, }; @@ -31,8 +32,8 @@ impl PdfOperator for InvokeXObject { const OPERAND_COUNT: Option = Some(1); - fn read(operands: &mut Operands) -> Result { - let name = operands.get_string_bytes()?; + fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { + let name = operands.at::(0)?.bytes; Ok(PdfOperatorVariant::InvokeXObject(Self::new(name))) } @@ -46,8 +47,8 @@ impl PdfOperator for InlineImage { const OPERAND_COUNT: Option = Some(0); - fn read(_operands: &mut Operands) -> Result { - Err(PdfOperatorError::UnsupportedOperator("BI")) + fn read(_operands: &mut OperandContext<'_, '_>) -> ReadResult { + Err(PdfOperatorError::UnsupportedOperator("BI").into()) } fn parse<'a>( diff --git a/crates/pdf-content-stream/src/content_stream.rs b/crates/pdf-content-stream/src/content_stream.rs index 08e6d980..770fdfdf 100644 --- a/crates/pdf-content-stream/src/content_stream.rs +++ b/crates/pdf-content-stream/src/content_stream.rs @@ -1,8 +1,8 @@ use crate::operator_stream_parser::OperatorStreamParser; use pdf_content_stream_operators::{error::PdfOperatorError, variants::PdfOperatorVariant}; use pdf_object_reader::{ - FromPdfObject, ObjectAccess, ObjectContext, ReadResult, object_kind::ObjectKind, - stream::StreamObject, + FromPdfObject, ObjectAccess, ObjectContext, ObjectReader, ReadResult, object_kind::ObjectKind, + object_resolver::PassthroughResolver, stream::StreamObject, }; /// Represents one materialized PDF content stream as parsed operators plus its @@ -59,7 +59,9 @@ impl ContentStream { input: &[u8], operators: &mut Vec, ) -> Result<(), PdfOperatorError> { - let mut parser = OperatorStreamParser::new(input, operators); + let reader = ObjectReader::new(PassthroughResolver); + let mut session = reader.session(); + let mut parser = OperatorStreamParser::new(input, &mut session, operators); while parser.parse_next_item()? {} Ok(()) } diff --git a/crates/pdf-content-stream/src/lib.rs b/crates/pdf-content-stream/src/lib.rs index 09484185..917d8f0c 100644 --- a/crates/pdf-content-stream/src/lib.rs +++ b/crates/pdf-content-stream/src/lib.rs @@ -4,4 +4,4 @@ mod operator_stream_parser; pub use content_stream::ContentStream; pub use pdf_object_reader::ContentStreamIdAllocator; -pub use pdf_content_stream_operators::{operands::Operands, operator_trait::PdfOperator}; +pub use pdf_content_stream_operators::operator_trait::PdfOperator; diff --git a/crates/pdf-content-stream/src/operator_stream_parser.rs b/crates/pdf-content-stream/src/operator_stream_parser.rs index 279ebe56..99ad39a3 100644 --- a/crates/pdf-content-stream/src/operator_stream_parser.rs +++ b/crates/pdf-content-stream/src/operator_stream_parser.rs @@ -1,32 +1,40 @@ -use pdf_object_reader::object_resolver::PassthroughResolver; +use pdf_object_reader::{ + ArrayContext, ReadSession, object_resolver::PassthroughResolver, object_variant::ObjectVariant, +}; use pdf_parser::error::ParserError; use pdf_parser::parser::PdfParser; use pdf_tokenizer::error::TokenizerError; use pdf_content_stream_operators::{ error::PdfOperatorError, - operands::Operands, operation_map::{OpDescriptor, get_operation_descriptor}, variants::PdfOperatorVariant, }; /// Incrementally parses one content stream into PDF operators while reusing the /// same operand buffer across operator boundaries. -pub(super) struct OperatorStreamParser<'a, 'out> { +pub(super) struct OperatorStreamParser<'a, 'out, 'session, 'reader> { parser: PdfParser<'a>, - operands: Operands, + session: &'session mut ReadSession<'reader, PassthroughResolver>, + operands: Vec, out: &'out mut Vec, } -impl<'a, 'out> OperatorStreamParser<'a, 'out> { +impl<'a, 'out, 'session, 'reader> OperatorStreamParser<'a, 'out, 'session, 'reader> { /// Creates a parser for a single borrowed content stream. /// - /// The operand buffer is preallocated because PDF operators typically have - /// a small fixed number of operands. - pub(super) fn new(input: &'a [u8], out: &'out mut Vec) -> Self { + /// Operators read their operands in place through `session`. The operand + /// buffer is preallocated because PDF operators typically have a small + /// fixed number of operands. + pub(super) fn new( + input: &'a [u8], + session: &'session mut ReadSession<'reader, PassthroughResolver>, + out: &'out mut Vec, + ) -> Self { Self { parser: PdfParser::from(input), - operands: Operands::with_capacity(6), + session, + operands: Vec::with_capacity(6), out, } } @@ -111,10 +119,10 @@ impl<'a, 'out> OperatorStreamParser<'a, 'out> { if let Some(operator) = parse_hook(&mut self.parser)? { Some(operator) } else { - parse_operator(&descriptor, &mut self.operands)? + parse_operator(&descriptor, &self.operands, self.session) } } else { - parse_operator(&descriptor, &mut self.operands)? + parse_operator(&descriptor, &self.operands, self.session) }; self.push_if_parsed(parsed); @@ -177,23 +185,22 @@ fn is_truncated_operand_error(error: &PdfOperatorError) -> bool { } } -/// Parses a single operator with its operands. +/// Parses a single operator from its operands, read in place. /// -/// Looks up the operator descriptor by name and validates the operand count -/// before parsing. The operand buffer retains its allocation for the next -/// operator, including when parsing fails. +/// Validates the operand count before parsing. An operator whose operands do +/// not decode is dropped, like any other malformed item. fn parse_operator( descriptor: &OpDescriptor, - operands: &mut Operands, -) -> Result, PdfOperatorError> { + operands: &[ObjectVariant], + session: &mut ReadSession<'_, PassthroughResolver>, +) -> Option { if let Some(required_count) = descriptor.operand_count && operands.len() != required_count { - return Ok(None); + return None; } - let operator = (descriptor.parser)(operands)?; - Ok(Some(operator)) + (descriptor.parser)(&mut ArrayContext::new(operands, session)).ok() } #[cfg(test)] @@ -202,7 +209,7 @@ mod tests { use pdf_content_stream_operators::{ graphics_state_operators::RestoreGraphicsState, variants::PdfOperatorVariant, }; - use pdf_object_reader::object_variant::ObjectVariant; + use pdf_object_reader::ObjectReader; use super::*; @@ -244,42 +251,40 @@ mod tests { } #[test] - fn parse_operator_reuses_operand_buffer_after_success() { + fn parse_operator_reads_operands_in_place() { let descriptor = get_operation_descriptor(b"m").expect("operator descriptor should exist"); - let mut operands = - Operands::from(vec![ObjectVariant::Integer(10), ObjectVariant::Integer(20)]); - let original_capacity = operands.capacity(); + let operands = vec![ObjectVariant::Integer(10), ObjectVariant::Integer(20)]; + let reader = ObjectReader::new(PassthroughResolver); - let operator = parse_operator(&descriptor, &mut operands).expect("operator should parse"); + let operator = parse_operator(&descriptor, &operands, &mut reader.session()); assert!(matches!(operator, Some(PdfOperatorVariant::MoveTo(_)))); - assert!(operands.is_empty()); - assert!(operands.capacity() >= original_capacity); + assert_eq!(operands.len(), 2); } #[test] fn parse_operator_skips_malformed_fixed_arity_operator_without_consuming_buffer() { let descriptor = get_operation_descriptor(b"m").expect("operator descriptor should exist"); - let mut operands = Operands::from(vec![ + let operands = vec![ ObjectVariant::Integer(1), ObjectVariant::Integer(2), ObjectVariant::Integer(3), - ]); - let original_capacity = operands.capacity(); + ]; + let reader = ObjectReader::new(PassthroughResolver); - let operator = parse_operator(&descriptor, &mut operands) - .expect("malformed operator should be skipped"); + let operator = parse_operator(&descriptor, &operands, &mut reader.session()); assert!(operator.is_none()); assert_eq!(operands.len(), 3); - assert_eq!(operands.peek_next(), Some(&ObjectVariant::Integer(1))); - assert_eq!(operands.capacity(), original_capacity); + assert_eq!(operands.first(), Some(&ObjectVariant::Integer(1))); } #[test] fn parse_next_item_skips_unknown_operator_and_clears_pending_operands() { let mut out = Vec::new(); - let mut parser = OperatorStreamParser::new(b"unknown q", &mut out); + let reader = ObjectReader::new(PassthroughResolver); + let mut session = reader.session(); + let mut parser = OperatorStreamParser::new(b"unknown q", &mut session, &mut out); parser.operands.push(ObjectVariant::Integer(1)); assert!( @@ -305,8 +310,13 @@ mod tests { #[test] fn parse_next_item_uses_parse_hook_and_resumes_after_inline_image() { let mut out = Vec::new(); - let mut parser = - OperatorStreamParser::new(b"BI /W 1 /H 1 /BPC 8 /CS /G ID x\nEI Q", &mut out); + let reader = ObjectReader::new(PassthroughResolver); + let mut session = reader.session(); + let mut parser = OperatorStreamParser::new( + b"BI /W 1 /H 1 /BPC 8 /CS /G ID x\nEI Q", + &mut session, + &mut out, + ); assert!( parser @@ -346,7 +356,9 @@ mod tests { #[test] fn parse_next_item_stops_cleanly_on_truncated_trailing_operand() { let mut out = Vec::new(); - let mut parser = OperatorStreamParser::new(b"0 j 0 J [ ", &mut out); + let reader = ObjectReader::new(PassthroughResolver); + let mut session = reader.session(); + let mut parser = OperatorStreamParser::new(b"0 j 0 J [ ", &mut session, &mut out); while parser .parse_next_item() @@ -368,7 +380,9 @@ mod tests { #[test] fn parse_next_item_recovers_from_leading_invalid_delimiter() { let mut out = Vec::new(); - let mut parser = OperatorStreamParser::new(b") q", &mut out); + let reader = ObjectReader::new(PassthroughResolver); + let mut session = reader.session(); + let mut parser = OperatorStreamParser::new(b") q", &mut session, &mut out); while parser .parse_next_item() @@ -386,7 +400,9 @@ mod tests { #[test] fn parse_next_item_recovers_from_corrupt_operand_before_valid_operator() { let mut out = Vec::new(); - let mut parser = OperatorStreamParser::new(b"[ ) 0 J", &mut out); + let reader = ObjectReader::new(PassthroughResolver); + let mut session = reader.session(); + let mut parser = OperatorStreamParser::new(b"[ ) 0 J", &mut session, &mut out); while parser .parse_next_item() @@ -404,7 +420,9 @@ mod tests { #[test] fn numeric_operands_recover_from_top_level_reference_syntax() { let mut out = Vec::new(); - let mut parser = OperatorStreamParser::new(b"1 0 R q", &mut out); + let reader = ObjectReader::new(PassthroughResolver); + let mut session = reader.session(); + let mut parser = OperatorStreamParser::new(b"1 0 R q", &mut session, &mut out); while parser .parse_next_item() @@ -421,7 +439,10 @@ mod tests { #[test] fn generic_operands_keep_nested_indirect_references() { let mut out = Vec::new(); - let mut parser = OperatorStreamParser::new(b"/Tag << /Ref 5 0 R >> BDC EMC", &mut out); + let reader = ObjectReader::new(PassthroughResolver); + let mut session = reader.session(); + let mut parser = + OperatorStreamParser::new(b"/Tag << /Ref 5 0 R >> BDC EMC", &mut session, &mut out); while parser .parse_next_item() @@ -442,7 +463,9 @@ mod tests { #[test] fn parse_next_item_recovers_from_invalid_operator_operand_value() { let mut out = Vec::new(); - let mut parser = OperatorStreamParser::new(b"9 J q", &mut out); + let reader = ObjectReader::new(PassthroughResolver); + let mut session = reader.session(); + let mut parser = OperatorStreamParser::new(b"9 J q", &mut session, &mut out); let original_capacity = parser.operands.capacity(); while parser @@ -462,7 +485,10 @@ mod tests { #[test] fn parse_next_item_recovers_from_malformed_inline_image() { let mut out = Vec::new(); - let mut parser = OperatorStreamParser::new(b"BI /W 1 /H 1 ID abc Q", &mut out); + let reader = ObjectReader::new(PassthroughResolver); + let mut session = reader.session(); + let mut parser = + OperatorStreamParser::new(b"BI /W 1 /H 1 ID abc Q", &mut session, &mut out); while parser .parse_next_item() @@ -480,7 +506,9 @@ mod tests { #[test] fn parse_next_item_skips_untokenized_delimiters() { let mut out = Vec::new(); - let mut parser = OperatorStreamParser::new(b"} { q", &mut out); + let reader = ObjectReader::new(PassthroughResolver); + let mut session = reader.session(); + let mut parser = OperatorStreamParser::new(b"} { q", &mut session, &mut out); while parser .parse_next_item() @@ -498,8 +526,11 @@ mod tests { #[test] fn parse_next_item_reads_inline_image_ending_directly_after_binary_data() { let mut out = Vec::new(); + let reader = ObjectReader::new(PassthroughResolver); + let mut session = reader.session(); let mut parser = OperatorStreamParser::new( b"BI /IM true /W 8 /H 1 /F /CCF /DP << /K -1 /Columns 8 >> ID \x01}\x10EI\nQ", + &mut session, &mut out, ); diff --git a/crates/pdf-filter/src/filter.rs b/crates/pdf-filter/src/filter.rs index e75a7eb7..a6357f0f 100644 --- a/crates/pdf-filter/src/filter.rs +++ b/crates/pdf-filter/src/filter.rs @@ -99,7 +99,7 @@ impl Filters { /// Reads a `/Filter` value: one filter name or an array applied in order. impl FromPdfObject for Filters { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - let filters = match context.object().value() { + let filters = match context.object() { ObjectVariant::Array(_) => context.array()?.read_all()?, _ => vec![Filter::from_pdf_object(context)?], }; @@ -110,9 +110,7 @@ impl FromPdfObject for Filters { /// Reads a filter name, accepting a malformed string where the name is expected. impl FromPdfObject for Filter { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - Ok(Self::from( - context.object().value().try_bytes(context.source())?, - )) + Ok(Self::from(context.object().try_bytes(context.source())?)) } } diff --git a/crates/pdf-filter/src/image_payload.rs b/crates/pdf-filter/src/image_payload.rs index 36d77fcc..576b03f3 100644 --- a/crates/pdf-filter/src/image_payload.rs +++ b/crates/pdf-filter/src/image_payload.rs @@ -116,7 +116,7 @@ enum DecodeParms { impl FromPdfObject for DecodeParms { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - match context.object().value() { + match context.object() { dictionary @ ObjectVariant::Dictionary(_) => Ok(Self::Shared(dictionary.clone())), ObjectVariant::Array(array) => Ok(Self::PerFilter(array.clone())), _ => Err(ObjectReadError::TypeMismatch { diff --git a/crates/pdf-font/src/encoding.rs b/crates/pdf-font/src/encoding.rs index 44d4ea03..0d7936d0 100644 --- a/crates/pdf-font/src/encoding.rs +++ b/crates/pdf-font/src/encoding.rs @@ -72,7 +72,7 @@ impl TryFrom for Encoding { impl FromPdfObject for Encoding { /// Reads a base-encoding name or an encoding dictionary in the active traversal. fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - if !matches!(context.object().value(), ObjectVariant::Dictionary(_)) { + if !matches!(context.object(), ObjectVariant::Dictionary(_)) { let name = Arc::<[u8]>::from_pdf_object(context)?; return Ok(Self::try_from(BaseEncoding::from(name.as_ref()))?); } diff --git a/crates/pdf-font/src/pdf_font_descriptor.rs b/crates/pdf-font/src/pdf_font_descriptor.rs index 8ab3b7df..be158435 100644 --- a/crates/pdf-font/src/pdf_font_descriptor.rs +++ b/crates/pdf-font/src/pdf_font_descriptor.rs @@ -9,7 +9,7 @@ impl FromPdfObject for PdfFontDescriptor { /// Reads a FontDescriptor dictionary, not the enclosing font dictionary, tolerating /// malformed optional metadata. fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - let value = context.object().value(); + let value = context.object(); let mut context = context.dictionary()?; Ok(Self { metadata: context.read(value)?, diff --git a/crates/pdf-font/src/pdf_font_metrics.rs b/crates/pdf-font/src/pdf_font_metrics.rs index 6429bf62..928b1acd 100644 --- a/crates/pdf-font/src/pdf_font_metrics.rs +++ b/crates/pdf-font/src/pdf_font_metrics.rs @@ -28,7 +28,7 @@ impl FromPdfObject for PdfMetrics { /// retain their font-domain source; optional descriptor hints remain lenient. /// The explicit table itself is read by [`crate::glyph_widths_map::GlyphWidthsMap`]. fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - let value = context.object().value(); + let value = context.object(); let mut context = context.dictionary()?; let subtype: Arc<[u8]> = context.required(b"Subtype")?; let default_width = match subtype.as_ref() { diff --git a/crates/pdf-font/src/pdf_font_spec.rs b/crates/pdf-font/src/pdf_font_spec.rs index c7ef97bd..7ac0e507 100644 --- a/crates/pdf-font/src/pdf_font_spec.rs +++ b/crates/pdf-font/src/pdf_font_spec.rs @@ -40,7 +40,7 @@ pub enum PdfFontSpec { impl FromPdfObject for PdfFontSpec { /// Reads a font resource, substituting a Standard 14 font on parsing failure. fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - let value = context.object().value(); + let value = context.object(); let mut context = context.dictionary()?; // Keep recovery at the resource boundary: a missing or unsupported subtype, or a // concrete font that fails to parse, falls back to a Standard 14 font. diff --git a/crates/pdf-font/src/simple_font_spec.rs b/crates/pdf-font/src/simple_font_spec.rs index b5a1357e..acc477e1 100644 --- a/crates/pdf-font/src/simple_font_spec.rs +++ b/crates/pdf-font/src/simple_font_spec.rs @@ -34,7 +34,7 @@ impl FromPdfObject for SimpleFontSpec { /// Decodes the concrete font dictionary without whole-font substitution, choosing its /// program and encoding policy from Subtype. fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - let value = context.object().value(); + let value = context.object(); let mut context = context.dictionary()?; let subtype: Arc<[u8]> = context.required(b"Subtype")?; let base_font: Option> = context.lenient(b"BaseFont"); diff --git a/crates/pdf-font/src/type0_font_spec.rs b/crates/pdf-font/src/type0_font_spec.rs index afabf81d..95cf5058 100644 --- a/crates/pdf-font/src/type0_font_spec.rs +++ b/crates/pdf-font/src/type0_font_spec.rs @@ -78,7 +78,7 @@ impl CidToGidMap { impl FromPdfObject for CidToGidMap { /// Decodes a stream into its `u16` table, ignoring a trailing odd byte. fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - let ObjectVariant::Stream(stream) = context.object().value() else { + let ObjectVariant::Stream(stream) = context.object() else { return Ok(Self::Identity); }; let (pairs, _) = stream.raw_data().as_chunks::<2>(); @@ -91,7 +91,7 @@ impl FromPdfObject for CidToGidMap { impl FromPdfObject for CidFontKind { /// Decodes the descendant dictionary's Subtype name into a supported CID kind. fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - match context.object().value().try_bytes(context.source())? { + match context.object().try_bytes(context.source())? { b"CIDFontType0" => Ok(Self::Type0), b"CIDFontType2" => Ok(Self::Type2), other => Err(FontError::UnsupportedCidFontSubtype { @@ -105,7 +105,7 @@ impl FromPdfObject for CidFontKind { impl FromPdfObject for CidFontSpec { /// Parses the descendant within its parent font's traversal. fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - let value = context.object().value(); + let value = context.object(); let mut context = context.dictionary()?; let kind: CidFontKind = context.required(b"Subtype")?; let base_font: Option> = context.lenient(b"BaseFont"); diff --git a/crates/pdf-font/src/type3_font_spec.rs b/crates/pdf-font/src/type3_font_spec.rs index aa11983a..6e3cde8b 100644 --- a/crates/pdf-font/src/type3_font_spec.rs +++ b/crates/pdf-font/src/type3_font_spec.rs @@ -78,7 +78,7 @@ impl FromPdfObject for Type3FontSpec { /// Decodes a Type 3 dictionary without whole-font substitution; absent glyph widths /// default to zero. fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - let value = context.object().value(); + let value = context.object(); let mut context = context.dictionary()?; let font_matrix = context .dictionary() diff --git a/crates/pdf-object-reader/src/context.rs b/crates/pdf-object-reader/src/context.rs index 07f2e3c2..596354fb 100644 --- a/crates/pdf-object-reader/src/context.rs +++ b/crates/pdf-object-reader/src/context.rs @@ -11,10 +11,8 @@ use crate::handle::ObjectHandle; use crate::object_id::ObjectId; use crate::object_kind::ObjectKind; use crate::object_variant::ObjectVariant; -use crate::pdf_array::PdfArray; use crate::pdf_name::PdfName; use crate::reader::ObjectAccess; -use crate::resolved_object::ResolvedObject; use crate::{dictionary::Dictionary, stream::StreamObject}; use std::collections::HashMap; @@ -24,16 +22,17 @@ use std::collections::HashMap; /// this context. Shape conversions preserve that same traversal and report a /// type mismatch instead of silently accepting an unrelated object kind. pub struct ObjectContext<'read, A: ObjectAccess + ?Sized> { - object: &'read ResolvedObject, + object: &'read ObjectVariant, access: &'read mut A, } impl<'read, A: ObjectAccess + ?Sized> ObjectContext<'read, A> { - /// Binds a validated direct object to an active access implementation. + /// Binds a direct object to an active access implementation. /// + /// The object must already be resolved: a top-level reference is never followed. /// Custom access implementations must establish their traversal scope /// before constructing the context and retain it until decoding returns. - pub fn new(object: &'read ResolvedObject, access: &'read mut A) -> Self { + pub fn new(object: &'read ObjectVariant, access: &'read mut A) -> Self { Self { object, access } } @@ -41,7 +40,7 @@ impl<'read, A: ObjectAccess + ?Sized> ObjectContext<'read, A> { /// /// The borrow outlives `&self`, so the object can be inspected while the /// context reads children. - pub fn object(&self) -> &'read ResolvedObject { + pub fn object(&self) -> &'read ObjectVariant { self.object } @@ -57,7 +56,7 @@ impl<'read, A: ObjectAccess + ?Sized> ObjectContext<'read, A> { /// Requires an array and transfers the traversal into its context. pub fn array(self) -> ReadResult> { - match self.object.value() { + match self.object { ObjectVariant::Array(array) => Ok(ArrayContext { array, access: self.access, @@ -71,7 +70,7 @@ impl<'read, A: ObjectAccess + ?Sized> ObjectContext<'read, A> { /// Requires a stream and transfers the traversal into its context. pub fn stream(self) -> ReadResult> { - match self.object.value() { + match self.object { ObjectVariant::Stream(stream) => Ok(StreamContext { stream, access: self.access, @@ -251,31 +250,45 @@ impl<'read, A: ObjectAccess + ?Sized> DictionaryContext<'read, A> { /// Provides bounds-checked typed array reads in source order. pub struct ArrayContext<'read, A: ObjectAccess + ?Sized> { - array: &'read PdfArray, + array: &'read [ObjectVariant], access: &'read mut A, } impl<'read, A: ObjectAccess + ?Sized> ArrayContext<'read, A> { - /// Returns the immutable array for length checks or custom inspection. + /// Binds a direct sequence of values the caller already holds to an active access + /// implementation. + /// + /// Elements are read within the access's current traversal, as if the array had + /// been reached through it. + pub fn new(array: &'read [ObjectVariant], access: &'read mut A) -> Self { + Self { array, access } + } + + /// Returns the immutable elements for length checks or custom inspection. /// /// The borrow outlives `&self`, so elements can be held while the context reads them. - pub fn array(&self) -> &'read PdfArray { + pub fn array(&self) -> &'read [ObjectVariant] { self.array } /// Reads an element, reporting an out-of-bounds error for an invalid index. + #[inline] pub fn at(&mut self, index: usize) -> ReadResult { - let result = match self.array.get(index) { - Some(object) => self.access.read(object), - None => Err(ObjectReadError::ArrayIndexOutOfBounds { - index, - length: self.array.len(), - }), + let Some(object) = self.array.get(index) else { + return Err(ObjectReadError::At { + location: ReadLocation::ArrayIndex(index), + source: Box::new(ObjectReadError::ArrayIndexOutOfBounds { + index, + length: self.array.len(), + }), + }); }; - result.map_err(|source| ObjectReadError::At { - location: ReadLocation::ArrayIndex(index), - source: Box::new(source), - }) + self.access + .read(object) + .map_err(|source| ObjectReadError::At { + location: ReadLocation::ArrayIndex(index), + source: Box::new(source), + }) } /// Reads an element through typed caching while retaining array diagnostics. @@ -298,7 +311,43 @@ impl<'read, A: ObjectAccess + ?Sized> ArrayContext<'read, A> { /// Eagerly decodes every element in source order, stopping at the first error. pub fn read_all(&mut self) -> ReadResult> { - (0..self.array.len()).map(|index| self.at(index)).collect() + let mut values = Vec::with_capacity(self.array.len()); + for (index, object) in self.array.iter().enumerate() { + values.push( + self.access + .read(object) + .map_err(|source| ObjectReadError::At { + location: ReadLocation::ArrayIndex(index), + source: Box::new(source), + })?, + ); + } + Ok(values) + } + + /// Decodes the first `N` elements in source order, validating the length first. + /// + /// Trailing elements are ignored, like the `[T; N]` decoder. + #[inline] + pub fn read_array(&mut self) -> ReadResult<[T; N]> { + if self.array.len() < N { + return Err(crate::object_error::ObjectError::InvalidArrayLength { + expected: N, + found: self.array.len(), + } + .into()); + } + let mut values = std::array::from_fn(|_| T::default()); + for (index, (value, object)) in values.iter_mut().zip(self.array).enumerate() { + *value = self + .access + .read(object) + .map_err(|source| ObjectReadError::At { + location: ReadLocation::ArrayIndex(index), + source: Box::new(source), + })?; + } + Ok(values) } } @@ -344,7 +393,10 @@ macro_rules! context_access { self.access.read_shared(object) } /// Resolves a raw child without entering its eager decoding scope. - pub fn resolve(&mut self, object: &ObjectVariant) -> ReadResult { + pub fn resolve( + &mut self, + object: &ObjectVariant, + ) -> ReadResult { self.access.resolve(object) } diff --git a/crates/pdf-object-reader/src/decode.rs b/crates/pdf-object-reader/src/decode.rs index 8e9506b5..121f7c11 100644 --- a/crates/pdf-object-reader/src/decode.rs +++ b/crates/pdf-object-reader/src/decode.rs @@ -61,7 +61,7 @@ macro_rules! value_decoder { fn from_pdf_object( context: ObjectContext<'_, impl ObjectAccess + ?Sized>, ) -> ReadResult { - match context.object().value() { + match context.object() { ObjectVariant::$variant(value) => Ok(value.clone()), _ => Err(ObjectReadError::TypeMismatch { expected: ObjectKind::$kind, @@ -78,7 +78,7 @@ value_decoder!(StreamObject, Stream, Stream); impl FromPdfObject for () { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - match context.object().value() { + match context.object() { ObjectVariant::Null => Ok(()), _ => Err(ObjectReadError::TypeMismatch { expected: ObjectKind::Null, @@ -94,7 +94,7 @@ impl FromPdfObject for f64 { reason = "PDF integer-to-real conversion intentionally permits rounding" )] fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - match context.object().value() { + match context.object() { ObjectVariant::Real(value) => Ok(*value), ObjectVariant::Integer(value) => Ok(*value as f64), _ => Err(ObjectReadError::TypeMismatch { @@ -107,13 +107,13 @@ impl FromPdfObject for f64 { impl FromPdfObject for PdfObject { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - Ok(context.object().object().clone()) + Ok(PdfObject::new(context.object().clone())) } } impl FromPdfObject for ResolvedObject { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - Ok(context.object().clone()) + Self::try_from(PdfObject::new(context.object().clone())) } } @@ -138,8 +138,9 @@ impl FromPdfObject for Vec { } impl FromPdfObject for PdfString { + #[inline] fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - match context.object().value() { + match context.object() { ObjectVariant::String(value) => Ok(value.clone()), _ => Err(ObjectReadError::TypeMismatch { expected: ObjectKind::String, @@ -151,7 +152,7 @@ impl FromPdfObject for PdfString { impl FromPdfObject for PdfArray { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - match context.object().value() { + match context.object() { ObjectVariant::Array(value) => Ok(value.clone()), _ => Err(ObjectReadError::TypeMismatch { expected: ObjectKind::Array, @@ -166,11 +167,12 @@ macro_rules! number_decoder { $( impl FromPdfObject for $type { /// Decodes a resolved number with the model's checked numeric conversion. + #[inline] fn from_pdf_object( context: ObjectContext<'_, impl ObjectAccess + ?Sized>, ) -> ReadResult { // The context has already resolved references and owns their scope. - Ok(context.object().value().try_number(context.source())?) + Ok(context.object().try_number(context.source())?) } } )+ @@ -181,7 +183,7 @@ number_decoder!(f32, i32, i64, u8, u16, u32, usize); impl FromPdfObject for bool { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - match context.object().value() { + match context.object() { ObjectVariant::Boolean(value) => Ok(*value), _ => Err(ObjectReadError::TypeMismatch { expected: ObjectKind::Boolean, @@ -223,8 +225,6 @@ impl FromPdfObject for [T; N] { /// Use `PdfName` or `PdfString` when the PDF object kind must be enforced. impl FromPdfObject for std::sync::Arc<[u8]> { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - Ok(Self::from( - context.object().value().try_bytes(context.source())?, - )) + Ok(Self::from(context.object().try_bytes(context.source())?)) } } diff --git a/crates/pdf-object-reader/src/object_variant.rs b/crates/pdf-object-reader/src/object_variant.rs index cb21ef0d..988d08f5 100644 --- a/crates/pdf-object-reader/src/object_variant.rs +++ b/crates/pdf-object-reader/src/object_variant.rs @@ -1,5 +1,7 @@ //! PDF object variants and checked accessors. +use crate::error::ObjectReadError; +use crate::object_kind::ObjectKind; use crate::pdf_array::PdfArray; use crate::pdf_string::PdfString; use crate::string_kind::StringKind; @@ -207,6 +209,40 @@ impl ObjectVariant { } } + /// Returns the runtime kind of this value. + pub fn kind(&self) -> ObjectKind { + match self { + ObjectVariant::Null => ObjectKind::Null, + ObjectVariant::Boolean(_) => ObjectKind::Boolean, + ObjectVariant::Integer(_) => ObjectKind::Integer, + ObjectVariant::Real(_) => ObjectKind::Real, + ObjectVariant::String(value) if value.kind() == StringKind::Name => ObjectKind::Name, + ObjectVariant::String(_) => ObjectKind::String, + ObjectVariant::Array(_) => ObjectKind::Array, + ObjectVariant::Dictionary(_) => ObjectKind::Dictionary, + ObjectVariant::Stream(_) => ObjectKind::Stream, + ObjectVariant::Reference(_) => ObjectKind::Reference, + ObjectVariant::Trailer(_) => ObjectKind::Trailer, + ObjectVariant::CrossReferenceTable(_) => ObjectKind::CrossReferenceTable, + ObjectVariant::EndOfFile => ObjectKind::EndOfFile, + } + } + + /// Returns the dictionary of a direct dictionary or stream. + /// + /// No reference is followed, so no resolver is borrowed and the dictionary + /// lives as long as this value. + pub fn dictionary(&self) -> Result<&Dictionary, ObjectReadError> { + match self { + ObjectVariant::Dictionary(dictionary) => Ok(dictionary), + ObjectVariant::Stream(stream) => Ok(&stream.dictionary), + _ => Err(ObjectReadError::TypeMismatch { + expected: ObjectKind::Dictionary, + actual: self.kind(), + }), + } + } + /// Returns `true` if this value is a Name object. pub fn is_name(&self) -> bool { matches!(self, ObjectVariant::String(value) if value.kind() == StringKind::Name) diff --git a/crates/pdf-object-reader/src/pdf_object.rs b/crates/pdf-object-reader/src/pdf_object.rs index 73da2fd6..7e0a49b4 100644 --- a/crates/pdf-object-reader/src/pdf_object.rs +++ b/crates/pdf-object-reader/src/pdf_object.rs @@ -25,24 +25,6 @@ impl PdfObject { /// Returns the runtime kind of this object. pub fn kind(&self) -> ObjectKind { - match self.value() { - ObjectVariant::Null => ObjectKind::Null, - ObjectVariant::Boolean(_) => ObjectKind::Boolean, - ObjectVariant::Integer(_) => ObjectKind::Integer, - ObjectVariant::Real(_) => ObjectKind::Real, - ObjectVariant::String(value) - if value.kind() == crate::string_kind::StringKind::Name => - { - ObjectKind::Name - } - ObjectVariant::String(_) => ObjectKind::String, - ObjectVariant::Array(_) => ObjectKind::Array, - ObjectVariant::Dictionary(_) => ObjectKind::Dictionary, - ObjectVariant::Stream(_) => ObjectKind::Stream, - ObjectVariant::Reference(_) => ObjectKind::Reference, - ObjectVariant::Trailer(_) => ObjectKind::Trailer, - ObjectVariant::CrossReferenceTable(_) => ObjectKind::CrossReferenceTable, - ObjectVariant::EndOfFile => ObjectKind::EndOfFile, - } + self.value().kind() } } diff --git a/crates/pdf-object-reader/src/reader.rs b/crates/pdf-object-reader/src/reader.rs index 30dee011..2b0597f9 100644 --- a/crates/pdf-object-reader/src/reader.rs +++ b/crates/pdf-object-reader/src/reader.rs @@ -310,39 +310,22 @@ where ResolvedObject::try_from(object) } + #[inline] fn read(&mut self, object: &ObjectVariant) -> ReadResult { if self.decode_depth >= self.reader.limits.max_decode_depth().get() { return Err(ObjectReadError::DecodeDepthExceeded { maximum: self.reader.limits.max_decode_depth().get(), }); } - // The child owns its traversal scope. Parent state remains intact on errors and unwinding. - let mut child = ReadSession { - reader: self.reader, - active_path: self.active_path.clone(), - active_ids: self.active_ids.clone(), - decode_depth: self.decode_depth.saturating_add(1), + let ObjectVariant::Reference(id) = object else { + // A direct value adds nothing to the active path, so it decodes in this + // session without copying the value or the traversal state. + self.decode_depth = self.decode_depth.saturating_add(1); + let result = T::from_pdf_object(ObjectContext::new(object, self)); + self.decode_depth = self.decode_depth.saturating_sub(1); + return result; }; - let mut object = PdfObject::new(object.clone()); - while let ObjectVariant::Reference(id) = object.value() { - let id = *id; - if !child.active_ids.insert(id) { - return Err(ObjectReadError::CyclicReference { - repeated: id, - path: child.active_path, - }); - } - if child.active_path.len() >= child.reader.limits.max_reference_depth().get() { - return Err(ObjectReadError::ReferenceDepthExceeded { - maximum: child.reader.limits.max_reference_depth().get(), - path: child.active_path, - }); - } - child.active_path.push(id); - object = child.load_object(id)?; - } - let object = ResolvedObject::try_from(object)?; - T::from_pdf_object(ObjectContext::new(&object, &mut child)) + self.read_reference(*id) } fn read_indirect(&mut self, id: ObjectId) -> ReadResult { @@ -383,3 +366,43 @@ where self.read_shared(&ObjectVariant::Reference(id)) } } + +impl ReadSession<'_, S, C> +where + S: ObjectSource, + C: ObjectCache, +{ + /// Follows a reference chain and decodes its target in a child traversal scope. + /// + /// Kept out of [`ObjectAccess::read`] so the direct-value path stays small enough to inline. + fn read_reference(&mut self, mut id: ObjectId) -> ReadResult { + // The child owns its traversal scope. Parent state remains intact on errors and unwinding. + let mut child = ReadSession { + reader: self.reader, + active_path: self.active_path.clone(), + active_ids: self.active_ids.clone(), + decode_depth: self.decode_depth.saturating_add(1), + }; + let object = loop { + if !child.active_ids.insert(id) { + return Err(ObjectReadError::CyclicReference { + repeated: id, + path: child.active_path, + }); + } + if child.active_path.len() >= child.reader.limits.max_reference_depth().get() { + return Err(ObjectReadError::ReferenceDepthExceeded { + maximum: child.reader.limits.max_reference_depth().get(), + path: child.active_path, + }); + } + child.active_path.push(id); + let object = child.load_object(id)?; + match object.value() { + ObjectVariant::Reference(next) => id = *next, + _ => break object, + } + }; + T::from_pdf_object(ObjectContext::new(object.value(), &mut child)) + } +} diff --git a/crates/pdf-object-reader/src/resolved_object.rs b/crates/pdf-object-reader/src/resolved_object.rs index 464ffce8..17da7e3a 100644 --- a/crates/pdf-object-reader/src/resolved_object.rs +++ b/crates/pdf-object-reader/src/resolved_object.rs @@ -44,13 +44,6 @@ impl ResolvedObject { /// The object is already direct, so no resolver is borrowed and the dictionary /// lives only as long as this object. pub fn dictionary(&self) -> Result<&Dictionary, ObjectReadError> { - match self.value() { - ObjectVariant::Dictionary(dictionary) => Ok(dictionary), - ObjectVariant::Stream(stream) => Ok(&stream.dictionary), - _ => Err(ObjectReadError::TypeMismatch { - expected: ObjectKind::Dictionary, - actual: self.kind(), - }), - } + self.value().dictionary() } } diff --git a/crates/pdf-optional-content/src/decode.rs b/crates/pdf-optional-content/src/decode.rs index 231f47a1..152fe401 100644 --- a/crates/pdf-optional-content/src/decode.rs +++ b/crates/pdf-optional-content/src/decode.rs @@ -37,7 +37,7 @@ impl FromPdfObject for OptionalContentConfiguration { /// Reads a `/BaseState` name. An absent entry is the caller's `On` default. impl FromPdfObject for OptionalContentBaseState { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - match context.object().value().try_bytes(context.source())? { + match context.object().try_bytes(context.source())? { b"ON" => Ok(Self::On), b"OFF" => Ok(Self::Off), other => Err(OptionalContentError::InvalidEntry { @@ -52,7 +52,7 @@ impl FromPdfObject for OptionalContentBaseState { /// Reads a `/P` policy name. An absent entry is the caller's `AnyOn` default. impl FromPdfObject for OptionalContentPolicy { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - match context.object().value().try_bytes(context.source())? { + match context.object().try_bytes(context.source())? { b"AnyOn" => Ok(Self::AnyOn), b"AllOn" => Ok(Self::AllOn), b"AnyOff" => Ok(Self::AnyOff), diff --git a/crates/pdf-resources/src/external_graphics_state.rs b/crates/pdf-resources/src/external_graphics_state.rs index e4d90361..b4cfd84f 100644 --- a/crates/pdf-resources/src/external_graphics_state.rs +++ b/crates/pdf-resources/src/external_graphics_state.rs @@ -108,7 +108,7 @@ struct BlendModeEntry(BlendMode); impl FromPdfObject for BlendModeEntry { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { let objects = context.source(); - let value = context.object().value(); + let value = context.object(); let ObjectVariant::Array(names) = value else { return Ok(Self(BlendMode::from(value.try_bytes(objects)?))); }; diff --git a/crates/pdf-resources/src/form.rs b/crates/pdf-resources/src/form.rs index 6a857b3d..0189aa48 100644 --- a/crates/pdf-resources/src/form.rs +++ b/crates/pdf-resources/src/form.rs @@ -24,13 +24,13 @@ impl FromPdfObject for FormXObject { let object = context.object(); // Both stream-backed and dictionary-only forms expose the same metadata. let mut context = context.dictionary()?; - let content_stream = match object.value() { + let content_stream = match object { // Dictionary-only forms still receive an ID, but have no drawing operators. ObjectVariant::Dictionary(_) => ContentStream { operators: Vec::new(), id: context.content_stream_ids().next_id()?, }, - _ => context.read::(object.value())?, + _ => context.read::(object)?, }; let bbox = context.dictionary().optional_bbox(context.source())?; let matrix = context.dictionary().optional_matrix(context.source())?; diff --git a/crates/pdf-resources/src/pattern.rs b/crates/pdf-resources/src/pattern.rs index e70a6e54..969542e4 100644 --- a/crates/pdf-resources/src/pattern.rs +++ b/crates/pdf-resources/src/pattern.rs @@ -133,7 +133,7 @@ impl FromPdfObject for Pattern { resources: context .optional_shared::(b"Resources")? .unwrap_or_else(|| ObjectHandle::from(Resources::default())), - content_stream: context.read::(object.value())?, + content_stream: context.read::(object)?, }), PatternType::Shading => Ok(Pattern::Shading { shading: context.required::(b"Shading")?, diff --git a/crates/pdf-resources/src/resources.rs b/crates/pdf-resources/src/resources.rs index 485c987c..659ea35c 100644 --- a/crates/pdf-resources/src/resources.rs +++ b/crates/pdf-resources/src/resources.rs @@ -37,7 +37,7 @@ pub struct Resources { impl FromPdfObject for Resources { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - if matches!(context.object().value(), ObjectVariant::Stream(_)) { + if matches!(context.object(), ObjectVariant::Stream(_)) { let mut stream = context.stream()?; Self::decode_dictionary(stream.dictionary()) } else { diff --git a/crates/pdf-resources/src/xobject.rs b/crates/pdf-resources/src/xobject.rs index 40cfd940..30c8a151 100644 --- a/crates/pdf-resources/src/xobject.rs +++ b/crates/pdf-resources/src/xobject.rs @@ -50,16 +50,16 @@ impl FromPdfObject for Resource { fn from_pdf_object( mut context: ObjectContext<'_, impl ObjectAccess + ?Sized>, ) -> ReadResult { - let value = context.object().value(); + let value = context.object(); let subtype = match value { ObjectVariant::Stream(_) => Some(context.read::(value)?), _ => context.read::(value).ok(), }; match subtype { None => Self::read_font(&mut context), - Some(XObjectSubtype::Form) => Ok(Self::from( - context.read::(context.object().value())?, - )), + Some(XObjectSubtype::Form) => { + Ok(Self::from(context.read::(context.object())?)) + } Some(XObjectSubtype::Image) => Self::read_image(&mut context), } } @@ -69,7 +69,7 @@ impl Resource { /// Reads a font, keeping the resources a Type 3 font's glyph procedures paint with. fn read_font(context: &mut ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { let object = context.object(); - let font: PdfFontSpec = context.read(object.value())?; + let font: PdfFontSpec = context.read(object)?; let resources = match object.dictionary()?.get(b"Resources") { Some(resources) if font.is_type3() => Some(context.read_shared(resources)?), _ => None, @@ -91,8 +91,8 @@ impl Resource { { return Ok(Self::UnavailableImage); } - let ObjectVariant::Stream(stream) = object.value() else { - return Err(ObjectError::TypeMismatch("Stream", object.value().name()).into()); + let ObjectVariant::Stream(stream) = object else { + return Err(ObjectError::TypeMismatch("Stream", object.name()).into()); }; let soft_mask = Self::read_soft_mask_image(context)?; match decode_image_xobject(dictionary, stream, context.access(), soft_mask.as_deref()) { diff --git a/crates/pdf-shading/src/parse.rs b/crates/pdf-shading/src/parse.rs index 5af74e35..c0c9bf27 100644 --- a/crates/pdf-shading/src/parse.rs +++ b/crates/pdf-shading/src/parse.rs @@ -31,7 +31,6 @@ impl FromPdfObject for Shading { .required_number::(b"ShadingType", context.source())? .try_into()?; let access = context.access(); - let object = object.value(); match shading_type { ShadingType::FunctionBased => {