From 05b3fda29bf5ff1c1c9f7be17d7ed642f072d87a Mon Sep 17 00:00:00 2001 From: Velli20 Date: Tue, 20 Oct 2026 22:15:05 +0300 Subject: [PATCH] Inline operator operands into PdfOperatorVariant Most operator structs only held decoded operands, assigned them in new, and forwarded them to one backend method. They become struct variants of PdfOperatorVariant, read in one expression in PdfOperatorVariant::read and dispatched in PdfOperatorVariant::call. Line cap, line join and text rendering mode are read through new FromPdfObject decoders for the pdf-graphics enums, which ExtGState now uses for /LC and /LJ in place of its own validation. The stroking and non-stroking color operators share one ColorOperands type. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 - crates/pdf-canvas/src/canvas_text_ops.rs | 4 +- crates/pdf-canvas/src/pdf_canvas.rs | 12 +- .../pdf-content-stream-operators/Cargo.toml | 1 - .../src/color_operators.rs | 384 +------- .../src/graphics_state_operators.rs | 275 ------ .../pdf-content-stream-operators/src/lib.rs | 8 - .../src/marked_content_operators.rs | 28 - .../src/path_operators.rs | 237 ----- .../src/shadings_operators.rs | 36 - .../src/text_positioning_operators.rs | 103 --- .../src/text_showing_operators.rs | 188 ---- .../src/text_state_operators.rs | 230 ----- .../src/type3_font_operators.rs | 70 -- .../src/variants.rs | 823 ++++++++++++++---- .../src/xobject_and_image_operators.rs | 86 -- .../pdf-content-stream/src/content_stream.rs | 9 +- .../src/operator_stream_parser.rs | 11 +- crates/pdf-content-stream/tests/parser_api.rs | 9 +- crates/pdf-object-reader/src/decode.rs | 29 + crates/pdf-object-reader/src/object_error.rs | 3 + crates/pdf-resources/src/error.rs | 6 - .../src/external_graphics_state.rs | 15 +- 23 files changed, 757 insertions(+), 1811 deletions(-) delete mode 100644 crates/pdf-content-stream-operators/src/graphics_state_operators.rs delete mode 100644 crates/pdf-content-stream-operators/src/path_operators.rs delete mode 100644 crates/pdf-content-stream-operators/src/shadings_operators.rs delete mode 100644 crates/pdf-content-stream-operators/src/text_positioning_operators.rs delete mode 100644 crates/pdf-content-stream-operators/src/text_showing_operators.rs delete mode 100644 crates/pdf-content-stream-operators/src/text_state_operators.rs delete mode 100644 crates/pdf-content-stream-operators/src/type3_font_operators.rs delete mode 100644 crates/pdf-content-stream-operators/src/xobject_and_image_operators.rs diff --git a/Cargo.lock b/Cargo.lock index 8891cb5c..4475a802 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2191,7 +2191,6 @@ name = "pdf-content-stream-operators" version = "0.1.0" dependencies = [ "bytes", - "num-traits", "pdf-graphics", "pdf-image", "pdf-object-reader", diff --git a/crates/pdf-canvas/src/canvas_text_ops.rs b/crates/pdf-canvas/src/canvas_text_ops.rs index bb029c77..cbbe895a 100644 --- a/crates/pdf-canvas/src/canvas_text_ops.rs +++ b/crates/pdf-canvas/src/canvas_text_ops.rs @@ -239,8 +239,8 @@ impl PdfCanvas<'_, B> { let mut filter = |operator: &PdfOperatorVariant| { matches!( operator, - PdfOperatorVariant::SetCharWidth(_) - | PdfOperatorVariant::SetCharWidthAndBoundingBox(_) + PdfOperatorVariant::SetCharWidth { .. } + | PdfOperatorVariant::SetCharWidthAndBoundingBox { .. } ) }; diff --git a/crates/pdf-canvas/src/pdf_canvas.rs b/crates/pdf-canvas/src/pdf_canvas.rs index 89ee22a1..e68934ab 100644 --- a/crates/pdf-canvas/src/pdf_canvas.rs +++ b/crates/pdf-canvas/src/pdf_canvas.rs @@ -423,12 +423,12 @@ impl<'a, B: CanvasBackend> PdfCanvas<'a, B> { op, PdfOperatorVariant::SetNonStrokingColor(_) | PdfOperatorVariant::SetStrokingColor(_) - | PdfOperatorVariant::SetGrayFill(_) - | PdfOperatorVariant::SetGrayStroke(_) - | PdfOperatorVariant::SetRGBFill(_) - | PdfOperatorVariant::SetRGBStroke(_) - | PdfOperatorVariant::SetCMYKFill(_) - | PdfOperatorVariant::SetCMYKStroke(_) + | PdfOperatorVariant::SetGrayFill { .. } + | PdfOperatorVariant::SetGrayStroke { .. } + | PdfOperatorVariant::SetRGBFill { .. } + | PdfOperatorVariant::SetRGBStroke { .. } + | PdfOperatorVariant::SetCMYKFill { .. } + | PdfOperatorVariant::SetCMYKStroke { .. } ) }; diff --git a/crates/pdf-content-stream-operators/Cargo.toml b/crates/pdf-content-stream-operators/Cargo.toml index ce6d0522..83910634 100644 --- a/crates/pdf-content-stream-operators/Cargo.toml +++ b/crates/pdf-content-stream-operators/Cargo.toml @@ -15,4 +15,3 @@ pdf-object-reader = { path = "../pdf-object-reader" } pdf-image = { path = "../pdf-image" } pdf-graphics = { path = "../pdf-graphics" } thiserror = "2.0.12" -num-traits = "0.2.19" diff --git a/crates/pdf-content-stream-operators/src/color_operators.rs b/crates/pdf-content-stream-operators/src/color_operators.rs index 340e520b..f9a24f28 100644 --- a/crates/pdf-content-stream-operators/src/color_operators.rs +++ b/crates/pdf-content-stream-operators/src/color_operators.rs @@ -1,288 +1,22 @@ use std::sync::Arc; -use crate::{ - OperandContext, - error::PdfOperatorError, - pdf_operator_backend::{BackendError, PdfOperatorBackend}, -}; +use crate::{OperandContext, error::PdfOperatorError}; use pdf_object_reader::{ReadResult, pdf_string::PdfString}; -/// Sets the fill color to a grayscale value. -/// The gray level applies to subsequent fill operations. +/// The operands of a color-setting operator whose color space takes any number +/// of components (`SC`, `SCN`, `sc`, `scn`). #[derive(Debug, Clone, PartialEq)] -pub struct SetGrayFill { - /// The gray level, a value between 0.0 (black) and 1.0 (white). - gray: f32, -} - -impl SetGrayFill { - pub fn new(gray: f32) -> Self { - Self { gray } - } - - /// Returns the gray component. - pub const fn gray(&self) -> f32 { - self.gray - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let gray = operands.at(0)?; - Ok(Self::new(gray)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_non_stroking_gray(self.gray) - } -} - -/// Sets the stroke color to a grayscale value. -/// The gray level applies to subsequent stroke operations. -#[derive(Debug, Clone, PartialEq)] -pub struct SetGrayStroke { - /// The gray level, a value between 0.0 (black) and 1.0 (white). - gray: f32, -} - -impl SetGrayStroke { - pub fn new(gray: f32) -> Self { - Self { gray } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let gray = operands.at(0)?; - Ok(Self::new(gray)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_stroking_gray(self.gray) - } -} - -/// Sets the fill color to an RGB (Red, Green, Blue) value. -/// The RGB color applies to subsequent fill operations. -#[derive(Debug, Clone, PartialEq)] -pub struct SetRGBFill { - /// The red component, a value between 0.0 and 1.0. - r: f32, - /// The green component, a value between 0.0 and 1.0. - g: f32, - /// The blue component, a value between 0.0 and 1.0. - b: f32, -} - -impl SetRGBFill { - pub fn new(r: f32, g: f32, b: f32) -> Self { - Self { r, g, b } - } - - /// Returns the RGB components. - pub const fn components(&self) -> [f32; 3] { - [self.r, self.g, self.b] - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let [r, g, b] = operands.read_array()?; - Ok(Self::new(r, g, b)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_non_stroking_rgb(self.r, self.g, self.b) - } -} - -/// Sets the stroke color to an RGB (Red, Green, Blue) value. -/// The RGB color applies to subsequent stroke operations. -#[derive(Debug, Clone, PartialEq)] -pub struct SetRGBStroke { - /// The red component, a value between 0.0 and 1.0. - r: f32, - /// The green component, a value between 0.0 and 1.0. - g: f32, - /// The blue component, a value between 0.0 and 1.0. - b: f32, -} - -impl SetRGBStroke { - pub fn new(r: f32, g: f32, b: f32) -> Self { - Self { r, g, b } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let [r, g, b] = operands.read_array()?; - Ok(Self::new(r, g, b)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_stroking_rgb(self.r, self.g, self.b) - } -} - -/// Sets the fill color to a CMYK (Cyan, Magenta, Yellow, Black/Key) value. -/// The CMYK color applies to subsequent fill operations. -#[derive(Debug, Clone, PartialEq)] -pub struct SetCMYKFill { - /// The cyan component, a value between 0.0 and 1.0. - c: f32, - /// The magenta component, a value between 0.0 and 1.0. - m: f32, - /// The yellow component, a value between 0.0 and 1.0. - y: f32, - /// The black (key) component, a value between 0.0 and 1.0. - k: f32, -} - -impl SetCMYKFill { - pub fn new(c: f32, m: f32, y: f32, k: f32) -> Self { - Self { c, m, y, k } - } - - /// Returns the CMYK components. - pub const fn components(&self) -> [f32; 4] { - [self.c, self.m, self.y, self.k] - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let [c, m, y, k] = operands.read_array()?; - Ok(Self::new(c, m, y, k)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_non_stroking_cmyk(self.c, self.m, self.y, self.k) - } -} - -/// Sets the stroke color to a CMYK (Cyan, Magenta, Yellow, Black/Key) value. -/// The CMYK color applies to subsequent stroke operations. -#[derive(Debug, Clone, PartialEq)] -pub struct SetCMYKStroke { - /// The cyan component, a value between 0.0 and 1.0. - c: f32, - /// The magenta component, a value between 0.0 and 1.0. - m: f32, - /// The yellow component, a value between 0.0 and 1.0. - y: f32, - /// The black (key) component, a value between 0.0 and 1.0. - k: f32, -} - -impl SetCMYKStroke { - pub fn new(c: f32, m: f32, y: f32, k: f32) -> Self { - Self { c, m, y, k } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let [c, m, y, k] = operands.read_array()?; - Ok(Self::new(c, m, y, k)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_stroking_cmyk(self.c, self.m, self.y, self.k) - } -} - -/// Sets the stroke color space value. -#[derive(Debug, Clone, PartialEq)] -pub struct SetStrokeColorSpace { - /// The name of the color space. - name: Arc<[u8]>, -} - -impl SetStrokeColorSpace { - pub fn new(name: Arc<[u8]>) -> Self { - Self { name } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let name = operands.at::(0)?.bytes; - Ok(Self::new(name)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_stroking_color_space(&self.name) - } -} - -/// Sets the non-stroking (fill) color space value. -#[derive(Debug, Clone, PartialEq)] -pub struct SetNonStrokingColorSpace { - /// The name of the color space. - name: Arc<[u8]>, -} - -impl SetNonStrokingColorSpace { - pub fn new(name: Arc<[u8]>) -> Self { - Self { name } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let name = operands.at::(0)?.bytes; - Ok(Self::new(name)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_non_stroking_color_space(&self.name) - } -} - -/// Sets the stroking color when the color space requires -/// multiple color components. -#[derive(Debug, Clone, PartialEq)] -pub struct SetStrokingColor { +pub struct ColorOperands { /// Color component values. - components: Vec, + pub components: Vec, /// An optional name of a pattern. - pattern: Option>, + pub pattern: Option>, } -impl SetStrokingColor { - pub fn new(components: Vec, pattern: Option>) -> Self { - Self { - components, - pattern, - } - } - - /// Reads the operator from its operands. +impl ColorOperands { + /// Reads the operands of `SCN` or `scn`: the color components, optionally + /// followed by a pattern name. + #[inline] pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { // The pattern name should come last, after the numeric color components let count = operands @@ -290,102 +24,38 @@ impl SetStrokingColor { .iter() .take_while(|value| !value.is_name()) .count(); - let values = (0..count) + let components = (0..count) .map(|index| operands.at(index)) .collect::>()?; let pattern = operands.at::(count).ok().map(|name| name.bytes); - Ok(Self::new(values, pattern)) - } - - /// Reads the operator from the color components of `SC`, which takes no pattern. - pub(crate) fn read_without_pattern(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(Self::new(values, None)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - if let Some(pattern) = &self.pattern { - backend.set_stroking_color_extended(&self.components, pattern) - } else { - backend.set_stroking_color(&self.components) - } - } -} - -/// Sets the non-stroking color when the color space requires -/// multiple color components. -#[derive(Debug, Clone, PartialEq)] -pub struct SetNonStrokingColor { - /// Color component values. - components: Vec, - /// An optional name of a pattern. - pattern: Option>, -} - -impl SetNonStrokingColor { - pub fn new(components: Vec, pattern: Option>) -> Self { - Self { + Ok(Self { components, pattern, - } + }) } - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - // The pattern name should come last, after the numeric color components - 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(Self::new(values, pattern)) - } - - /// Reads the operator from the color components of `sc`, which takes no pattern. - pub(crate) fn read_without_pattern(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let values: Vec = operands.read_all()?; + /// Reads the operands of `operator` (`SC` or `sc`), which takes at least one + /// color component and no pattern. + #[inline] + pub(crate) fn read_without_pattern( + operands: &mut OperandContext<'_, '_>, + operator: &'static str, + ) -> ReadResult { + let components: Vec = operands.read_all()?; - if values.is_empty() { + if components.is_empty() { return Err(PdfOperatorError::OperandCountMismatch { - operator: "sc".to_string(), + operator: operator.to_string(), expected: 1, actual: 0, } .into()); } - Ok(Self::new(values, None)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - if let Some(pattern) = &self.pattern { - backend.set_non_stroking_color_extended(&self.components, pattern) - } else { - backend.set_non_stroking_color(&self.components) - } + Ok(Self { + components, + pattern: None, + }) } } diff --git a/crates/pdf-content-stream-operators/src/graphics_state_operators.rs b/crates/pdf-content-stream-operators/src/graphics_state_operators.rs deleted file mode 100644 index f4863202..00000000 --- a/crates/pdf-content-stream-operators/src/graphics_state_operators.rs +++ /dev/null @@ -1,275 +0,0 @@ -use std::sync::Arc; - -use num_traits::FromPrimitive; -use pdf_graphics::{LineCap, LineJoin, transform::Transform}; -use pdf_object_reader::{ReadResult, pdf_string::PdfString}; - -use crate::{ - OperandContext, - error::PdfOperatorError, - pdf_operator_backend::{BackendError, PdfOperatorBackend}, -}; - -/// Sets the line width for path stroking. -#[derive(Debug, Clone, PartialEq)] -pub struct SetLineWidth { - /// The new line width in user space units. - width: f32, -} - -impl SetLineWidth { - pub fn new(width: f32) -> Self { - Self { width } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let width = operands.at(0)?; - Ok(Self::new(width)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_line_width(self.width) - } -} - -/// Sets the line cap style for path stroking. -#[derive(Debug, Clone, PartialEq)] -pub struct SetLineCapStyle { - /// The line cap style to apply. - style: LineCap, -} - -impl SetLineCapStyle { - pub fn new(style: u8) -> Result { - match LineCap::from_u8(style) { - Some(style_enum) => Ok(Self { style: style_enum }), - None => Err(PdfOperatorError::InvalidOperandValue { - expected: "line cap style (0 butt, 1 round, 2 projecting square)", - found: style.to_string(), - }), - } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let style = operands.at(0)?; - Ok(Self::new(style)?) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_line_cap(self.style) - } -} - -/// Sets the line join style for path stroking. -#[derive(Debug, Clone, PartialEq)] -pub struct SetLineJoinStyle { - /// The line join style to apply. - style: LineJoin, -} - -impl SetLineJoinStyle { - pub fn new(style: u8) -> Result { - match LineJoin::from_u8(style) { - Some(style_enum) => Ok(Self { style: style_enum }), - None => Err(PdfOperatorError::InvalidOperandValue { - expected: "line join style (0 miter, 1 round, 2 bevel)", - found: style.to_string(), - }), - } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let style = operands.at(0)?; - Ok(Self::new(style)?) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_line_join(self.style) - } -} - -/// Sets the miter limit for path stroking. -#[derive(Debug, Clone, PartialEq)] -pub struct SetMiterLimit { - /// The new miter limit. This controls when a miter join is automatically - /// converted to a bevel join to prevent excessively long spikes. - limit: f32, -} - -impl SetMiterLimit { - pub fn new(limit: f32) -> Self { - Self { limit } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let limit = operands.at(0)?; - Ok(Self::new(limit)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_miter_limit(self.limit) - } -} - -/// Sets the dash pattern for path stroking. -#[derive(Debug, Clone, PartialEq)] -pub struct SetDashPattern { - /// An array of numbers specifying the lengths of alternating dashes and gaps. - array: Vec, - /// The phase, specifying the distance into the dash pattern at which to start. - phase: f32, -} - -impl SetDashPattern { - pub fn new(array: Vec, phase: f32) -> Self { - Self { array, phase } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let array = operands.at(0)?; - let phase = operands.at(1)?; - Ok(Self::new(array, phase)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_dash_pattern(&self.array, self.phase) - } -} - -/// Sets the flatness tolerance, controlling the accuracy with which curves -/// are approximated. (PDF operator `i`) -#[derive(Debug, Clone, PartialEq)] -pub struct SetFlatnessTolerance { - /// The flatness tolerance value. Typical values range from 1 to 100. - tolerance: f32, -} - -impl SetFlatnessTolerance { - pub fn new(tolerance: f32) -> Self { - Self { tolerance } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let tolerance = operands.at(0)?; - Ok(Self::new(tolerance)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_flatness_tolerance(self.tolerance) - } -} - -/// Sets the rendering intent for color reproduction. (PDF operator `ri`) -/// -/// The operand is a name object indicating the intent, e.g., -/// /AbsoluteColorimetric, /RelativeColorimetric, /Saturation, /Perceptual. -#[derive(Debug, Clone, PartialEq)] -pub struct SetRenderingIntent { - intent: Arc<[u8]>, -} - -impl SetRenderingIntent { - pub fn new(intent: Arc<[u8]>) -> Self { - Self { intent } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let intent = operands.at::(0)?.bytes; - Ok(Self::new(intent)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_rendering_intent(&self.intent) - } -} - -/// Modifies the current transformation matrix (CTM) by concatenating the specified matrix. -#[derive(Debug, Clone, PartialEq)] -pub struct ConcatMatrix { - /// The matrix to concatenate with the CTM. - matrix: Transform, -} - -impl ConcatMatrix { - pub fn new(matrix: [f32; 6]) -> Self { - Self { - matrix: Transform::from(matrix), - } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - Ok(Self::new(operands.read_array()?)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.concat_matrix(&self.matrix) - } -} - -/// Sets multiple graphics state parameters from a named graphics state parameter dictionary. -/// The dictionary is expected to be in the resource dictionary. (PDF operator `gs`) -#[derive(Debug, Clone, PartialEq)] -pub struct SetGraphicsStateFromDict { - /// The name of the graphics state parameter dictionary. - dict_name: Arc<[u8]>, -} - -impl SetGraphicsStateFromDict { - pub fn new(dict_name: Arc<[u8]>) -> Self { - Self { dict_name } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let dict_name = operands.at::(0)?.bytes; - Ok(Self::new(dict_name)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_graphics_state_from_dict(&self.dict_name) - } -} diff --git a/crates/pdf-content-stream-operators/src/lib.rs b/crates/pdf-content-stream-operators/src/lib.rs index d36f18cd..7b8e6f12 100644 --- a/crates/pdf-content-stream-operators/src/lib.rs +++ b/crates/pdf-content-stream-operators/src/lib.rs @@ -1,18 +1,10 @@ pub mod color_operators; pub mod error; -pub mod graphics_state_operators; pub mod marked_content_operators; pub mod variants; -pub mod path_operators; pub mod pdf_operator_backend; pub mod recording_pdf_operator_backend; -pub mod shadings_operators; -pub mod text_positioning_operators; -pub mod text_showing_operators; -pub mod text_state_operators; -pub mod type3_font_operators; -pub mod xobject_and_image_operators; extern crate alloc; use std::sync::Arc; 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 70b8beeb..5a565284 100644 --- a/crates/pdf-content-stream-operators/src/marked_content_operators.rs +++ b/crates/pdf-content-stream-operators/src/marked_content_operators.rs @@ -7,34 +7,6 @@ use crate::{ pdf_operator_backend::{BackendError, PdfOperatorBackend}, }; -/// Begins a marked-content sequence. -/// Marked-content sequences associate a tag with a sequence of content stream operations. -#[derive(Debug, Clone, PartialEq)] -pub struct BeginMarkedContent { - /// The tag indicating the role or nature of the marked-content sequence. - tag: Arc<[u8]>, -} - -impl BeginMarkedContent { - pub fn new(tag: Arc<[u8]>) -> Self { - Self { tag } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let tag = operands.at::(0)?.bytes; - Ok(Self::new(tag)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.begin_marked_content(&self.tag) - } -} - /// Begins a marked-content sequence with an associated property list. /// The property list can be either a name (referring to an entry in the Properties subdictionary /// of the current resource dictionary) or an inline dictionary. diff --git a/crates/pdf-content-stream-operators/src/path_operators.rs b/crates/pdf-content-stream-operators/src/path_operators.rs deleted file mode 100644 index c51bbf96..00000000 --- a/crates/pdf-content-stream-operators/src/path_operators.rs +++ /dev/null @@ -1,237 +0,0 @@ -use crate::{ - OperandContext, - pdf_operator_backend::{BackendError, PdfOperatorBackend}, -}; -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. -#[derive(Debug, Clone, PartialEq)] -pub struct MoveTo { - /// The x-coordinate of the new current point. - x: f32, - /// The y-coordinate of the new current point. - y: f32, -} - -impl MoveTo { - pub fn new(x: f32, y: f32) -> Self { - Self { x, y } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let [x, y] = operands.read_array()?; - Ok(Self::new(x, y)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.move_to(self.x, self.y) - } -} - -/// Appends a straight line segment from the current point to the specified point (x, y). -/// The new current point becomes (x, y). -#[derive(Debug, Clone, PartialEq)] -pub struct LineTo { - /// The x-coordinate of the line segment's end point. - x: f32, - /// The y-coordinate of the line segment's end point. - y: f32, -} - -impl LineTo { - pub fn new(x: f32, y: f32) -> Self { - Self { x, y } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let [x, y] = operands.read_array()?; - Ok(Self::new(x, y)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.line_to(self.x, self.y) - } -} - -/// Appends a cubic Bézier curve to the current path. -/// The curve extends from the current point to (x3, y3), using (x1, y1) and (x2, y2) as Bézier control points. -/// The new current point becomes (x3, y3). -#[derive(Debug, Clone, PartialEq)] -pub struct CurveTo { - /// The x-coordinate of the first Bézier control point. - x1: f32, - /// The y-coordinate of the first Bézier control point. - y1: f32, - /// The x-coordinate of the second Bézier control point. - x2: f32, - /// The y-coordinate of the second Bézier control point. - y2: f32, - /// The x-coordinate of the curve's end point. - x3: f32, - /// The y-coordinate of the curve's end point. - y3: f32, -} - -impl CurveTo { - pub fn new(x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) -> Self { - Self { - x1, - y1, - x2, - y2, - x3, - y3, - } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let [x1, y1, x2, y2, x3, y3] = operands.read_array()?; - Ok(Self::new(x1, y1, x2, y2, x3, y3)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.curve_to(self.x1, self.y1, self.x2, self.y2, self.x3, self.y3) - } -} - -/// Appends a cubic Bézier curve to the current path. -/// The current point is used as the first control point (x1, y1). -/// (x2, y2) is the second Bézier control point, and (x3, y3) is the end point of the curve. -/// The new current point becomes (x3, y3). -#[derive(Debug, Clone, PartialEq)] -pub struct CurveToV { - /// The x-coordinate of the second Bézier control point. - x2: f32, - /// The y-coordinate of the second Bézier control point. - y2: f32, - /// The x-coordinate of the curve's end point. - x3: f32, - /// The y-coordinate of the curve's end point. - y3: f32, -} - -impl CurveToV { - pub fn new(x2: f32, y2: f32, x3: f32, y3: f32) -> Self { - Self { x2, y2, x3, y3 } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let [x2, y2, x3, y3] = operands.read_array()?; - Ok(Self::new(x2, y2, x3, y3)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.curve_to_v(self.x2, self.y2, self.x3, self.y3) - } -} - -/// Appends a cubic Bézier curve to the current path. -/// (x1, y1) is the first Bézier control point. The second control point (x2, y2) is the same as the curve's end point (x3, y3). -/// The new current point becomes (x3, y3). -#[derive(Debug, Clone, PartialEq)] -pub struct CurveToY { - /// The x-coordinate of the first Bézier control point. - x1: f32, - /// The y-coordinate of the first Bézier control point. - y1: f32, - /// The x-coordinate of the curve's end point (and second control point). - x3: f32, - /// The y-coordinate of the curve's end point (and second control point). - y3: f32, -} - -impl CurveToY { - pub fn new(x1: f32, y1: f32, x3: f32, y3: f32) -> Self { - Self { x1, y1, x3, y3 } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let [x1, y1, x3, y3] = operands.read_array()?; - Ok(Self::new(x1, y1, x3, y3)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.curve_to_y(self.x1, self.y1, self.x3, self.y3) - } -} - -/// Appends a complete rectangle, defined by its bottom-left corner (x, y), width, and height, -/// to the current path as a complete subpath. -/// The new current point is undefined after this operation. -#[derive(Debug, Clone, PartialEq)] -pub struct Rectangle { - /// The x-coordinate of the bottom-left corner of the rectangle. - x: f32, - /// The y-coordinate of the bottom-left corner of the rectangle. - y: f32, - /// The width of the rectangle. - width: f32, - /// The height of the rectangle. - height: f32, -} - -impl Rectangle { - pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self { - Self { - x, - y, - width, - height, - } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let [x, y, width, height] = operands.read_array()?; - Ok(Self::new(x, y, width, height)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.rectangle(self.x, self.y, self.width, self.height) - } -} - -#[cfg(test)] -#[allow(clippy::unwrap_used, clippy::expect_used)] -mod tests { - use crate::recording_pdf_operator_backend::RecordingBackend; - - use super::*; - - #[test] - fn test_path_operations() { - let mut backend = RecordingBackend::default(); - let a = MoveTo::new(0.0, 0.0); - a.call(&mut backend).unwrap(); - } -} diff --git a/crates/pdf-content-stream-operators/src/shadings_operators.rs b/crates/pdf-content-stream-operators/src/shadings_operators.rs deleted file mode 100644 index d48e6dc2..00000000 --- a/crates/pdf-content-stream-operators/src/shadings_operators.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::sync::Arc; - -use crate::{ - OperandContext, - pdf_operator_backend::{BackendError, PdfOperatorBackend}, -}; -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. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PaintShading { - /// The name of the shading dictionary resource from the Shading subdictionary - /// of the current resource dictionary. - name: Arc<[u8]>, -} - -impl PaintShading { - pub fn new(name: Arc<[u8]>) -> Self { - Self { name } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let name = operands.at::(0)?.bytes; - Ok(Self::new(name)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.paint_shading(&self.name) - } -} diff --git a/crates/pdf-content-stream-operators/src/text_positioning_operators.rs b/crates/pdf-content-stream-operators/src/text_positioning_operators.rs deleted file mode 100644 index 6ed8dc36..00000000 --- a/crates/pdf-content-stream-operators/src/text_positioning_operators.rs +++ /dev/null @@ -1,103 +0,0 @@ -use pdf_graphics::transform::Transform; - -use crate::{ - OperandContext, - pdf_operator_backend::{BackendError, PdfOperatorBackend}, -}; -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. -/// More precisely, this operator sets the text line matrix, T_lm, to: -/// `[1 0 0 1 tx ty] * T_lm` -#[derive(Debug, Clone, PartialEq)] -pub struct MoveTextPosition { - /// The horizontal offset. - tx: f32, - /// The vertical offset. - ty: f32, -} - -impl MoveTextPosition { - pub fn new(tx: f32, ty: f32) -> Self { - Self { tx, ty } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let [tx, ty] = operands.read_array()?; - Ok(Self::new(tx, ty)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.move_text_position(self.tx, self.ty) - } -} - -/// Moves to the start of the next line, offset from the start of the current line by (`tx`, `ty`), -/// and sets the text leading, `TL`, to `-ty`. (PDF operator `TD`) -/// This has the same effect as `SetLeading { leading: -ty }` followed by `MoveTextPosition { tx, ty }`. -#[derive(Debug, Clone, PartialEq)] -pub struct MoveTextPositionAndSetLeading { - /// The horizontal offset. - tx: f32, - /// The vertical offset. The new text leading will be set to the negative of this value. - ty: f32, -} - -impl MoveTextPositionAndSetLeading { - pub fn new(tx: f32, ty: f32) -> Self { - Self { tx, ty } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let [tx, ty] = operands.read_array()?; - Ok(Self::new(tx, ty)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.move_text_position_and_set_leading(self.tx, self.ty) - } -} - -/// Sets the text matrix, `Tm`, and the text line matrix, `Tlm`. -#[derive(Debug, Clone, PartialEq)] -pub struct SetTextMatrix { - /// The text matrix. - matrix: Transform, -} - -impl SetTextMatrix { - pub fn new(matrix: [f32; 6]) -> Self { - Self { - matrix: Transform::from(matrix), - } - } - - /// Returns the text matrix. - pub const fn matrix(&self) -> &Transform { - &self.matrix - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - Ok(Self::new(operands.read_array()?)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_text_matrix(&self.matrix) - } -} diff --git a/crates/pdf-content-stream-operators/src/text_showing_operators.rs b/crates/pdf-content-stream-operators/src/text_showing_operators.rs deleted file mode 100644 index 1085c5f1..00000000 --- a/crates/pdf-content-stream-operators/src/text_showing_operators.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crate::OperandContext; -use crate::PdfTextItem; -use crate::pdf_operator_backend::{BackendError, PdfOperatorBackend}; -use pdf_object_reader::{ReadResult, pdf_string::PdfString}; - -/// Shows a text string. -#[derive(Debug, Clone, PartialEq)] -pub struct ShowText { - /// The text item to be shown. Its bytes are typically encoded according to the font's - /// encoding. - text: PdfTextItem, -} - -impl ShowText { - pub fn new(text: PdfTextItem) -> Self { - Self { text } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let text = operands.at::(0)?.bytes; - Ok(Self::new(PdfTextItem::Text(text))) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.show_text(&self.text) - } -} - -/// Moves to the next line and shows a text string. -#[derive(Debug, Clone, PartialEq)] -pub struct MoveNextLineShowText { - /// The text string to be shown. - text: PdfTextItem, -} - -impl MoveNextLineShowText { - pub fn new(text: PdfTextItem) -> Self { - Self { text } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let text = operands.at::(0)?.bytes; - Ok(Self::new(PdfTextItem::Text(text))) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.move_to_next_line_and_show_text(&self.text) - } -} - -/// Sets the word and character spacing, moves to the next line, and shows a text string. -/// This is equivalent to `SetWordSpacing`, `SetCharacterSpacing`, and `MoveNextLineShowText`. -#[derive(Debug, Clone, PartialEq)] -pub struct SetSpacingMoveShowText { - /// The new word spacing to set before showing the text. - word_spacing: f32, - /// The new character spacing to set before showing the text. - char_spacing: f32, - /// The text string to be shown. - text: PdfTextItem, -} - -impl SetSpacingMoveShowText { - pub fn new(word_spacing: f32, char_spacing: f32, text: PdfTextItem) -> Self { - Self { - word_spacing, - char_spacing, - text, - } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let [word_spacing, char_spacing] = operands.read_array()?; - let text = operands.at::(2)?.bytes; - - Ok(Self::new( - word_spacing, - char_spacing, - PdfTextItem::Text(text), - )) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_spacing_and_show_text(self.word_spacing, self.char_spacing, &self.text) - } -} - -/// Shows one or more text strings, allowing individual glyph positioning. -/// The array can contain strings and numbers. Numbers specify an additional horizontal or vertical -/// displacement (depending on the writing mode) to apply before showing the next string or glyph. -#[derive(Debug, Clone, PartialEq)] -pub struct ShowTextArray { - /// A vector of `PdfTextItem`s, where each item is either a string to show - /// or a numeric adjustment for positioning. - elements: Vec, -} - -impl ShowTextArray { - pub fn new(elements: Vec) -> Self { - Self { elements } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - Ok(Self::new(operands.at(0)?)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.show_text_with_glyph_positioning(&self.elements) - } -} - -#[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] - fn show_text_moves_the_operand_byte_allocation() { - let text: Arc<[u8]> = Arc::from(b"text without a copy".to_vec()); - - let original_pointer = text.as_ptr(); - - 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 ArrayContext::new(&operands, &mut session)) - .expect("text should parse"); - let PdfTextItem::Text(text) = operator.text else { - panic!("expected text item"); - }; - assert_eq!(&*text, b"text without a copy"); - assert_eq!(text.as_ptr(), original_pointer); - } - - #[test] - fn spacing_move_show_text_groups_numeric_prefix_before_text() { - 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 ArrayContext::new(&operands, &mut session)) - .expect("mixed operands should parse successfully"); - assert_eq!(operator.word_spacing, 1.5); - assert_eq!(operator.char_spacing, 2.0); - 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 deleted file mode 100644 index 4207de39..00000000 --- a/crates/pdf-content-stream-operators/src/text_state_operators.rs +++ /dev/null @@ -1,230 +0,0 @@ -use std::sync::Arc; - -use crate::{ - OperandContext, - error::PdfOperatorError, - pdf_operator_backend::{BackendError, PdfOperatorBackend}, -}; -use num_traits::FromPrimitive; -use pdf_graphics::TextRenderingMode; -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)] -pub struct SetCharacterSpacing { - /// The character spacing. Added to the horizontal displacement otherwise produced by showing a glyph. - spacing: f32, -} - -impl SetCharacterSpacing { - pub fn new(spacing: f32) -> Self { - Self { spacing } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let spacing = operands.at(0)?; - Ok(Self::new(spacing)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_character_spacing(self.spacing) - } -} - -/// Sets the word spacing, `Tw`, which is a number expressed in unscaled text space units. -/// Word spacing is used by the `Tj`, `'`, and `"` operators. -#[derive(Debug, Clone, PartialEq)] -pub struct SetWordSpacing { - /// The word spacing. Added to the character spacing when the character is a space (char code 32). - spacing: f32, -} - -impl SetWordSpacing { - pub fn new(spacing: f32) -> Self { - Self { spacing } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let spacing = operands.at(0)?; - Ok(Self::new(spacing)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_word_spacing(self.spacing) - } -} - -/// Sets the horizontal scaling, `Tz`, which adjusts the width of glyphs by -/// stretching or compressing them horizontally. -#[derive(Debug, Clone, PartialEq)] -pub struct SetHorizontalScaling { - /// The horizontal scaling factor as a percentage (e.g., 100.0 for 100% - no scaling). - scale: f32, -} - -impl SetHorizontalScaling { - pub fn new(scale: f32) -> Self { - Self { scale } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let scale = operands.at(0)?; - Ok(Self::new(scale)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_horizontal_text_scaling(self.scale) - } -} - -/// Sets the text leading, `TL`, which is the vertical distance between the baselines of -/// adjacent lines of text. -#[derive(Debug, Clone, PartialEq)] -pub struct SetLeading { - /// The text leading, in unscaled text space units. - leading: f32, -} - -impl SetLeading { - /// Creates a leading operator with the given baseline distance. - pub fn new(leading: f32) -> Self { - Self { leading } - } - - /// Returns the baseline distance in unscaled text-space units. - pub const fn leading(&self) -> f32 { - self.leading - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let leading = operands.at(0)?; - Ok(Self::new(leading)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_text_leading(self.leading) - } -} - -/// Sets the text font, `Tf`, to a font resource in the resource dictionary and the text -/// font size, `Tfs`, in unscaled text space units. -#[derive(Debug, Clone, PartialEq)] -pub struct SetFont { - /// The name of the font resource. - name: Arc<[u8]>, - /// The font size. - size: f32, -} - -impl SetFont { - pub fn new(name: Arc<[u8]>, size: f32) -> Self { - Self { name, size } - } - - /// Returns the font resource name. - pub fn name(&self) -> &[u8] { - &self.name - } - - /// Returns the font size. - pub const fn size(&self) -> f32 { - self.size - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let name = operands.at::(0)?.bytes; - let size = operands.at(1)?; - Ok(Self::new(name, size)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_font_and_size(&self.name, self.size) - } -} - -/// Sets the text rendering mode, which determines whether text is filled, stroked, -/// used as a clipping path, or some combination. -#[derive(Debug, Clone, PartialEq)] -pub struct SetRenderingMode { - /// The rendering mode. - mode: TextRenderingMode, -} - -impl SetRenderingMode { - pub fn new(mode: u8) -> Result { - match TextRenderingMode::from_u8(mode) { - Some(mode) => Ok(Self { mode }), - None => Err(PdfOperatorError::InvalidOperandValue { - expected: "One of the valid text rendering modes (0-7)", - found: mode.to_string(), - }), - } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let mode = operands.at(0)?; - Ok(Self::new(mode)?) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_text_rendering_mode(self.mode) - } -} - -/// Sets the text rise, `Ts`, which specifies the vertical distance -/// to shift the baseline of text relative to the current baseline. -#[derive(Debug, Clone, PartialEq)] -pub struct SetTextRise { - /// The text rise, in unscaled text space units. A positive value moves the baseline up. - rise: f32, -} - -impl SetTextRise { - pub fn new(rise: f32) -> Self { - Self { rise } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let rise = operands.at(0)?; - Ok(Self::new(rise)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.set_text_rise(self.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 deleted file mode 100644 index 4db3084c..00000000 --- a/crates/pdf-content-stream-operators/src/type3_font_operators.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::{ - OperandContext, - pdf_operator_backend::{BackendError, PdfOperatorBackend}, -}; -use pdf_object_reader::ReadResult; - -#[derive(Debug, Clone, PartialEq, Default)] -pub struct SetCharWidthAndBoundingBox { - /// The x-component of the character width vector. - pub wx: f32, - /// The y-component of the character width vector. - wy: f32, - /// The x-coordinate of the lower-left corner of the character bounding box. - llx: f32, - /// The y-coordinate of the lower-left corner of the character bounding box. - lly: f32, - /// The x-coordinate of the upper-right corner of the character bounding box. - urx: f32, - /// The y-coordinate of the upper-right corner of the character bounding box. - ury: f32, -} - -impl SetCharWidthAndBoundingBox { - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let [wx, wy, llx, lly, urx, ury] = operands.read_array()?; - - Ok(Self { - wx, - wy, - llx, - lly, - urx, - ury, - }) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - _backend: &mut T, - ) -> Result<(), BackendError> { - Ok(()) - } -} - -#[derive(Debug, Clone, PartialEq, Default)] -pub struct SetCharWidth { - /// The x-component of the character width vector. - pub wx: f32, - /// The y-component of the character width vector. - wy: f32, -} - -impl SetCharWidth { - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let [wx, wy] = operands.read_array()?; - - Ok(Self { wx, wy }) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - _backend: &mut T, - ) -> Result<(), BackendError> { - Ok(()) - } -} diff --git a/crates/pdf-content-stream-operators/src/variants.rs b/crates/pdf-content-stream-operators/src/variants.rs index 054ade50..b68386f4 100644 --- a/crates/pdf-content-stream-operators/src/variants.rs +++ b/crates/pdf-content-stream-operators/src/variants.rs @@ -1,35 +1,95 @@ use std::sync::Arc; +use pdf_graphics::{LineCap, LineJoin, TextRenderingMode, transform::Transform}; use pdf_image::InlineImage; -use pdf_object_reader::ReadResult; +use pdf_object_reader::{ReadResult, pdf_string::PdfString}; use crate::{ - OperandContext, - color_operators::*, - graphics_state_operators::*, - marked_content_operators::*, - path_operators::*, + OperandContext, PdfTextItem, + color_operators::ColorOperands, + marked_content_operators::BeginMarkedContentWithProps, pdf_operator_backend::{BackendError, PdfOperatorBackend}, - shadings_operators::PaintShading, - text_positioning_operators::*, - text_showing_operators::*, - text_state_operators::*, - type3_font_operators::{SetCharWidth, SetCharWidthAndBoundingBox}, - xobject_and_image_operators::*, }; /// One PDF content-stream operation with its decoded operands. #[derive(Debug, Clone)] pub enum PdfOperatorVariant { - LineTo(LineTo), - MoveTo(MoveTo), - CurveTo(CurveTo), - CurveToV(CurveToV), - CurveToY(CurveToY), + /// Appends a straight line segment from the current point to the specified point (x, y). + /// The new current point becomes (x, y). + LineTo { + /// The x-coordinate of the line segment's end point. + x: f32, + /// The y-coordinate of the line segment's end point. + y: f32, + }, + /// 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. + MoveTo { + /// The x-coordinate of the new current point. + x: f32, + /// The y-coordinate of the new current point. + y: f32, + }, + /// Appends a cubic Bézier curve to the current path. + /// The curve extends from the current point to (x3, y3), using (x1, y1) and (x2, y2) as Bézier control points. + /// The new current point becomes (x3, y3). + CurveTo { + /// The x-coordinate of the first Bézier control point. + x1: f32, + /// The y-coordinate of the first Bézier control point. + y1: f32, + /// The x-coordinate of the second Bézier control point. + x2: f32, + /// The y-coordinate of the second Bézier control point. + y2: f32, + /// The x-coordinate of the curve's end point. + x3: f32, + /// The y-coordinate of the curve's end point. + y3: f32, + }, + /// Appends a cubic Bézier curve to the current path. + /// The current point is used as the first control point (x1, y1). + /// (x2, y2) is the second Bézier control point, and (x3, y3) is the end point of the curve. + /// The new current point becomes (x3, y3). + CurveToV { + /// The x-coordinate of the second Bézier control point. + x2: f32, + /// The y-coordinate of the second Bézier control point. + y2: f32, + /// The x-coordinate of the curve's end point. + x3: f32, + /// The y-coordinate of the curve's end point. + y3: f32, + }, + /// Appends a cubic Bézier curve to the current path. + /// (x1, y1) is the first Bézier control point. The second control point (x2, y2) is the same as the curve's end point (x3, y3). + /// The new current point becomes (x3, y3). + CurveToY { + /// The x-coordinate of the first Bézier control point. + x1: f32, + /// The y-coordinate of the first Bézier control point. + y1: f32, + /// The x-coordinate of the curve's end point (and second control point). + x3: f32, + /// The y-coordinate of the curve's end point (and second control point). + y3: f32, + }, /// Closes the current subpath by appending a straight line segment from the current point /// to the starting point of the subpath. (PDF operator `h`) ClosePath, - Rectangle(Rectangle), + /// Appends a complete rectangle, defined by its bottom-left corner (x, y), width, and height, + /// to the current path as a complete subpath. + /// The new current point is undefined after this operation. + Rectangle { + /// The x-coordinate of the bottom-left corner of the rectangle. + x: f32, + /// The y-coordinate of the bottom-left corner of the rectangle. + y: f32, + /// The width of the rectangle. + width: f32, + /// The height of the rectangle. + height: f32, + }, /// Strokes the current path. StrokePath, /// Closes the current subpath and then strokes the path. @@ -58,25 +118,117 @@ pub enum PdfOperatorVariant { ClipNonZero, /// Modifies the current clipping path by intersecting it with the current path, using the even-odd rule to determine the region to clip. ClipEvenOdd, - SetGrayFill(SetGrayFill), - SetGrayStroke(SetGrayStroke), - SetRGBFill(SetRGBFill), - SetRGBStroke(SetRGBStroke), - SetCMYKFill(SetCMYKFill), - SetCMYKStroke(SetCMYKStroke), - SetLineWidth(SetLineWidth), - SetLineCapStyle(SetLineCapStyle), - SetLineJoinStyle(SetLineJoinStyle), - SetMiterLimit(SetMiterLimit), - SetDashPattern(SetDashPattern), - SetFlatnessTolerance(SetFlatnessTolerance), - SetGraphicsStateFromDict(SetGraphicsStateFromDict), + /// Sets the fill color to a grayscale value. + /// The gray level applies to subsequent fill operations. + SetGrayFill { + /// The gray level, a value between 0.0 (black) and 1.0 (white). + gray: f32, + }, + /// Sets the stroke color to a grayscale value. + /// The gray level applies to subsequent stroke operations. + SetGrayStroke { + /// The gray level, a value between 0.0 (black) and 1.0 (white). + gray: f32, + }, + /// Sets the fill color to an RGB (Red, Green, Blue) value. + /// The RGB color applies to subsequent fill operations. + SetRGBFill { + /// The red component, a value between 0.0 and 1.0. + r: f32, + /// The green component, a value between 0.0 and 1.0. + g: f32, + /// The blue component, a value between 0.0 and 1.0. + b: f32, + }, + /// Sets the stroke color to an RGB (Red, Green, Blue) value. + /// The RGB color applies to subsequent stroke operations. + SetRGBStroke { + /// The red component, a value between 0.0 and 1.0. + r: f32, + /// The green component, a value between 0.0 and 1.0. + g: f32, + /// The blue component, a value between 0.0 and 1.0. + b: f32, + }, + /// Sets the fill color to a CMYK (Cyan, Magenta, Yellow, Black/Key) value. + /// The CMYK color applies to subsequent fill operations. + SetCMYKFill { + /// The cyan component, a value between 0.0 and 1.0. + c: f32, + /// The magenta component, a value between 0.0 and 1.0. + m: f32, + /// The yellow component, a value between 0.0 and 1.0. + y: f32, + /// The black (key) component, a value between 0.0 and 1.0. + k: f32, + }, + /// Sets the stroke color to a CMYK (Cyan, Magenta, Yellow, Black/Key) value. + /// The CMYK color applies to subsequent stroke operations. + SetCMYKStroke { + /// The cyan component, a value between 0.0 and 1.0. + c: f32, + /// The magenta component, a value between 0.0 and 1.0. + m: f32, + /// The yellow component, a value between 0.0 and 1.0. + y: f32, + /// The black (key) component, a value between 0.0 and 1.0. + k: f32, + }, + /// Sets the line width for path stroking. + SetLineWidth { + /// The new line width in user space units. + width: f32, + }, + /// Sets the line cap style for path stroking. + SetLineCapStyle { + /// The line cap style to apply. + style: LineCap, + }, + /// Sets the line join style for path stroking. + SetLineJoinStyle { + /// The line join style to apply. + style: LineJoin, + }, + /// Sets the miter limit for path stroking. + SetMiterLimit { + /// The new miter limit. This controls when a miter join is automatically + /// converted to a bevel join to prevent excessively long spikes. + limit: f32, + }, + /// Sets the dash pattern for path stroking. + SetDashPattern { + /// An array of numbers specifying the lengths of alternating dashes and gaps. + array: Vec, + /// The phase, specifying the distance into the dash pattern at which to start. + phase: f32, + }, + /// Sets the flatness tolerance, controlling the accuracy with which curves + /// are approximated. (PDF operator `i`) + SetFlatnessTolerance { + /// The flatness tolerance value. Typical values range from 1 to 100. + tolerance: f32, + }, + /// Sets multiple graphics state parameters from a named graphics state parameter dictionary. + /// The dictionary is expected to be in the resource dictionary. (PDF operator `gs`) + SetGraphicsStateFromDict { + /// The name of the graphics state parameter dictionary. + dict_name: Arc<[u8]>, + }, /// Saves the current graphics state on the graphics state stack. SaveGraphicsState, /// Restores the graphics state by removing the most recently saved state from the stack. RestoreGraphicsState, - ConcatMatrix(ConcatMatrix), - BeginMarkedContent(BeginMarkedContent), + /// Modifies the current transformation matrix (CTM) by concatenating the specified matrix. + ConcatMatrix { + /// The matrix to concatenate with the CTM. + matrix: Transform, + }, + /// Begins a marked-content sequence. + /// Marked-content sequences associate a tag with a sequence of content stream operations. + BeginMarkedContent { + /// The tag indicating the role or nature of the marked-content sequence. + tag: Arc<[u8]>, + }, BeginMarkedContentWithProps(BeginMarkedContentWithProps), /// Ends a marked-content sequence begun by a `BMC` or `BDC` operator. EndMarkedContent, @@ -85,25 +237,112 @@ pub enum PdfOperatorVariant { BeginText, /// Ends a text object, discarding the text matrix and text line matrix. EndText, - MoveTextPosition(MoveTextPosition), - MoveTextPositionAndSetLeading(MoveTextPositionAndSetLeading), - SetTextMatrix(SetTextMatrix), + /// 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. + /// More precisely, this operator sets the text line matrix, T_lm, to: + /// `[1 0 0 1 tx ty] * T_lm` + MoveTextPosition { + /// The horizontal offset. + tx: f32, + /// The vertical offset. + ty: f32, + }, + /// Moves to the start of the next line, offset from the start of the current line by (`tx`, `ty`), + /// and sets the text leading, `TL`, to `-ty`. (PDF operator `TD`) + /// This has the same effect as `SetLeading { leading: -ty }` followed by `MoveTextPosition { tx, ty }`. + MoveTextPositionAndSetLeading { + /// The horizontal offset. + tx: f32, + /// The vertical offset. The new text leading will be set to the negative of this value. + ty: f32, + }, + /// Sets the text matrix, `Tm`, and the text line matrix, `Tlm`. + SetTextMatrix { + /// The text matrix. + matrix: Transform, + }, /// Moves to the start of the next line. /// This has the same effect as `MoveTextPosition { tx: 0.0, ty: -leading }`, /// where `leading` is the current value of the text leading parameter in the text state. MoveToNextLine, - ShowText(ShowText), - MoveNextLineShowText(MoveNextLineShowText), - SetSpacingMoveShowText(SetSpacingMoveShowText), - ShowTextArray(ShowTextArray), - SetCharacterSpacing(SetCharacterSpacing), - SetWordSpacing(SetWordSpacing), - SetHorizontalScaling(SetHorizontalScaling), - SetLeading(SetLeading), - SetFont(SetFont), - SetRenderingMode(SetRenderingMode), - SetTextRise(SetTextRise), - InvokeXObject(InvokeXObject), + /// Shows a text string. + ShowText { + /// The text item to be shown. Its bytes are typically encoded according to the font's + /// encoding. + text: PdfTextItem, + }, + /// Moves to the next line and shows a text string. + MoveNextLineShowText { + /// The text string to be shown. + text: PdfTextItem, + }, + /// Sets the word and character spacing, moves to the next line, and shows a text string. + /// This is equivalent to `SetWordSpacing`, `SetCharacterSpacing`, and `MoveNextLineShowText`. + SetSpacingMoveShowText { + /// The new word spacing to set before showing the text. + word_spacing: f32, + /// The new character spacing to set before showing the text. + char_spacing: f32, + /// The text string to be shown. + text: PdfTextItem, + }, + /// Shows one or more text strings, allowing individual glyph positioning. + /// The array can contain strings and numbers. Numbers specify an additional horizontal or vertical + /// displacement (depending on the writing mode) to apply before showing the next string or glyph. + ShowTextArray { + /// A vector of `PdfTextItem`s, where each item is either a string to show + /// or a numeric adjustment for positioning. + elements: Vec, + }, + /// Sets the character spacing, `Tc`, which is a number expressed in unscaled text space units. + SetCharacterSpacing { + /// The character spacing. Added to the horizontal displacement otherwise produced by showing a glyph. + spacing: f32, + }, + /// Sets the word spacing, `Tw`, which is a number expressed in unscaled text space units. + /// Word spacing is used by the `Tj`, `'`, and `"` operators. + SetWordSpacing { + /// The word spacing. Added to the character spacing when the character is a space (char code 32). + spacing: f32, + }, + /// Sets the horizontal scaling, `Tz`, which adjusts the width of glyphs by + /// stretching or compressing them horizontally. + SetHorizontalScaling { + /// The horizontal scaling factor as a percentage (e.g., 100.0 for 100% - no scaling). + scale: f32, + }, + /// Sets the text leading, `TL`, which is the vertical distance between the baselines of + /// adjacent lines of text. + SetLeading { + /// The text leading, in unscaled text space units. + leading: f32, + }, + /// Sets the text font, `Tf`, to a font resource in the resource dictionary and the text + /// font size, `Tfs`, in unscaled text space units. + SetFont { + /// The name of the font resource. + name: Arc<[u8]>, + /// The font size. + size: f32, + }, + /// Sets the text rendering mode, which determines whether text is filled, stroked, + /// used as a clipping path, or some combination. + SetRenderingMode { + /// The rendering mode. + mode: TextRenderingMode, + }, + /// Sets the text rise, `Ts`, which specifies the vertical distance + /// to shift the baseline of text relative to the current baseline. + SetTextRise { + /// The text rise, in unscaled text space units. A positive value moves the baseline up. + rise: f32, + }, + /// Invokes a named XObject. + /// XObjects are external objects such as images or self-contained page descriptions (Form XObjects). + InvokeXObject { + /// The name of the XObject resource to invoke, as defined in the resource dictionary. + name: Arc<[u8]>, + }, /// Begins a compatibility section (BX). /// /// According to the PDF specification, a compatibility section allows @@ -116,14 +355,56 @@ pub enum PdfOperatorVariant { InlineImage(Arc), /// Ends a compatibility section (EX). EndCompatibility, - PaintShading(PaintShading), - SetCharWidthAndBoundingBox(SetCharWidthAndBoundingBox), - SetCharWidth(SetCharWidth), - SetRenderingIntent(SetRenderingIntent), - SetStrokeColorSpace(SetStrokeColorSpace), - SetNonStrokingColorSpace(SetNonStrokingColorSpace), - SetStrokingColor(SetStrokingColor), - SetNonStrokingColor(SetNonStrokingColor), + /// Paints the shape and color shading defined by a shading dictionary resource. + /// The `sh` operator takes one operand, the name of a shading dictionary. + PaintShading { + /// The name of the shading dictionary resource from the Shading subdictionary + /// of the current resource dictionary. + name: Arc<[u8]>, + }, + SetCharWidthAndBoundingBox { + /// The x-component of the character width vector. + wx: f32, + /// The y-component of the character width vector. + wy: f32, + /// The x-coordinate of the lower-left corner of the character bounding box. + llx: f32, + /// The y-coordinate of the lower-left corner of the character bounding box. + lly: f32, + /// The x-coordinate of the upper-right corner of the character bounding box. + urx: f32, + /// The y-coordinate of the upper-right corner of the character bounding box. + ury: f32, + }, + SetCharWidth { + /// The x-component of the character width vector. + wx: f32, + /// The y-component of the character width vector. + wy: f32, + }, + /// Sets the rendering intent for color reproduction. (PDF operator `ri`) + /// + /// The operand is a name object indicating the intent, e.g., + /// /AbsoluteColorimetric, /RelativeColorimetric, /Saturation, /Perceptual. + SetRenderingIntent { + intent: Arc<[u8]>, + }, + /// Sets the stroke color space value. + SetStrokeColorSpace { + /// The name of the color space. + name: Arc<[u8]>, + }, + /// Sets the non-stroking (fill) color space value. + SetNonStrokingColorSpace { + /// The name of the color space. + name: Arc<[u8]>, + }, + /// Sets the stroking color when the color space requires + /// multiple color components. + SetStrokingColor(ColorOperands), + /// Sets the non-stroking color when the color space requires + /// multiple color components. + SetNonStrokingColor(ColorOperands), } impl PdfOperatorVariant { @@ -134,81 +415,182 @@ impl PdfOperatorVariant { /// from the content stream instead. pub fn read(name: &[u8], operands: &mut OperandContext<'_, '_>) -> ReadResult> { let operator = match (name, operands.array().len()) { - (b"\"", 3) => Self::SetSpacingMoveShowText(SetSpacingMoveShowText::read(operands)?), - (b"'", 1) => Self::MoveNextLineShowText(MoveNextLineShowText::read(operands)?), + (b"\"", 3) => Self::SetSpacingMoveShowText { + word_spacing: operands.at(0)?, + char_spacing: operands.at(1)?, + text: PdfTextItem::Text(operands.at::(2)?.bytes), + }, + (b"'", 1) => Self::MoveNextLineShowText { + text: PdfTextItem::Text(operands.at::(0)?.bytes), + }, (b"B", 0) => Self::FillAndStrokePathNonZero, (b"B*", 0) => Self::FillAndStrokePathEvenOdd, (b"BDC", 2) => { Self::BeginMarkedContentWithProps(BeginMarkedContentWithProps::read(operands)?) } - (b"BMC", 1) => Self::BeginMarkedContent(BeginMarkedContent::read(operands)?), + (b"BMC", 1) => Self::BeginMarkedContent { + tag: operands.at::(0)?.bytes, + }, (b"BT", 0) => Self::BeginText, (b"BX", 0) => Self::BeginCompatibility, - (b"CS", 1) => Self::SetStrokeColorSpace(SetStrokeColorSpace::read(operands)?), - (b"Do", 1) => Self::InvokeXObject(InvokeXObject::read(operands)?), + (b"CS", 1) => Self::SetStrokeColorSpace { + name: operands.at::(0)?.bytes, + }, + (b"Do", 1) => Self::InvokeXObject { + name: operands.at::(0)?.bytes, + }, (b"EMC", 0) => Self::EndMarkedContent, (b"ET", 0) => Self::EndText, (b"EX", 0) => Self::EndCompatibility, - (b"G", 1) => Self::SetGrayStroke(SetGrayStroke::read(operands)?), - (b"J", 1) => Self::SetLineCapStyle(SetLineCapStyle::read(operands)?), - (b"K", 4) => Self::SetCMYKStroke(SetCMYKStroke::read(operands)?), - (b"M", 1) => Self::SetMiterLimit(SetMiterLimit::read(operands)?), + (b"G", 1) => Self::SetGrayStroke { + gray: operands.at(0)?, + }, + (b"J", 1) => Self::SetLineCapStyle { + style: operands.at(0)?, + }, + (b"K", 4) => operands + .read_array() + .map(|[c, m, y, k]| Self::SetCMYKStroke { c, m, y, k })?, + (b"M", 1) => Self::SetMiterLimit { + limit: operands.at(0)?, + }, (b"Q", 0) => Self::RestoreGraphicsState, - (b"RG", 3) => Self::SetRGBStroke(SetRGBStroke::read(operands)?), + (b"RG", 3) => operands + .read_array() + .map(|[r, g, b]| Self::SetRGBStroke { r, g, b })?, (b"S", 0) => Self::StrokePath, - (b"SC", _) => Self::SetStrokingColor(SetStrokingColor::read_without_pattern(operands)?), - (b"SCN", _) => Self::SetStrokingColor(SetStrokingColor::read(operands)?), - (b"T*", 0) => Self::MoveToNextLine, - (b"TD", 2) => { - Self::MoveTextPositionAndSetLeading(MoveTextPositionAndSetLeading::read(operands)?) + (b"SC", _) => { + Self::SetStrokingColor(ColorOperands::read_without_pattern(operands, "SC")?) } - (b"TJ", 1) => Self::ShowTextArray(ShowTextArray::read(operands)?), - (b"TL", 1) => Self::SetLeading(SetLeading::read(operands)?), - (b"Tc", 1) => Self::SetCharacterSpacing(SetCharacterSpacing::read(operands)?), - (b"Td", 2) => Self::MoveTextPosition(MoveTextPosition::read(operands)?), - (b"Tf", 2) => Self::SetFont(SetFont::read(operands)?), - (b"Tj", 1) => Self::ShowText(ShowText::read(operands)?), - (b"Tm", 6) => Self::SetTextMatrix(SetTextMatrix::read(operands)?), - (b"Tr", 1) => Self::SetRenderingMode(SetRenderingMode::read(operands)?), - (b"Ts", 1) => Self::SetTextRise(SetTextRise::read(operands)?), - (b"Tw", 1) => Self::SetWordSpacing(SetWordSpacing::read(operands)?), - (b"Tz", 1) => Self::SetHorizontalScaling(SetHorizontalScaling::read(operands)?), + (b"SCN", _) => Self::SetStrokingColor(ColorOperands::read(operands)?), + (b"T*", 0) => Self::MoveToNextLine, + (b"TD", 2) => operands + .read_array() + .map(|[tx, ty]| Self::MoveTextPositionAndSetLeading { tx, ty })?, + (b"TJ", 1) => Self::ShowTextArray { + elements: operands.at(0)?, + }, + (b"TL", 1) => Self::SetLeading { + leading: operands.at(0)?, + }, + (b"Tc", 1) => Self::SetCharacterSpacing { + spacing: operands.at(0)?, + }, + (b"Td", 2) => operands + .read_array() + .map(|[tx, ty]| Self::MoveTextPosition { tx, ty })?, + (b"Tf", 2) => Self::SetFont { + name: operands.at::(0)?.bytes, + size: operands.at(1)?, + }, + (b"Tj", 1) => Self::ShowText { + text: PdfTextItem::Text(operands.at::(0)?.bytes), + }, + (b"Tm", 6) => Self::SetTextMatrix { + matrix: Transform::from(operands.read_array::()?), + }, + (b"Tr", 1) => Self::SetRenderingMode { + mode: operands.at(0)?, + }, + (b"Ts", 1) => Self::SetTextRise { + rise: operands.at(0)?, + }, + (b"Tw", 1) => Self::SetWordSpacing { + spacing: operands.at(0)?, + }, + (b"Tz", 1) => Self::SetHorizontalScaling { + scale: operands.at(0)?, + }, (b"W", 0) => Self::ClipNonZero, (b"W*", 0) => Self::ClipEvenOdd, (b"b", 0) => Self::CloseFillAndStrokePathNonZero, (b"b*", 0) => Self::CloseFillAndStrokePathEvenOdd, - (b"c", 6) => Self::CurveTo(CurveTo::read(operands)?), - (b"cm", 6) => Self::ConcatMatrix(ConcatMatrix::read(operands)?), - (b"cs", 1) => Self::SetNonStrokingColorSpace(SetNonStrokingColorSpace::read(operands)?), - (b"d", 2) => Self::SetDashPattern(SetDashPattern::read(operands)?), - (b"d0", 2) => Self::SetCharWidth(SetCharWidth::read(operands)?), - (b"d1", 6) => { - Self::SetCharWidthAndBoundingBox(SetCharWidthAndBoundingBox::read(operands)?) - } + (b"c", 6) => operands + .read_array() + .map(|[x1, y1, x2, y2, x3, y3]| Self::CurveTo { + x1, + y1, + x2, + y2, + x3, + y3, + })?, + (b"cm", 6) => Self::ConcatMatrix { + matrix: Transform::from(operands.read_array::()?), + }, + (b"cs", 1) => Self::SetNonStrokingColorSpace { + name: operands.at::(0)?.bytes, + }, + (b"d", 2) => Self::SetDashPattern { + array: operands.at(0)?, + phase: operands.at(1)?, + }, + (b"d0", 2) => operands + .read_array() + .map(|[wx, wy]| Self::SetCharWidth { wx, wy })?, + (b"d1", 6) => operands.read_array().map(|[wx, wy, llx, lly, urx, ury]| { + Self::SetCharWidthAndBoundingBox { + wx, + wy, + llx, + lly, + urx, + ury, + } + })?, (b"f", 0) => Self::FillPathNonZero, (b"f*", 0) => Self::FillPathEvenOdd, - (b"g", 1) => Self::SetGrayFill(SetGrayFill::read(operands)?), - (b"gs", 1) => Self::SetGraphicsStateFromDict(SetGraphicsStateFromDict::read(operands)?), + (b"g", 1) => Self::SetGrayFill { + gray: operands.at(0)?, + }, + (b"gs", 1) => Self::SetGraphicsStateFromDict { + dict_name: operands.at::(0)?.bytes, + }, (b"h", 0) => Self::ClosePath, - (b"i", 1) => Self::SetFlatnessTolerance(SetFlatnessTolerance::read(operands)?), - (b"j", 1) => Self::SetLineJoinStyle(SetLineJoinStyle::read(operands)?), - (b"k", 4) => Self::SetCMYKFill(SetCMYKFill::read(operands)?), - (b"l", 2) => Self::LineTo(LineTo::read(operands)?), - (b"m", 2) => Self::MoveTo(MoveTo::read(operands)?), + (b"i", 1) => Self::SetFlatnessTolerance { + tolerance: operands.at(0)?, + }, + (b"j", 1) => Self::SetLineJoinStyle { + style: operands.at(0)?, + }, + (b"k", 4) => operands + .read_array() + .map(|[c, m, y, k]| Self::SetCMYKFill { c, m, y, k })?, + (b"l", 2) => operands.read_array().map(|[x, y]| Self::LineTo { x, y })?, + (b"m", 2) => operands.read_array().map(|[x, y]| Self::MoveTo { x, y })?, (b"n", 0) => Self::EndPath, (b"q", 0) => Self::SaveGraphicsState, - (b"re", 4) => Self::Rectangle(Rectangle::read(operands)?), - (b"rg", 3) => Self::SetRGBFill(SetRGBFill::read(operands)?), - (b"ri", 1) => Self::SetRenderingIntent(SetRenderingIntent::read(operands)?), + (b"re", 4) => operands + .read_array() + .map(|[x, y, width, height]| Self::Rectangle { + x, + y, + width, + height, + })?, + (b"rg", 3) => operands + .read_array() + .map(|[r, g, b]| Self::SetRGBFill { r, g, b })?, + (b"ri", 1) => Self::SetRenderingIntent { + intent: operands.at::(0)?.bytes, + }, (b"s", 0) => Self::CloseStrokePath, (b"sc", _) => { - Self::SetNonStrokingColor(SetNonStrokingColor::read_without_pattern(operands)?) + Self::SetNonStrokingColor(ColorOperands::read_without_pattern(operands, "sc")?) } - (b"scn", _) => Self::SetNonStrokingColor(SetNonStrokingColor::read(operands)?), - (b"sh", 1) => Self::PaintShading(PaintShading::read(operands)?), - (b"v", 4) => Self::CurveToV(CurveToV::read(operands)?), - (b"w", 1) => Self::SetLineWidth(SetLineWidth::read(operands)?), - (b"y", 4) => Self::CurveToY(CurveToY::read(operands)?), + (b"scn", _) => Self::SetNonStrokingColor(ColorOperands::read(operands)?), + (b"sh", 1) => Self::PaintShading { + name: operands.at::(0)?.bytes, + }, + (b"v", 4) => operands + .read_array() + .map(|[x2, y2, x3, y3]| Self::CurveToV { x2, y2, x3, y3 })?, + (b"w", 1) => Self::SetLineWidth { + width: operands.at(0)?, + }, + (b"y", 4) => operands + .read_array() + .map(|[x1, y1, x3, y3]| Self::CurveToY { x1, y1, x3, y3 })?, _ => return Ok(None), }; Ok(Some(operator)) @@ -217,13 +599,25 @@ impl PdfOperatorVariant { /// Dispatches the operation to `backend`. pub fn call(&self, backend: &mut T) -> Result<(), BackendError> { match self { - Self::LineTo(op) => op.call(backend), - Self::MoveTo(op) => op.call(backend), - Self::CurveTo(op) => op.call(backend), - Self::CurveToV(op) => op.call(backend), - Self::CurveToY(op) => op.call(backend), + Self::LineTo { x, y } => backend.line_to(*x, *y), + Self::MoveTo { x, y } => backend.move_to(*x, *y), + Self::CurveTo { + x1, + y1, + x2, + y2, + x3, + y3, + } => backend.curve_to(*x1, *y1, *x2, *y2, *x3, *y3), + Self::CurveToV { x2, y2, x3, y3 } => backend.curve_to_v(*x2, *y2, *x3, *y3), + Self::CurveToY { x1, y1, x3, y3 } => backend.curve_to_y(*x1, *y1, *x3, *y3), Self::ClosePath => backend.close_path(), - Self::Rectangle(op) => op.call(backend), + Self::Rectangle { + x, + y, + width, + height, + } => backend.rectangle(*x, *y, *width, *height), Self::StrokePath => backend.stroke_path(), Self::CloseStrokePath => backend.close_and_stroke_path(), Self::FillPathNonZero => backend.fill_path_nonzero_winding(), @@ -237,54 +631,183 @@ impl PdfOperatorVariant { Self::EndPath => backend.end_path_no_op(), Self::ClipNonZero => backend.clip_path_nonzero_winding(), Self::ClipEvenOdd => backend.clip_path_even_odd(), - Self::SetGrayFill(op) => op.call(backend), - Self::SetGrayStroke(op) => op.call(backend), - Self::SetRGBFill(op) => op.call(backend), - Self::SetRGBStroke(op) => op.call(backend), - Self::SetCMYKFill(op) => op.call(backend), - Self::SetCMYKStroke(op) => op.call(backend), - Self::SetLineWidth(op) => op.call(backend), - Self::SetLineCapStyle(op) => op.call(backend), - Self::SetLineJoinStyle(op) => op.call(backend), - Self::SetMiterLimit(op) => op.call(backend), - Self::SetDashPattern(op) => op.call(backend), - Self::SetFlatnessTolerance(op) => op.call(backend), - Self::SetGraphicsStateFromDict(op) => op.call(backend), + Self::SetGrayFill { gray } => backend.set_non_stroking_gray(*gray), + Self::SetGrayStroke { gray } => backend.set_stroking_gray(*gray), + Self::SetRGBFill { r, g, b } => backend.set_non_stroking_rgb(*r, *g, *b), + Self::SetRGBStroke { r, g, b } => backend.set_stroking_rgb(*r, *g, *b), + Self::SetCMYKFill { c, m, y, k } => backend.set_non_stroking_cmyk(*c, *m, *y, *k), + Self::SetCMYKStroke { c, m, y, k } => backend.set_stroking_cmyk(*c, *m, *y, *k), + Self::SetLineWidth { width } => backend.set_line_width(*width), + Self::SetLineCapStyle { style } => backend.set_line_cap(*style), + Self::SetLineJoinStyle { style } => backend.set_line_join(*style), + Self::SetMiterLimit { limit } => backend.set_miter_limit(*limit), + Self::SetDashPattern { array, phase } => backend.set_dash_pattern(array, *phase), + Self::SetFlatnessTolerance { tolerance } => backend.set_flatness_tolerance(*tolerance), + Self::SetGraphicsStateFromDict { dict_name } => { + backend.set_graphics_state_from_dict(dict_name) + } Self::SaveGraphicsState => backend.save_graphics_state(), Self::RestoreGraphicsState => backend.restore_graphics_state(), - Self::ConcatMatrix(op) => op.call(backend), - Self::BeginMarkedContent(op) => op.call(backend), + Self::ConcatMatrix { matrix } => backend.concat_matrix(matrix), + Self::BeginMarkedContent { tag } => backend.begin_marked_content(tag), Self::BeginMarkedContentWithProps(op) => op.call(backend), Self::EndMarkedContent => backend.end_marked_content(), Self::BeginText => backend.begin_text_object(), Self::EndText => backend.end_text_object(), - Self::MoveTextPosition(op) => op.call(backend), - Self::MoveTextPositionAndSetLeading(op) => op.call(backend), - Self::SetTextMatrix(op) => op.call(backend), + Self::MoveTextPosition { tx, ty } => backend.move_text_position(*tx, *ty), + Self::MoveTextPositionAndSetLeading { tx, ty } => { + backend.move_text_position_and_set_leading(*tx, *ty) + } + Self::SetTextMatrix { matrix } => backend.set_text_matrix(matrix), Self::MoveToNextLine => backend.move_to_start_of_next_line(), - Self::ShowText(op) => op.call(backend), - Self::MoveNextLineShowText(op) => op.call(backend), - Self::SetSpacingMoveShowText(op) => op.call(backend), - Self::ShowTextArray(op) => op.call(backend), - Self::SetCharacterSpacing(op) => op.call(backend), - Self::SetWordSpacing(op) => op.call(backend), - Self::SetHorizontalScaling(op) => op.call(backend), - Self::SetLeading(op) => op.call(backend), - Self::SetFont(op) => op.call(backend), - Self::SetRenderingMode(op) => op.call(backend), - Self::SetTextRise(op) => op.call(backend), - Self::InvokeXObject(op) => op.call(backend), + Self::ShowText { text } => backend.show_text(text), + Self::MoveNextLineShowText { text } => backend.move_to_next_line_and_show_text(text), + Self::SetSpacingMoveShowText { + word_spacing, + char_spacing, + text, + } => backend.set_spacing_and_show_text(*word_spacing, *char_spacing, text), + Self::ShowTextArray { elements } => backend.show_text_with_glyph_positioning(elements), + Self::SetCharacterSpacing { spacing } => backend.set_character_spacing(*spacing), + Self::SetWordSpacing { spacing } => backend.set_word_spacing(*spacing), + Self::SetHorizontalScaling { scale } => backend.set_horizontal_text_scaling(*scale), + Self::SetLeading { leading } => backend.set_text_leading(*leading), + Self::SetFont { name, size } => backend.set_font_and_size(name, *size), + Self::SetRenderingMode { mode } => backend.set_text_rendering_mode(*mode), + Self::SetTextRise { rise } => backend.set_text_rise(*rise), + Self::InvokeXObject { name } => backend.invoke_xobject(name), Self::BeginCompatibility => Ok(()), Self::InlineImage(image) => backend.paint_inline_image(image), Self::EndCompatibility => Ok(()), - Self::PaintShading(op) => op.call(backend), - Self::SetCharWidthAndBoundingBox(op) => op.call(backend), - Self::SetCharWidth(op) => op.call(backend), - Self::SetRenderingIntent(op) => op.call(backend), - Self::SetStrokeColorSpace(op) => op.call(backend), - Self::SetNonStrokingColorSpace(op) => op.call(backend), - Self::SetStrokingColor(op) => op.call(backend), - Self::SetNonStrokingColor(op) => op.call(backend), + Self::PaintShading { name } => backend.paint_shading(name), + Self::SetCharWidthAndBoundingBox { .. } => Ok(()), + Self::SetCharWidth { .. } => Ok(()), + Self::SetRenderingIntent { intent } => backend.set_rendering_intent(intent), + Self::SetStrokeColorSpace { name } => backend.set_stroking_color_space(name), + Self::SetNonStrokingColorSpace { name } => backend.set_non_stroking_color_space(name), + Self::SetStrokingColor(color) => match &color.pattern { + Some(pattern) => backend.set_stroking_color_extended(&color.components, pattern), + None => backend.set_stroking_color(&color.components), + }, + Self::SetNonStrokingColor(color) => match &color.pattern { + Some(pattern) => { + backend.set_non_stroking_color_extended(&color.components, pattern) + } + None => backend.set_non_stroking_color(&color.components), + }, } } } + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] +mod tests { + use std::collections::BTreeMap; + + use pdf_object_reader::{ + ArrayContext, ObjectReader, dictionary::Dictionary, object_resolver::PassthroughResolver, + object_variant::ObjectVariant, + }; + + use crate::recording_pdf_operator_backend::{RecordedOperation, RecordingBackend}; + + use super::*; + + #[test] + fn test_path_operations() { + let mut backend = RecordingBackend::default(); + let a = PdfOperatorVariant::MoveTo { x: 0.0, y: 0.0 }; + a.call(&mut backend).unwrap(); + } + + #[test] + fn show_text_moves_the_operand_byte_allocation() { + let text: Arc<[u8]> = Arc::from(b"text without a copy".to_vec()); + + let original_pointer = text.as_ptr(); + + 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 = + PdfOperatorVariant::read(b"Tj", &mut ArrayContext::new(&operands, &mut session)) + .expect("text should parse"); + let Some(PdfOperatorVariant::ShowText { + text: PdfTextItem::Text(text), + }) = operator + else { + panic!("expected a ShowText text item"); + }; + assert_eq!(&*text, b"text without a copy"); + assert_eq!(text.as_ptr(), original_pointer); + } + + #[test] + fn spacing_move_show_text_groups_numeric_prefix_before_text() { + 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 = + PdfOperatorVariant::read(b"\"", &mut ArrayContext::new(&operands, &mut session)) + .expect("mixed operands should parse successfully"); + let Some(PdfOperatorVariant::SetSpacingMoveShowText { + word_spacing, + char_spacing, + text, + }) = operator + else { + panic!("expected SetSpacingMoveShowText"); + }; + assert_eq!(word_spacing, 1.5); + assert_eq!(char_spacing, 2.0); + assert_eq!(text, PdfTextItem::Text(Arc::from(&b"text"[..]))); + } + + #[test] + fn inline_image_call_dispatches_to_backend_hook() { + let image = InlineImage::new( + Dictionary::new(BTreeMap::from([ + (Vec::from(b"BPC"), ObjectVariant::Integer(8)), + ( + Vec::from(b"CS"), + pdf_object_reader::pdf_string::PdfString::from( + b"G", + pdf_object_reader::string_kind::StringKind::Name, + ), + ), + (Vec::from(b"H"), ObjectVariant::Integer(1)), + (Vec::from(b"W"), ObjectVariant::Integer(2)), + ])), + vec![0x01, 0x02], + &mut ObjectReader::new(PassthroughResolver).session(), + ) + .expect("unfiltered inline image should be constructed"); + let image = Arc::new(image); + let mut backend = RecordingBackend::default(); + + PdfOperatorVariant::InlineImage(Arc::clone(&image)) + .call(&mut backend) + .expect("inline image should dispatch"); + + assert_eq!( + backend.operations, + vec![RecordedOperation::PaintInlineImage { + data: image.shared_data() + }] + ); + } +} 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 deleted file mode 100644 index 8d4f2a5f..00000000 --- a/crates/pdf-content-stream-operators/src/xobject_and_image_operators.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::sync::Arc; - -use pdf_object_reader::{ReadResult, pdf_string::PdfString}; - -use crate::{ - OperandContext, - pdf_operator_backend::{BackendError, PdfOperatorBackend}, -}; - -/// Invokes a named XObject. -/// XObjects are external objects such as images or self-contained page descriptions (Form XObjects). -#[derive(Debug, Clone, PartialEq)] -pub struct InvokeXObject { - /// The name of the XObject resource to invoke, as defined in the resource dictionary. - name: Arc<[u8]>, -} - -impl InvokeXObject { - pub fn new(name: Arc<[u8]>) -> Self { - Self { name } - } - - /// Reads the operator from its operands. - pub(crate) fn read(operands: &mut OperandContext<'_, '_>) -> ReadResult { - let name = operands.at::(0)?.bytes; - Ok(Self::new(name)) - } - - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.invoke_xobject(&self.name) - } -} - -#[cfg(test)] -mod tests { - use std::{collections::BTreeMap, sync::Arc}; - - use pdf_image::InlineImage; - use pdf_object_reader::{ - ObjectReader, dictionary::Dictionary, object_resolver::PassthroughResolver, - object_variant::ObjectVariant, - }; - - use crate::{ - recording_pdf_operator_backend::{RecordedOperation, RecordingBackend}, - variants::PdfOperatorVariant, - }; - - #[test] - fn inline_image_call_dispatches_to_backend_hook() { - let image = InlineImage::new( - Dictionary::new(BTreeMap::from([ - (Vec::from(b"BPC"), ObjectVariant::Integer(8)), - ( - Vec::from(b"CS"), - pdf_object_reader::pdf_string::PdfString::from( - b"G", - pdf_object_reader::string_kind::StringKind::Name, - ), - ), - (Vec::from(b"H"), ObjectVariant::Integer(1)), - (Vec::from(b"W"), ObjectVariant::Integer(2)), - ])), - vec![0x01, 0x02], - &mut ObjectReader::new(PassthroughResolver).session(), - ) - .expect("unfiltered inline image should be constructed"); - let image = Arc::new(image); - let mut backend = RecordingBackend::default(); - - PdfOperatorVariant::InlineImage(Arc::clone(&image)) - .call(&mut backend) - .expect("inline image should dispatch"); - - assert_eq!( - backend.operations, - vec![RecordedOperation::PaintInlineImage { - data: image.shared_data() - }] - ); - } -} diff --git a/crates/pdf-content-stream/src/content_stream.rs b/crates/pdf-content-stream/src/content_stream.rs index 4c10ca56..ef9a2cc1 100644 --- a/crates/pdf-content-stream/src/content_stream.rs +++ b/crates/pdf-content-stream/src/content_stream.rs @@ -75,7 +75,6 @@ mod tests { use pdf_content_stream_operators::{ PdfTextItem, recording_pdf_operator_backend::{RecordedOperation, RecordingBackend}, - text_showing_operators::ShowTextArray, variants::PdfOperatorVariant, }; use pdf_object_reader::{ @@ -194,9 +193,9 @@ mod tests { assert!(matches!( parsed.operators.get(3), - Some(PdfOperatorVariant::ShowTextArray(op)) - if op - == &ShowTextArray::new(vec![ + Some(PdfOperatorVariant::ShowTextArray { elements }) + if elements + == &vec![ PdfTextItem::Text(Arc::from(&b"e"[..])), PdfTextItem::Adjustment(-4.0), PdfTextItem::Text(Arc::from(&b"x"[..])), @@ -218,7 +217,7 @@ mod tests { PdfTextItem::Text(Arc::from(&b"l"[..])), PdfTextItem::Adjustment(3.0), PdfTextItem::Text(Arc::from(&b"y"[..])), - ]) + ] )); } diff --git a/crates/pdf-content-stream/src/operator_stream_parser.rs b/crates/pdf-content-stream/src/operator_stream_parser.rs index a527a369..8fdb7f1b 100644 --- a/crates/pdf-content-stream/src/operator_stream_parser.rs +++ b/crates/pdf-content-stream/src/operator_stream_parser.rs @@ -219,7 +219,10 @@ mod tests { &mut ArrayContext::new(&operands, &mut reader.session()), ); - assert!(matches!(operator, Ok(Some(PdfOperatorVariant::MoveTo(_))))); + assert!(matches!( + operator, + Ok(Some(PdfOperatorVariant::MoveTo { .. })) + )); assert_eq!(operands.len(), 2); } @@ -330,11 +333,11 @@ mod tests { assert_eq!(parser.out.len(), 2); assert!(matches!( parser.out.first(), - Some(PdfOperatorVariant::SetLineJoinStyle(_)) + Some(PdfOperatorVariant::SetLineJoinStyle { .. }) )); assert!(matches!( parser.out.get(1), - Some(PdfOperatorVariant::SetLineCapStyle(_)) + Some(PdfOperatorVariant::SetLineCapStyle { .. }) )); } @@ -374,7 +377,7 @@ mod tests { assert_eq!(parser.out.len(), 1); assert!(matches!( parser.out.first(), - Some(PdfOperatorVariant::SetLineCapStyle(_)) + Some(PdfOperatorVariant::SetLineCapStyle { .. }) )); } diff --git a/crates/pdf-content-stream/tests/parser_api.rs b/crates/pdf-content-stream/tests/parser_api.rs index 0cb1abab..27d4dab1 100644 --- a/crates/pdf-content-stream/tests/parser_api.rs +++ b/crates/pdf-content-stream/tests/parser_api.rs @@ -5,7 +5,6 @@ use pdf_content_stream::ContentStream; use pdf_content_stream_operators::{ PdfTextItem, recording_pdf_operator_backend::{RecordedOperation, RecordingBackend}, - text_showing_operators::ShowTextArray, variants::PdfOperatorVariant, }; use pdf_object_reader::{ @@ -122,9 +121,9 @@ fn content_stream_read_handles_bare_sign_text_array_adjustment() { assert!(matches!( parsed.operators.get(3), - Some(PdfOperatorVariant::ShowTextArray(op)) - if op - == &ShowTextArray::new(vec![ + Some(PdfOperatorVariant::ShowTextArray { elements }) + if elements + == &vec![ PdfTextItem::Text(Arc::from(&b"e"[..])), PdfTextItem::Adjustment(-4.0), PdfTextItem::Text(Arc::from(&b"x"[..])), @@ -146,7 +145,7 @@ fn content_stream_read_handles_bare_sign_text_array_adjustment() { PdfTextItem::Text(Arc::from(&b"l"[..])), PdfTextItem::Adjustment(3.0), PdfTextItem::Text(Arc::from(&b"y"[..])), - ]) + ] )); } diff --git a/crates/pdf-object-reader/src/decode.rs b/crates/pdf-object-reader/src/decode.rs index 121f7c11..63440ddc 100644 --- a/crates/pdf-object-reader/src/decode.rs +++ b/crates/pdf-object-reader/src/decode.rs @@ -1,7 +1,11 @@ //! Extensible typed decoding with one object-bound context per invocation. +use num_traits::FromPrimitive; +use pdf_graphics::{LineCap, LineJoin, TextRenderingMode}; + use crate::context::ObjectContext; use crate::error::{ObjectReadError, ReadResult}; +use crate::object_error::ObjectError; use crate::object_kind::ObjectKind; use crate::object_variant::ObjectVariant; use crate::pdf_array::PdfArray; @@ -181,6 +185,31 @@ macro_rules! number_decoder { number_decoder!(f32, i32, i64, u8, u16, u32, usize); +macro_rules! enumerated_number_decoder { + ($($type:ty => $expected:literal),+ $(,)?) => { + $( + impl FromPdfObject for $type { + /// Decodes the number that names one value of this parameter. + fn from_pdf_object( + context: ObjectContext<'_, impl ObjectAccess + ?Sized>, + ) -> ReadResult { + let found = context.object().try_number::(context.source())?; + Ok(Self::from_i32(found).ok_or(ObjectError::InvalidEnumValue { + expected: $expected, + found, + })?) + } + } + )+ + }; +} + +enumerated_number_decoder!( + LineCap => "a line cap style (0 butt, 1 round, 2 projecting square)", + LineJoin => "a line join style (0 miter, 1 round, 2 bevel)", + TextRenderingMode => "a text rendering mode (0 to 7)", +); + impl FromPdfObject for bool { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { match context.object() { diff --git a/crates/pdf-object-reader/src/object_error.rs b/crates/pdf-object-reader/src/object_error.rs index 78fe2450..7898da43 100644 --- a/crates/pdf-object-reader/src/object_error.rs +++ b/crates/pdf-object-reader/src/object_error.rs @@ -29,6 +29,9 @@ pub enum ObjectError { FailedResolveStreamObject { resolved_type: &'static str }, #[error("Invalid array length: expected {expected}, but found {found}")] InvalidArrayLength { expected: usize, found: usize }, + /// Indicates a number that does not name any value of an enumerated parameter. + #[error("Invalid value {found}; expected {expected}")] + InvalidEnumValue { expected: &'static str, found: i32 }, /// Indicates that an attempt was made to insert an object into a collection /// but the object does not have an associated object number (is not a reference, /// indirect object, or stream with object number). diff --git a/crates/pdf-resources/src/error.rs b/crates/pdf-resources/src/error.rs index 8b9ec524..ceb8701c 100644 --- a/crates/pdf-resources/src/error.rs +++ b/crates/pdf-resources/src/error.rs @@ -33,12 +33,6 @@ pub enum PdfPagesError { InvalidExtGStateArrayLength { entry: &'static str, found: usize }, #[error("invalid /ExtGState entry '/D': {0}")] InvalidExtGStateDashPattern(#[from] DashPatternError), - #[error("invalid /ExtGState entry '/LC': unsupported line cap value {0} (expected 0, 1, or 2)")] - InvalidExtGStateLineCap(i32), - #[error( - "invalid /ExtGState entry '/LJ': unsupported line join value {0} (expected 0, 1, or 2)" - )] - InvalidExtGStateLineJoin(i32), #[error( "invalid /ExtGState entry '/SMask': expected a soft mask dictionary or the name 'None'" )] diff --git a/crates/pdf-resources/src/external_graphics_state.rs b/crates/pdf-resources/src/external_graphics_state.rs index b4cfd84f..27e50421 100644 --- a/crates/pdf-resources/src/external_graphics_state.rs +++ b/crates/pdf-resources/src/external_graphics_state.rs @@ -4,7 +4,6 @@ use pdf_object_reader::{ }; use crate::{error::PdfPagesError, resource::Resource, soft_mask::SoftMask}; -use num_traits::FromPrimitive; use pdf_graphics::{BlendMode, DashPattern, LineCap, LineJoin}; /// One parsed entry of a graphics state parameter dictionary (`ExtGState`). @@ -138,18 +137,8 @@ fn parse_entry( } let parsed = match name { b"LW" => ExternalGraphicsStateKey::LineWidth(context.read(value)?), - b"LC" => { - let cap = context.read::(value)?; - ExternalGraphicsStateKey::LineCap( - LineCap::from_i32(cap).ok_or(PdfPagesError::InvalidExtGStateLineCap(cap))?, - ) - } - b"LJ" => { - let join = context.read::(value)?; - ExternalGraphicsStateKey::LineJoin( - LineJoin::from_i32(join).ok_or(PdfPagesError::InvalidExtGStateLineJoin(join))?, - ) - } + b"LC" => ExternalGraphicsStateKey::LineCap(context.read(value)?), + b"LJ" => ExternalGraphicsStateKey::LineJoin(context.read(value)?), b"ML" => ExternalGraphicsStateKey::MiterLimit(context.read(value)?), b"D" => match context.read::(value)? { DashEntry(Some(pattern)) => ExternalGraphicsStateKey::DashPattern(pattern),