diff --git a/AGENTS.md b/AGENTS.md index e044d122..e8b860c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,7 +60,7 @@ PDF bytes → pdf-tokenizer → pdf-parser → pdf-object → pdf-document Key architectural traits: - **`CanvasBackend`** (in `pdf-canvas`): Abstracts rendering. Skia and FemtoVG are current implementations. -- **`PdfOperatorBackend`**: Content stream operators dispatched via traits, enabling substitution with analyzers/exporters without modifying core logic. +- **`PdfOperatorVariant`** (in `pdf-content-stream-operators`): A closed enum of parsed content stream operators. Consumers (the canvas, analyzers, exporters) match on it directly; `PdfCanvas::apply_operator` dispatches it for rendering. Supporting crates: - `pdf-font`: Font decoding (Type1/TrueType/Type3), isolated from rendering diff --git a/crates/pdf-canvas/src/canvas_clip_ops.rs b/crates/pdf-canvas/src/canvas_clip_ops.rs index 5dcde4ad..ac02defc 100644 --- a/crates/pdf-canvas/src/canvas_clip_ops.rs +++ b/crates/pdf-canvas/src/canvas_clip_ops.rs @@ -1,15 +1,15 @@ -use pdf_content_stream_operators::pdf_operator_backend::ClippingPathOps; use pdf_graphics::PathFillType; use crate::{canvas_backend::CanvasBackend, error::PdfCanvasError, pdf_canvas::PdfCanvas}; -impl ClippingPathOps for PdfCanvas<'_, B> { - type ErrorType = PdfCanvasError; - fn clip_path_nonzero_winding(&mut self) -> Result<(), Self::ErrorType> { +impl PdfCanvas<'_, B> { + /// Handles `W`: marks the current path as a nonzero-winding clip. + pub(crate) fn clip_path_nonzero_winding(&mut self) -> Result<(), PdfCanvasError> { self.mark_clip_path(PathFillType::Winding) } - fn clip_path_even_odd(&mut self) -> Result<(), Self::ErrorType> { + /// Handles `W*`: marks the current path as an even-odd clip. + pub(crate) fn clip_path_even_odd(&mut self) -> Result<(), PdfCanvasError> { self.mark_clip_path(PathFillType::EvenOdd) } } diff --git a/crates/pdf-canvas/src/canvas_color_ops.rs b/crates/pdf-canvas/src/canvas_color_ops.rs index be38e4fa..cd71cb24 100644 --- a/crates/pdf-canvas/src/canvas_color_ops.rs +++ b/crates/pdf-canvas/src/canvas_color_ops.rs @@ -1,6 +1,5 @@ use pdf_color_space::color_space::ColorSpace; use pdf_color_space::error::ColorSpaceError; -use pdf_content_stream_operators::pdf_operator_backend::ColorOps; use std::sync::Arc; use crate::{ @@ -28,17 +27,22 @@ fn apply_content_stream_color( } } -impl ColorOps for PdfCanvas<'_, B> { - type ErrorType = PdfCanvasError; - fn set_stroking_color_space(&mut self, name: &[u8]) -> Result<(), Self::ErrorType> { +impl PdfCanvas<'_, B> { + /// Handles `CS`: selects the stroking color space. + pub(crate) fn set_stroking_color_space(&mut self, name: &[u8]) -> Result<(), PdfCanvasError> { self.set_color_space(name, true) } - fn set_non_stroking_color_space(&mut self, name: &[u8]) -> Result<(), Self::ErrorType> { + /// Handles `cs`: selects the non-stroking color space. + pub(crate) fn set_non_stroking_color_space( + &mut self, + name: &[u8], + ) -> Result<(), PdfCanvasError> { self.set_color_space(name, false) } - fn set_stroking_color(&mut self, components: &[f32]) -> Result<(), Self::ErrorType> { + /// Handles `SC`/`SCN` without a pattern: sets the stroking color. + pub(crate) fn set_stroking_color(&mut self, components: &[f32]) -> Result<(), PdfCanvasError> { let state = self.current_state_mut()?; state.stroke_pattern = None; @@ -52,7 +56,11 @@ impl ColorOps for PdfCanvas<'_, B> { Ok(()) } - fn set_non_stroking_color(&mut self, components: &[f32]) -> Result<(), Self::ErrorType> { + /// Handles `sc`/`scn` without a pattern: sets the non-stroking color. + pub(crate) fn set_non_stroking_color( + &mut self, + components: &[f32], + ) -> Result<(), PdfCanvasError> { let state = self.current_state_mut()?; state.fill_pattern = None; @@ -66,11 +74,12 @@ impl ColorOps for PdfCanvas<'_, B> { Ok(()) } - fn set_non_stroking_color_extended( + /// Handles `scn` with a pattern name: sets the non-stroking pattern. + pub(crate) fn set_non_stroking_color_extended( &mut self, components: &[f32], pattern_name: &[u8], - ) -> Result<(), Self::ErrorType> { + ) -> Result<(), PdfCanvasError> { if !components.is_empty() { let state = self.current_state_mut()?; state.fill_pattern = None; @@ -91,11 +100,12 @@ impl ColorOps for PdfCanvas<'_, B> { self.set_fill_pattern(pattern_name) } - fn set_stroking_color_extended( + /// Handles `SCN` with a pattern name: sets the stroking pattern. + pub(crate) fn set_stroking_color_extended( &mut self, components: &[f32], pattern_name: &[u8], - ) -> Result<(), Self::ErrorType> { + ) -> Result<(), PdfCanvasError> { if !components.is_empty() { let state = self.current_state_mut()?; state.stroke_pattern = None; @@ -116,7 +126,8 @@ impl ColorOps for PdfCanvas<'_, B> { self.set_stroke_pattern(pattern_name) } - fn set_stroking_gray(&mut self, gray: f32) -> Result<(), Self::ErrorType> { + /// Handles `G`: sets a DeviceGray stroking color. + pub(crate) fn set_stroking_gray(&mut self, gray: f32) -> Result<(), PdfCanvasError> { let state = self.current_state_mut()?; state.paint.set_stroke_color(Color::from_gray(gray)); state.stroke_pattern = None; @@ -124,7 +135,8 @@ impl ColorOps for PdfCanvas<'_, B> { Ok(()) } - fn set_non_stroking_gray(&mut self, gray: f32) -> Result<(), Self::ErrorType> { + /// Handles `g`: sets a DeviceGray non-stroking color. + pub(crate) fn set_non_stroking_gray(&mut self, gray: f32) -> Result<(), PdfCanvasError> { let state = self.current_state_mut()?; state.paint.set_fill_color(Color::from_gray(gray)); state.fill_pattern = None; @@ -132,7 +144,13 @@ impl ColorOps for PdfCanvas<'_, B> { Ok(()) } - fn set_stroking_rgb(&mut self, r: f32, g: f32, b: f32) -> Result<(), Self::ErrorType> { + /// Handles `RG`: sets a DeviceRGB stroking color. + pub(crate) fn set_stroking_rgb( + &mut self, + r: f32, + g: f32, + b: f32, + ) -> Result<(), PdfCanvasError> { let state = self.current_state_mut()?; state.paint.set_stroke_color(Color::from_rgb(r, g, b)); state.stroke_pattern = None; @@ -140,7 +158,13 @@ impl ColorOps for PdfCanvas<'_, B> { Ok(()) } - fn set_non_stroking_rgb(&mut self, r: f32, g: f32, b: f32) -> Result<(), Self::ErrorType> { + /// Handles `rg`: sets a DeviceRGB non-stroking color. + pub(crate) fn set_non_stroking_rgb( + &mut self, + r: f32, + g: f32, + b: f32, + ) -> Result<(), PdfCanvasError> { let state = self.current_state_mut()?; state.paint.set_fill_color(Color::from_rgb(r, g, b)); state.fill_pattern = None; @@ -148,7 +172,14 @@ impl ColorOps for PdfCanvas<'_, B> { Ok(()) } - fn set_stroking_cmyk(&mut self, c: f32, m: f32, y: f32, k: f32) -> Result<(), Self::ErrorType> { + /// Handles `K`: sets a DeviceCMYK stroking color. + pub(crate) fn set_stroking_cmyk( + &mut self, + c: f32, + m: f32, + y: f32, + k: f32, + ) -> Result<(), PdfCanvasError> { let state = self.current_state_mut()?; state.paint.set_stroke_color(Color::from_cmyk(c, m, y, k)); state.stroke_pattern = None; @@ -156,13 +187,14 @@ impl ColorOps for PdfCanvas<'_, B> { Ok(()) } - fn set_non_stroking_cmyk( + /// Handles `k`: sets a DeviceCMYK non-stroking color. + pub(crate) fn set_non_stroking_cmyk( &mut self, c: f32, m: f32, y: f32, k: f32, - ) -> Result<(), Self::ErrorType> { + ) -> Result<(), PdfCanvasError> { let state = self.current_state_mut()?; state.paint.set_fill_color(Color::from_cmyk(c, m, y, k)); state.fill_pattern = None; diff --git a/crates/pdf-canvas/src/canvas_external_object_ops.rs b/crates/pdf-canvas/src/canvas_external_object_ops.rs index 8fec8cb9..255833da 100644 --- a/crates/pdf-canvas/src/canvas_external_object_ops.rs +++ b/crates/pdf-canvas/src/canvas_external_object_ops.rs @@ -7,9 +7,8 @@ //! Images are placed by handing the backend the full image-space to device-space //! matrix, so rotation, shear and mirroring in the CTM are all preserved. -use pdf_content_stream_operators::pdf_operator_backend::XObjectOps; use pdf_graphics::{Image, PixelFormat, transform::Transform}; -use pdf_image::{InlineImage, decode_inline_image, raster::stencil_image}; +use pdf_image::{PdfImage, raster::stencil_image}; use pdf_resources::resource::Resource; use crate::{canvas_backend::CanvasBackend, error::PdfCanvasError, pdf_canvas::PdfCanvas}; @@ -22,8 +21,7 @@ use crate::{canvas_backend::CanvasBackend, error::PdfCanvasError, pdf_canvas::Pd /// lands on the top edge of the placed image. const IMAGE_SPACE_Y_FLIP: Transform = Transform::from_row(1.0, 0.0, 0.0, -1.0, 0.0, 1.0); -impl XObjectOps for PdfCanvas<'_, B> { - type ErrorType = PdfCanvasError; +impl PdfCanvas<'_, B> { /// Invokes (renders) an XObject by name from the current resource dictionary. /// /// This method handles two types of XObjects: @@ -41,7 +39,7 @@ impl XObjectOps for PdfCanvas<'_, B> { /// Form XObjects are self-contained content streams that can include /// their own resources. They are rendered by recursively processing /// the form's content stream with the form's transformation matrix. - fn invoke_xobject(&mut self, xobject_name: &[u8]) -> Result<(), Self::ErrorType> { + pub(crate) fn invoke_xobject(&mut self, xobject_name: &[u8]) -> Result<(), PdfCanvasError> { let resources = self .current_state()? .resources @@ -79,8 +77,9 @@ impl XObjectOps for PdfCanvas<'_, B> { } /// Decodes inline image samples before applying the current painting state. - fn paint_inline_image(&mut self, image: &InlineImage) -> Result<(), Self::ErrorType> { - let decoded = decode_inline_image(image, None) + pub(crate) fn paint_inline_image(&mut self, image: &PdfImage) -> Result<(), PdfCanvasError> { + let decoded = image + .decode(None) .map_err(|e| PdfCanvasError::InvalidImageData(e.to_string()))?; self.render_decoded_image(&decoded, true) diff --git a/crates/pdf-canvas/src/canvas_graphics_state_ops.rs b/crates/pdf-canvas/src/canvas_graphics_state_ops.rs index d5e80921..175456f2 100644 --- a/crates/pdf-canvas/src/canvas_graphics_state_ops.rs +++ b/crates/pdf-canvas/src/canvas_graphics_state_ops.rs @@ -1,26 +1,14 @@ //! PDF graphics-state selection and save/restore operators. use std::sync::Arc; -use pdf_content_stream_operators::pdf_operator_backend::GraphicsStateOps; use pdf_graphics::{DashPattern, LineCap, LineJoin, MaskMode, transform::Transform}; use pdf_resources::{external_graphics_state::ExternalGraphicsStateKey, resource::Resource}; use crate::{canvas_backend::CanvasBackend, error::PdfCanvasError, pdf_canvas::PdfCanvas}; -impl GraphicsStateOps for PdfCanvas<'_, B> { - type ErrorType = PdfCanvasError; - /// Saves PDF graphics state together with the corresponding backend state. - fn save_graphics_state(&mut self) -> Result<(), Self::ErrorType> { - self.save() - } - - /// Restores PDF graphics state and its corresponding backend save. - fn restore_graphics_state(&mut self) -> Result<(), Self::ErrorType> { - self.restore() - } - +impl PdfCanvas<'_, B> { /// Concatenates the supplied matrix with the current PDF transformation. - fn concat_matrix(&mut self, transform: &Transform) -> Result<(), Self::ErrorType> { + pub(crate) fn concat_matrix(&mut self, transform: &Transform) -> Result<(), PdfCanvasError> { // PDF 'cm' operator: update the current transformation matrix (CTM) by // concatenating the provided matrix [a b c d e f] onto the current CTM. // @@ -31,51 +19,44 @@ impl GraphicsStateOps for PdfCanvas<'_, B> { } /// Updates the width used for subsequent path strokes. - fn set_line_width(&mut self, width: f32) -> Result<(), Self::ErrorType> { + pub(crate) fn set_line_width(&mut self, width: f32) -> Result<(), PdfCanvasError> { self.current_state_mut()?.paint.line_width = width; Ok(()) } /// Updates the end-cap style used for subsequent path strokes. - fn set_line_cap(&mut self, cap_style: LineCap) -> Result<(), Self::ErrorType> { + pub(crate) fn set_line_cap(&mut self, cap_style: LineCap) -> Result<(), PdfCanvasError> { self.current_state_mut()?.paint.line_cap = cap_style; Ok(()) } /// Updates the corner-join style used for subsequent path strokes. - fn set_line_join(&mut self, line_join: LineJoin) -> Result<(), Self::ErrorType> { + pub(crate) fn set_line_join(&mut self, line_join: LineJoin) -> Result<(), PdfCanvasError> { self.current_state_mut()?.paint.line_join = line_join; Ok(()) } /// Updates the maximum miter length relative to stroke width. - fn set_miter_limit(&mut self, miter_limit: f32) -> Result<(), Self::ErrorType> { + pub(crate) fn set_miter_limit(&mut self, miter_limit: f32) -> Result<(), PdfCanvasError> { self.current_state_mut()?.paint.miter_limit = miter_limit; Ok(()) } /// Validates and stores dash intervals and their starting phase. - fn set_dash_pattern( + pub(crate) fn set_dash_pattern( &mut self, dash_array: &[f32], dash_phase: f32, - ) -> Result<(), Self::ErrorType> { + ) -> Result<(), PdfCanvasError> { self.current_state_mut()?.paint.dash_pattern = DashPattern::new(dash_array, dash_phase)?; Ok(()) } - /// Handles the rendering-intent operator; color conversion currently uses its default intent. - fn set_rendering_intent(&mut self, _intent: &[u8]) -> Result<(), Self::ErrorType> { - Ok(()) - } - - /// Handles the flatness operator; path approximation remains backend-controlled. - fn set_flatness_tolerance(&mut self, _tolerance: f32) -> Result<(), Self::ErrorType> { - Ok(()) - } - /// Applies extended graphics-state entries, preparing soft-mask recordings when selected. - fn set_graphics_state_from_dict(&mut self, dict_name: &[u8]) -> Result<(), Self::ErrorType> { + pub(crate) fn set_graphics_state_from_dict( + &mut self, + dict_name: &[u8], + ) -> Result<(), PdfCanvasError> { let resources = self .current_state()? .resources @@ -163,7 +144,7 @@ impl GraphicsStateOps for PdfCanvas<'_, B> { Arc::new(recording_canvas), transform, smask.mask_type.clone(), - smask.transfer.clone(), + smask.transfer.table().cloned(), )?); } else { self.current_state_mut()?.soft_mask = None; diff --git a/crates/pdf-canvas/src/canvas_marked_content_ops.rs b/crates/pdf-canvas/src/canvas_marked_content_ops.rs index f8ce7321..c721df4f 100644 --- a/crates/pdf-canvas/src/canvas_marked_content_ops.rs +++ b/crates/pdf-canvas/src/canvas_marked_content_ops.rs @@ -1,33 +1,20 @@ -use pdf_content_stream_operators::pdf_operator_backend::MarkedContentOps; - use crate::{canvas_backend::CanvasBackend, error::PdfCanvasError, pdf_canvas::PdfCanvas}; -impl MarkedContentOps for PdfCanvas<'_, B> { - type ErrorType = PdfCanvasError; - fn mark_point(&mut self, _tag: &[u8]) -> Result<(), Self::ErrorType> { - Ok(()) - } - - fn mark_point_with_properties( - &mut self, - _tag: &[u8], - _properties_name_or_dict: &[u8], - ) -> Result<(), Self::ErrorType> { - Ok(()) - } - - fn begin_marked_content(&mut self, tag: &[u8]) -> Result<(), Self::ErrorType> { +impl PdfCanvas<'_, B> { + /// Handles `BMC`: begins a marked-content sequence. + pub(crate) fn begin_marked_content(&mut self, tag: &[u8]) -> Result<(), PdfCanvasError> { if let Some(filter) = &mut self.optional_content { filter.begin(tag, || None); } Ok(()) } - fn begin_marked_content_with_properties( + /// Handles `BDC`: begins a marked-content sequence with a property list. + pub(crate) fn begin_marked_content_with_properties( &mut self, tag: &[u8], properties: Option<&[u8]>, - ) -> Result<(), Self::ErrorType> { + ) -> Result<(), PdfCanvasError> { let Some(filter) = &mut self.optional_content else { return Ok(()); }; @@ -40,7 +27,8 @@ impl MarkedContentOps for PdfCanvas<'_, B> { Ok(()) } - fn end_marked_content(&mut self) -> Result<(), Self::ErrorType> { + /// Handles `EMC`: ends a marked-content sequence. + pub(crate) fn end_marked_content(&mut self) -> Result<(), PdfCanvasError> { if let Some(filter) = &mut self.optional_content { filter.end(); } diff --git a/crates/pdf-canvas/src/canvas_operator_dispatch.rs b/crates/pdf-canvas/src/canvas_operator_dispatch.rs new file mode 100644 index 00000000..8c4f26b4 --- /dev/null +++ b/crates/pdf-canvas/src/canvas_operator_dispatch.rs @@ -0,0 +1,131 @@ +//! Dispatch of parsed content-stream operators to their canvas handlers. +use pdf_content_stream_operators::variants::PdfOperatorVariant; + +use crate::{canvas_backend::CanvasBackend, error::PdfCanvasError, pdf_canvas::PdfCanvas}; + +impl PdfCanvas<'_, B> { + /// Applies one content-stream operator to the canvas. + /// + /// Operators without a visual effect on this canvas (compatibility sections, Type 3 glyph + /// metrics, rendering intent and flatness) are accepted and ignored. + pub(crate) fn apply_operator(&mut self, op: &PdfOperatorVariant) -> Result<(), PdfCanvasError> { + match op { + PdfOperatorVariant::LineTo { x, y } => self.line_to(*x, *y), + PdfOperatorVariant::MoveTo { x, y } => self.move_to(*x, *y), + PdfOperatorVariant::CurveTo { + x1, + y1, + x2, + y2, + x3, + y3, + } => self.curve_to(*x1, *y1, *x2, *y2, *x3, *y3), + PdfOperatorVariant::CurveToV { x2, y2, x3, y3 } => self.curve_to_v(*x2, *y2, *x3, *y3), + PdfOperatorVariant::CurveToY { x1, y1, x3, y3 } => self.curve_to_y(*x1, *y1, *x3, *y3), + PdfOperatorVariant::ClosePath => self.close_path(), + PdfOperatorVariant::Rectangle { + x, + y, + width, + height, + } => self.rectangle(*x, *y, *width, *height), + PdfOperatorVariant::StrokePath => self.stroke_path(), + PdfOperatorVariant::CloseStrokePath => self.close_and_stroke_path(), + PdfOperatorVariant::FillPathNonZero => self.fill_path_nonzero_winding(), + PdfOperatorVariant::FillPathEvenOdd => self.fill_path_even_odd(), + PdfOperatorVariant::FillAndStrokePathNonZero => { + self.fill_and_stroke_path_nonzero_winding() + } + PdfOperatorVariant::FillAndStrokePathEvenOdd => self.fill_and_stroke_path_even_odd(), + PdfOperatorVariant::CloseFillAndStrokePathNonZero => { + self.close_fill_and_stroke_path_nonzero_winding() + } + PdfOperatorVariant::CloseFillAndStrokePathEvenOdd => { + self.close_fill_and_stroke_path_even_odd() + } + PdfOperatorVariant::EndPath => self.end_path_no_op(), + PdfOperatorVariant::ClipNonZero => self.clip_path_nonzero_winding(), + PdfOperatorVariant::ClipEvenOdd => self.clip_path_even_odd(), + PdfOperatorVariant::SetGrayFill { gray } => self.set_non_stroking_gray(*gray), + PdfOperatorVariant::SetGrayStroke { gray } => self.set_stroking_gray(*gray), + PdfOperatorVariant::SetRGBFill { r, g, b } => self.set_non_stroking_rgb(*r, *g, *b), + PdfOperatorVariant::SetRGBStroke { r, g, b } => self.set_stroking_rgb(*r, *g, *b), + PdfOperatorVariant::SetCMYKFill { c, m, y, k } => { + self.set_non_stroking_cmyk(*c, *m, *y, *k) + } + PdfOperatorVariant::SetCMYKStroke { c, m, y, k } => { + self.set_stroking_cmyk(*c, *m, *y, *k) + } + PdfOperatorVariant::SetLineWidth { width } => self.set_line_width(*width), + PdfOperatorVariant::SetLineCapStyle { style } => self.set_line_cap(*style), + PdfOperatorVariant::SetLineJoinStyle { style } => self.set_line_join(*style), + PdfOperatorVariant::SetMiterLimit { limit } => self.set_miter_limit(*limit), + PdfOperatorVariant::SetDashPattern { array, phase } => { + self.set_dash_pattern(array, *phase) + } + PdfOperatorVariant::SetGraphicsStateFromDict { dict_name } => { + self.set_graphics_state_from_dict(dict_name) + } + PdfOperatorVariant::SaveGraphicsState => self.save(), + PdfOperatorVariant::RestoreGraphicsState => self.restore(), + PdfOperatorVariant::ConcatMatrix { matrix } => self.concat_matrix(matrix), + PdfOperatorVariant::BeginMarkedContent { tag } => self.begin_marked_content(tag), + PdfOperatorVariant::BeginMarkedContentWithProps(op) => { + self.begin_marked_content_with_properties(op.tag(), op.properties()) + } + PdfOperatorVariant::EndMarkedContent => self.end_marked_content(), + PdfOperatorVariant::BeginText => self.begin_text_object(), + PdfOperatorVariant::EndText => self.end_text_object(), + PdfOperatorVariant::MoveTextPosition { tx, ty } => self.move_text_position(*tx, *ty), + PdfOperatorVariant::MoveTextPositionAndSetLeading { tx, ty } => { + self.move_text_position_and_set_leading(*tx, *ty) + } + PdfOperatorVariant::SetTextMatrix { matrix } => self.set_text_matrix(matrix), + PdfOperatorVariant::MoveToNextLine => self.move_to_start_of_next_line(), + PdfOperatorVariant::ShowText { text } => self.show_text(text), + PdfOperatorVariant::MoveNextLineShowText { text } => { + self.move_to_next_line_and_show_text(text) + } + PdfOperatorVariant::SetSpacingMoveShowText { + word_spacing, + char_spacing, + text, + } => self.set_spacing_and_show_text(*word_spacing, *char_spacing, text), + PdfOperatorVariant::ShowTextArray { elements } => { + self.show_text_with_glyph_positioning(elements) + } + PdfOperatorVariant::SetCharacterSpacing { spacing } => { + self.set_character_spacing(*spacing) + } + PdfOperatorVariant::SetWordSpacing { spacing } => self.set_word_spacing(*spacing), + PdfOperatorVariant::SetHorizontalScaling { scale } => { + self.set_horizontal_text_scaling(*scale) + } + PdfOperatorVariant::SetLeading { leading } => self.set_text_leading(*leading), + PdfOperatorVariant::SetFont { name, size } => self.set_font_and_size(name, *size), + PdfOperatorVariant::SetRenderingMode { mode } => self.set_text_rendering_mode(*mode), + PdfOperatorVariant::SetTextRise { rise } => self.set_text_rise(*rise), + PdfOperatorVariant::InvokeXObject { name } => self.invoke_xobject(name), + PdfOperatorVariant::InlineImage(image) => self.paint_inline_image(image), + PdfOperatorVariant::PaintShading { name } => self.paint_shading(name), + PdfOperatorVariant::SetStrokeColorSpace { name } => self.set_stroking_color_space(name), + PdfOperatorVariant::SetNonStrokingColorSpace { name } => { + self.set_non_stroking_color_space(name) + } + PdfOperatorVariant::SetStrokingColor(color) => match &color.pattern { + Some(pattern) => self.set_stroking_color_extended(&color.components, pattern), + None => self.set_stroking_color(&color.components), + }, + PdfOperatorVariant::SetNonStrokingColor(color) => match &color.pattern { + Some(pattern) => self.set_non_stroking_color_extended(&color.components, pattern), + None => self.set_non_stroking_color(&color.components), + }, + PdfOperatorVariant::BeginCompatibility + | PdfOperatorVariant::EndCompatibility + | PdfOperatorVariant::SetCharWidthAndBoundingBox { .. } + | PdfOperatorVariant::SetCharWidth { .. } + | PdfOperatorVariant::SetRenderingIntent { .. } + | PdfOperatorVariant::SetFlatnessTolerance { .. } => Ok(()), + } + } +} diff --git a/crates/pdf-canvas/src/canvas_path_ops.rs b/crates/pdf-canvas/src/canvas_path_ops.rs index fe21c2a4..437833b2 100644 --- a/crates/pdf-canvas/src/canvas_path_ops.rs +++ b/crates/pdf-canvas/src/canvas_path_ops.rs @@ -1,25 +1,26 @@ -use pdf_content_stream_operators::pdf_operator_backend::{PathConstructionOps, PathPaintingOps}; use pdf_graphics::{PaintMode, PathFillType, pdf_path::PdfPath}; use crate::{canvas_backend::CanvasBackend, error::PdfCanvasError, pdf_canvas::PdfCanvas}; -impl PathConstructionOps for PdfCanvas<'_, B> { - type ErrorType = PdfCanvasError; - fn move_to(&mut self, x: f32, y: f32) -> Result<(), Self::ErrorType> { +impl PdfCanvas<'_, B> { + /// Handles `m`: begins a new subpath at (x, y). + pub(crate) fn move_to(&mut self, x: f32, y: f32) -> Result<(), PdfCanvasError> { self.current_path .get_or_insert_with(PdfPath::default) .move_to(x, y); Ok(()) } - fn line_to(&mut self, x: f32, y: f32) -> Result<(), Self::ErrorType> { + /// Handles `l`: appends a line segment to (x, y). + pub(crate) fn line_to(&mut self, x: f32, y: f32) -> Result<(), PdfCanvasError> { self.current_path .get_or_insert_with(PdfPath::default) .line_to(x, y); Ok(()) } - fn curve_to( + /// Handles `c`: appends a cubic Bézier curve. + pub(crate) fn curve_to( &mut self, x1: f32, y1: f32, @@ -27,14 +28,21 @@ impl PathConstructionOps for PdfCanvas<'_, B> { y2: f32, x3: f32, y3: f32, - ) -> Result<(), Self::ErrorType> { + ) -> Result<(), PdfCanvasError> { self.current_path .get_or_insert_with(PdfPath::default) .curve_to(x1, y1, x2, y2, x3, y3); Ok(()) } - fn curve_to_v(&mut self, x2: f32, y2: f32, x3: f32, y3: f32) -> Result<(), Self::ErrorType> { + /// Handles `v`: appends a Bézier curve whose first control point is the current point. + pub(crate) fn curve_to_v( + &mut self, + x2: f32, + y2: f32, + x3: f32, + y3: f32, + ) -> Result<(), PdfCanvasError> { let path = self.current_path.get_or_insert_with(PdfPath::default); if let Some((x, y)) = path.current_point() { path.curve_to(x, y, x2, y2, x3, y3); @@ -44,27 +52,36 @@ impl PathConstructionOps for PdfCanvas<'_, B> { } } - fn curve_to_y(&mut self, x1: f32, y1: f32, x3: f32, y3: f32) -> Result<(), Self::ErrorType> { + /// Handles `y`: appends a Bézier curve whose second control point is its end point. + pub(crate) fn curve_to_y( + &mut self, + x1: f32, + y1: f32, + x3: f32, + y3: f32, + ) -> Result<(), PdfCanvasError> { self.current_path .get_or_insert_with(PdfPath::default) .curve_to(x1, y1, x3, y3, x3, y3); Ok(()) } - fn close_path(&mut self) -> Result<(), Self::ErrorType> { + /// Handles `h`: closes the current subpath. + pub(crate) fn close_path(&mut self) -> Result<(), PdfCanvasError> { self.current_path .get_or_insert_with(PdfPath::default) .close(); Ok(()) } - fn rectangle( + /// Handles `re`: appends a closed rectangle subpath. + pub(crate) fn rectangle( &mut self, x: f32, y: f32, width: f32, height: f32, - ) -> Result<(), Self::ErrorType> { + ) -> Result<(), PdfCanvasError> { let path = self.current_path.get_or_insert_with(PdfPath::default); path.move_to(x, y); @@ -76,44 +93,54 @@ impl PathConstructionOps for PdfCanvas<'_, B> { } } -impl PathPaintingOps for PdfCanvas<'_, B> { - type ErrorType = PdfCanvasError; - fn stroke_path(&mut self) -> Result<(), Self::ErrorType> { +impl PdfCanvas<'_, B> { + /// Handles `S`: strokes the current path. + pub(crate) fn stroke_path(&mut self) -> Result<(), PdfCanvasError> { self.end_path(Some((PaintMode::Stroke, PathFillType::default()))) } - fn close_and_stroke_path(&mut self) -> Result<(), Self::ErrorType> { + /// Handles `s`: closes and strokes the current path. + pub(crate) fn close_and_stroke_path(&mut self) -> Result<(), PdfCanvasError> { self.close_path()?; - PathPaintingOps::stroke_path(self) + self.stroke_path() } - fn fill_path_nonzero_winding(&mut self) -> Result<(), Self::ErrorType> { + /// Handles `f`/`F`: fills the current path with the nonzero winding rule. + pub(crate) fn fill_path_nonzero_winding(&mut self) -> Result<(), PdfCanvasError> { self.end_path(Some((PaintMode::Fill, PathFillType::Winding))) } - fn fill_path_even_odd(&mut self) -> Result<(), Self::ErrorType> { + /// Handles `f*`: fills the current path with the even-odd rule. + pub(crate) fn fill_path_even_odd(&mut self) -> Result<(), PdfCanvasError> { self.end_path(Some((PaintMode::Fill, PathFillType::EvenOdd))) } - fn fill_and_stroke_path_nonzero_winding(&mut self) -> Result<(), Self::ErrorType> { + /// Handles `B`: fills (nonzero winding) and strokes the current path. + pub(crate) fn fill_and_stroke_path_nonzero_winding(&mut self) -> Result<(), PdfCanvasError> { self.end_path(Some((PaintMode::FillAndStroke, PathFillType::Winding))) } - fn fill_and_stroke_path_even_odd(&mut self) -> Result<(), Self::ErrorType> { + /// Handles `B*`: fills (even-odd) and strokes the current path. + pub(crate) fn fill_and_stroke_path_even_odd(&mut self) -> Result<(), PdfCanvasError> { self.end_path(Some((PaintMode::FillAndStroke, PathFillType::EvenOdd))) } - fn close_fill_and_stroke_path_nonzero_winding(&mut self) -> Result<(), Self::ErrorType> { + /// Handles `b`: closes, fills (nonzero winding) and strokes the current path. + pub(crate) fn close_fill_and_stroke_path_nonzero_winding( + &mut self, + ) -> Result<(), PdfCanvasError> { self.close_path()?; self.fill_and_stroke_path_nonzero_winding() } - fn close_fill_and_stroke_path_even_odd(&mut self) -> Result<(), Self::ErrorType> { + /// Handles `b*`: closes, fills (even-odd) and strokes the current path. + pub(crate) fn close_fill_and_stroke_path_even_odd(&mut self) -> Result<(), PdfCanvasError> { self.close_path()?; self.fill_and_stroke_path_even_odd() } - fn end_path_no_op(&mut self) -> Result<(), Self::ErrorType> { + /// Handles `n`: ends the current path without painting it. + pub(crate) fn end_path_no_op(&mut self) -> Result<(), PdfCanvasError> { self.end_path(None) } } diff --git a/crates/pdf-canvas/src/canvas_text_ops.rs b/crates/pdf-canvas/src/canvas_text_ops.rs index cbbe895a..54622d89 100644 --- a/crates/pdf-canvas/src/canvas_text_ops.rs +++ b/crates/pdf-canvas/src/canvas_text_ops.rs @@ -2,52 +2,52 @@ use crate::CanvasPath; use crate::canvas_backend::CanvasBackend; use crate::error::PdfCanvasError; use crate::pdf_canvas::PdfCanvas; -use pdf_content_stream_operators::pdf_operator_backend::{ - TextObjectOps, TextPositioningOps, TextShowingOps, TextStateOps, -}; use pdf_content_stream_operators::variants::PdfOperatorVariant; use pdf_graphics::pdf_path::PdfPath; use pdf_graphics::transform::Transform; use pdf_graphics::{PaintMode, PathFillType, TextRenderingMode}; use pdf_text_engine::text::{PdfTextItem, PdfTextRun}; -impl TextPositioningOps for PdfCanvas<'_, B> { - type ErrorType = PdfCanvasError; - fn move_text_position(&mut self, tx: f32, ty: f32) -> Result<(), Self::ErrorType> { +impl PdfCanvas<'_, B> { + /// Handles `Td`: offsets the start of the next line. + pub(crate) fn move_text_position(&mut self, tx: f32, ty: f32) -> Result<(), PdfCanvasError> { self.current_state_mut()? .text_state .move_line_position(tx, ty); Ok(()) } - fn move_text_position_and_set_leading( + /// Handles `TD`: sets the leading to `-ty` and offsets the next line. + pub(crate) fn move_text_position_and_set_leading( &mut self, tx: f32, ty: f32, - ) -> Result<(), Self::ErrorType> { + ) -> Result<(), PdfCanvasError> { // TD: Set leading to -ty, then perform Td(tx, ty) let neg_ty = -ty; self.current_state_mut()?.text_state.leading = neg_ty; self.move_text_position(tx, ty) } - fn set_text_matrix(&mut self, transform: &Transform) -> Result<(), Self::ErrorType> { + /// Handles `Tm`: sets the text and text line matrices. + pub(crate) fn set_text_matrix(&mut self, transform: &Transform) -> Result<(), PdfCanvasError> { self.current_state_mut()? .text_state .set_matrices(*transform); Ok(()) } - fn move_to_start_of_next_line(&mut self) -> Result<(), Self::ErrorType> { + /// Handles `T*`: moves to the start of the next line. + pub(crate) fn move_to_start_of_next_line(&mut self) -> Result<(), PdfCanvasError> { // T*: Move to start of next line using current leading: Tlm = Tlm * T(0, -Tl); Tm = Tlm. self.current_state_mut()?.text_state.move_to_next_line(); Ok(()) } } -impl TextObjectOps for PdfCanvas<'_, B> { - type ErrorType = PdfCanvasError; - fn begin_text_object(&mut self) -> Result<(), Self::ErrorType> { +impl PdfCanvas<'_, B> { + /// Handles `BT`: resets the text matrices. + pub(crate) fn begin_text_object(&mut self) -> Result<(), PdfCanvasError> { let state = self.current_state_mut()?; state.text_state.matrix = Transform::identity(); state.text_state.line_matrix = Transform::identity(); @@ -56,7 +56,8 @@ impl TextObjectOps for PdfCanvas<'_, B> { Ok(()) } - fn end_text_object(&mut self) -> Result<(), Self::ErrorType> { + /// Handles `ET`: applies any pending text clip. + pub(crate) fn end_text_object(&mut self) -> Result<(), PdfCanvasError> { // Apply any glyph outlines accumulated for clip-mode text rendering (modes 4–7). // Per ISO 32000 §9.3.6, the clip path is set at the end of the text object. if let Some(clip_path) = self.current_state_mut()?.pending_text_clip.take() { @@ -70,30 +71,41 @@ impl TextObjectOps for PdfCanvas<'_, B> { } } -impl TextStateOps for PdfCanvas<'_, B> { - type ErrorType = PdfCanvasError; - fn set_character_spacing(&mut self, spacing: f32) -> Result<(), Self::ErrorType> { +impl PdfCanvas<'_, B> { + /// Handles `Tc`: sets the character spacing. + pub(crate) fn set_character_spacing(&mut self, spacing: f32) -> Result<(), PdfCanvasError> { self.current_state_mut()?.text_state.style.character_spacing = spacing; Ok(()) } - fn set_word_spacing(&mut self, spacing: f32) -> Result<(), Self::ErrorType> { + /// Handles `Tw`: sets the word spacing. + pub(crate) fn set_word_spacing(&mut self, spacing: f32) -> Result<(), PdfCanvasError> { self.current_state_mut()?.text_state.style.word_spacing = spacing; Ok(()) } - fn set_horizontal_text_scaling(&mut self, scale_percent: f32) -> Result<(), Self::ErrorType> { + /// Handles `Tz`: sets the horizontal scaling from a percentage. + pub(crate) fn set_horizontal_text_scaling( + &mut self, + scale_percent: f32, + ) -> Result<(), PdfCanvasError> { self.current_state_mut()?.text_state.style.horizontal_scale = scale_percent / 100.0; Ok(()) } - fn set_text_leading(&mut self, leading: f32) -> Result<(), Self::ErrorType> { + /// Handles `TL`: sets the text leading. + pub(crate) fn set_text_leading(&mut self, leading: f32) -> Result<(), PdfCanvasError> { // TL sets the text leading parameter self.current_state_mut()?.text_state.leading = leading; Ok(()) } - fn set_font_and_size(&mut self, font_name: &[u8], size: f32) -> Result<(), Self::ErrorType> { + /// Handles `Tf`: selects a font resource and size. + pub(crate) fn set_font_and_size( + &mut self, + font_name: &[u8], + size: f32, + ) -> Result<(), PdfCanvasError> { let (font, nested_resources) = self .current_state()? .resources @@ -111,44 +123,52 @@ impl TextStateOps for PdfCanvas<'_, B> { Ok(()) } - fn set_text_rendering_mode(&mut self, mode: TextRenderingMode) -> Result<(), Self::ErrorType> { + /// Handles `Tr`: sets the text rendering mode. + pub(crate) fn set_text_rendering_mode( + &mut self, + mode: TextRenderingMode, + ) -> Result<(), PdfCanvasError> { self.current_state_mut()?.paint.rendering_mode = mode; Ok(()) } - fn set_text_rise(&mut self, rise: f32) -> Result<(), Self::ErrorType> { + /// Handles `Ts`: sets the text rise. + pub(crate) fn set_text_rise(&mut self, rise: f32) -> Result<(), PdfCanvasError> { self.current_state_mut()?.text_state.style.rise = rise; Ok(()) } } -impl TextShowingOps for PdfCanvas<'_, B> { - type ErrorType = PdfCanvasError; - fn show_text(&mut self, text: &PdfTextItem) -> Result<(), Self::ErrorType> { +impl PdfCanvas<'_, B> { + /// Handles `Tj`: shows a text string. + pub(crate) fn show_text(&mut self, text: &PdfTextItem) -> Result<(), PdfCanvasError> { self.render_text_items(std::slice::from_ref(text)) } - fn show_text_with_glyph_positioning( + /// Handles `TJ`: shows strings with individual position adjustments. + pub(crate) fn show_text_with_glyph_positioning( &mut self, elements: &[PdfTextItem], - ) -> Result<(), Self::ErrorType> { + ) -> Result<(), PdfCanvasError> { self.render_text_items(elements) } - fn move_to_next_line_and_show_text( + /// Handles `'`: moves to the next line and shows text. + pub(crate) fn move_to_next_line_and_show_text( &mut self, text: &PdfTextItem, - ) -> Result<(), Self::ErrorType> { + ) -> Result<(), PdfCanvasError> { self.move_to_start_of_next_line()?; self.show_text(text) } - fn set_spacing_and_show_text( + /// Handles `"`: sets word and character spacing, then behaves as `'`. + pub(crate) fn set_spacing_and_show_text( &mut self, word_spacing: f32, char_spacing: f32, text: &PdfTextItem, - ) -> Result<(), Self::ErrorType> { + ) -> Result<(), PdfCanvasError> { self.set_word_spacing(word_spacing)?; self.set_character_spacing(char_spacing)?; self.move_to_next_line_and_show_text(text) diff --git a/crates/pdf-canvas/src/lib.rs b/crates/pdf-canvas/src/lib.rs index f16b764a..5b292b9b 100644 --- a/crates/pdf-canvas/src/lib.rs +++ b/crates/pdf-canvas/src/lib.rs @@ -1,7 +1,4 @@ //! PDF drawing interpretation, recording, and portable backend contracts. -use canvas_backend::CanvasBackend; -use pdf_canvas::PdfCanvas; -use pdf_content_stream_operators::pdf_operator_backend::PdfOperatorBackend; /// Drawing operations implemented by concrete rendering backends. pub mod canvas_backend; @@ -15,6 +12,8 @@ mod canvas_external_object_ops; mod canvas_graphics_state_ops; /// Marked-content operator handling. mod canvas_marked_content_ops; +/// Dispatch of parsed content-stream operators to their handlers. +mod canvas_operator_dispatch; /// PDF path construction and painting operators. mod canvas_path_ops; /// PDF graphics-state values retained across saved states. @@ -49,5 +48,3 @@ pub mod text; mod text_state; /// Tiling shader. pub mod tiling_shader; - -impl PdfOperatorBackend for PdfCanvas<'_, B> {} diff --git a/crates/pdf-canvas/src/pdf_canvas.rs b/crates/pdf-canvas/src/pdf_canvas.rs index e68934ab..b620de80 100644 --- a/crates/pdf-canvas/src/pdf_canvas.rs +++ b/crates/pdf-canvas/src/pdf_canvas.rs @@ -771,7 +771,7 @@ impl<'a, B: CanvasBackend> PdfCanvas<'a, B> { if filter.as_mut().is_some_and(|filter| filter(op)) { continue; } - op.call(self)?; + self.apply_operator(op)?; } Ok(()) @@ -901,7 +901,6 @@ impl<'a, B: CanvasBackend> PdfCanvas<'a, B> { #[cfg(test)] mod tests { - use pdf_content_stream_operators::pdf_operator_backend::GraphicsStateOps; use pdf_document::page::PdfPage; use pdf_graphics::rect::Rect; use pdf_text_engine::bundled_font_system; diff --git a/crates/pdf-canvas/src/shading.rs b/crates/pdf-canvas/src/shading.rs index 525d2341..cadb1924 100644 --- a/crates/pdf-canvas/src/shading.rs +++ b/crates/pdf-canvas/src/shading.rs @@ -1,15 +1,13 @@ //! Shading operators and shared paint preparation. use crate::CanvasPath; -use pdf_content_stream_operators::pdf_operator_backend::ShadingOps; use pdf_graphics::{PathFillType, pdf_path::PdfPath, rect::Rect, transform::Transform}; use pdf_shading::model::Shading; use crate::{canvas_backend::CanvasBackend, error::PdfCanvasError, pdf_canvas::PdfCanvas}; -impl ShadingOps for PdfCanvas<'_, B> { - type ErrorType = PdfCanvasError; +impl PdfCanvas<'_, B> { /// Prepares a shading paint and fills the current clip through the selected soft mask. - fn paint_shading(&mut self, shading_name: &[u8]) -> Result<(), Self::ErrorType> { + pub(crate) fn paint_shading(&mut self, shading_name: &[u8]) -> Result<(), PdfCanvasError> { let state = self.current_state()?; let Some(shading) = state diff --git a/crates/pdf-canvas/tests/canvas_graphics_state_ops.rs b/crates/pdf-canvas/tests/canvas_graphics_state_ops.rs index 9ac726bb..f2602b86 100644 --- a/crates/pdf-canvas/tests/canvas_graphics_state_ops.rs +++ b/crates/pdf-canvas/tests/canvas_graphics_state_ops.rs @@ -14,7 +14,7 @@ use pdf_resources::{ form::FormXObject, resource::Resource, resources::Resources, - soft_mask::SoftMask, + soft_mask::{MaskTransfer, SoftMask}, }; use pdf_text_engine::bundled_font_system; @@ -99,7 +99,7 @@ fn soft_mask_form_with_zero_area_bbox_is_ignored() { let resources = Arc::new(graphics_state_resource(vec![ ExternalGraphicsStateKey::SoftMask(Some(pdf_object_reader::ObjectHandle::from(SoftMask { mask_type: MaskMode::Alpha, - transfer: None, + transfer: MaskTransfer::Identity, shape: form.into(), }))), ])); diff --git a/crates/pdf-canvas/tests/pdf_canvas.rs b/crates/pdf-canvas/tests/pdf_canvas.rs index 386e14f4..6388de8b 100644 --- a/crates/pdf-canvas/tests/pdf_canvas.rs +++ b/crates/pdf-canvas/tests/pdf_canvas.rs @@ -10,7 +10,7 @@ use pdf_content_stream::ContentStream; use pdf_content_stream_operators::variants::PdfOperatorVariant; use pdf_document::page::PdfPage; use pdf_graphics::{BlendMode, PixelFormat, rect::Rect}; -use pdf_image::InlineImage; +use pdf_image::PdfImage; use pdf_object_reader::{ ObjectHandle, dictionary::Dictionary, object_resolver::PassthroughResolver, object_variant::ObjectVariant, @@ -74,8 +74,8 @@ fn image_dictionary() -> Dictionary { ])) } -fn inline_image() -> InlineImage { - InlineImage::new( +fn inline_image() -> PdfImage { + PdfImage::inline( Dictionary::new(std::collections::BTreeMap::from([ (Vec::from(b"BPC"), ObjectVariant::Integer(1)), ( @@ -231,15 +231,13 @@ fn unavailable_image_xobject_is_a_no_op() { #[test] fn inline_image_render_path_matches_image_xobject_path() { - let image = pdf_image::decode_normalized_image( - &image_dictionary(), - vec![0b1010_0000].into(), - &mut pdf_object_reader::ObjectReader::new( - pdf_object_reader::object_resolver::PassthroughResolver, - ) - .session(), - None, + let image = pdf_object_reader::ObjectReader::new( + pdf_object_reader::object_resolver::PassthroughResolver, ) + .read::(&pdf_object_reader::object_variant::ObjectVariant::Stream( + pdf_object_reader::stream::StreamObject::new(1, 0, image_dictionary(), vec![0b1010_0000]), + )) + .and_then(|image| Ok(image.decode(None)?)) .expect("image XObject should decode"); let graphics_state = Resource::ExternalGraphicsState(pdf_object_reader::ObjectHandle::from( ExternalGraphicsState { diff --git a/crates/pdf-color-space/src/color_space.rs b/crates/pdf-color-space/src/color_space.rs index 0d2603ca..2fcf1c5d 100644 --- a/crates/pdf-color-space/src/color_space.rs +++ b/crates/pdf-color-space/src/color_space.rs @@ -35,28 +35,6 @@ pub enum ColorSpace { Pattern(Option>), } -impl TryFrom<&[u8]> for ColorSpace { - type Error = ColorSpaceError; - - fn try_from(name: &[u8]) -> Result { - match name { - b"DeviceRGB" => Ok(Self::DeviceRGB), - b"DeviceCMYK" => Ok(Self::DeviceCMYK), - b"DeviceGray" => Ok(Self::DeviceGray), - // Default color spaces substitute for the device spaces when a resource-level - // override is not available. Without access to the resource dictionary here, - // fall back to the corresponding device space. - b"DefaultGray" => Ok(Self::DeviceGray), - b"DefaultRGB" => Ok(Self::DeviceRGB), - b"DefaultCMYK" => Ok(Self::DeviceCMYK), - b"Pattern" => Ok(Self::Pattern(None)), - unknown => Err(ColorSpaceError::InvalidColorSpace { - description: format!("unsupported color space name: /{unknown:?}"), - }), - } - } -} - impl ColorSpace { /// Returns the number of color components required as *input* to this color space. /// diff --git a/crates/pdf-color-space/src/color_space_reader.rs b/crates/pdf-color-space/src/color_space_reader.rs index e6915309..a360b342 100644 --- a/crates/pdf-color-space/src/color_space_reader.rs +++ b/crates/pdf-color-space/src/color_space_reader.rs @@ -1,11 +1,10 @@ +use std::sync::Arc; + use pdf_object_reader::{ FromPdfObject, ObjectAccess, ObjectContext, ReadResult, object_variant::ObjectVariant, }; -use crate::{ - color_space::ColorSpace, device_n_color_space::DeviceNColorSpace, error::ColorSpaceError, - indexed_color_space::IndexedColorSpace, separation_color_space::SeparationColorSpace, -}; +use crate::{color_space::ColorSpace, error::ColorSpaceError}; /// Reads a color space given as a name (e.g., `/DeviceRGB`) or as an array /// (e.g., `[/Indexed /DeviceRGB 255 ]`). @@ -13,32 +12,66 @@ use crate::{ /// Nested color spaces are read through the same traversal, so the reader's /// decode-depth limit and cycle detection bound maliciously nested definitions. impl FromPdfObject for ColorSpace { - fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - let value = context.object(); - let ObjectVariant::Array(array) = value else { - return Ok(Self::try_from(value.try_bytes(context.source())?)?); + fn from_pdf_object( + mut context: ObjectContext<'_, impl ObjectAccess + ?Sized>, + ) -> ReadResult { + let object = context.object(); + let ObjectVariant::Array(array) = object else { + return match object.try_bytes(context.source())? { + b"DeviceRGB" => Ok(Self::DeviceRGB), + b"DeviceCMYK" => Ok(Self::DeviceCMYK), + b"DeviceGray" => Ok(Self::DeviceGray), + // Default color spaces substitute for the device spaces when a resource-level + // override is not available. Without access to the resource dictionary here, + // fall back to the corresponding device space. + b"DefaultGray" => Ok(Self::DeviceGray), + b"DefaultRGB" => Ok(Self::DeviceRGB), + b"DefaultCMYK" => Ok(Self::DeviceCMYK), + b"Pattern" => Ok(Self::Pattern(None)), + unknown => Err(ColorSpaceError::InvalidColorSpace { + description: format!("unsupported color space name: /{unknown:?}"), + } + .into()), + }; }; - let family = array - .first() - .ok_or_else(|| ColorSpaceError::InvalidColorSpace { + let Some(first) = array.first() else { + return Err(ColorSpaceError::InvalidColorSpace { description: "empty color space array".into(), - })? - .try_bytes(context.source())?; + } + .into()); + }; + // A one-element array names its family alone, as in `[/DeviceRGB]`. if array.len() == 1 { - return Ok(Self::try_from(family)?); + return context.read(first); } - match family { - b"Indexed" => IndexedColorSpace::from_pdf_object(context).map(Self::Indexed), - b"Separation" => SeparationColorSpace::from_pdf_object(context).map(Self::Separation), - b"DeviceN" => DeviceNColorSpace::from_pdf_object(context).map(Self::DeviceN), - b"ICCBased" => sole_operand(context, "ICCBased").map(Self::ICCBased), - b"Lab" => sole_operand(context, "Lab").map(Self::Lab), - b"CalGray" => sole_operand(context, "CalGray").map(Self::CalGray), - b"CalRGB" => sole_operand(context, "CalRGB").map(Self::CalRGB), + let family = context.read::>(first)?; + match &*family { + b"Indexed" => context.read(object).map(Self::Indexed), + b"Separation" => context.read(object).map(Self::Separation), + b"DeviceN" => context.read(object).map(Self::DeviceN), // The second element is the underlying color space used with uncolored // tiling patterns. b"Pattern" => Ok(Self::Pattern(Some(Box::new(context.array()?.at(1)?)))), + family @ (b"ICCBased" | b"Lab" | b"CalGray" | b"CalRGB") => { + let mut array = context.array()?; + let found = array.array().len(); + if found != 2 { + return Err(ColorSpaceError::InvalidColorSpace { + description: format!( + "/{} requires 2 elements, found {found}", + String::from_utf8_lossy(family) + ), + } + .into()); + } + match family { + b"ICCBased" => array.at(1).map(Self::ICCBased), + b"Lab" => array.at(1).map(Self::Lab), + b"CalGray" => array.at(1).map(Self::CalGray), + _ => array.at(1).map(Self::CalRGB), + } + } unknown => Err(ColorSpaceError::InvalidColorSpace { description: format!( "unsupported color space type: /{} (array with {} elements)", @@ -51,22 +84,6 @@ impl FromPdfObject for ColorSpace { } } -/// Reads the single operand of a `[/Family operand]` color space array. -fn sole_operand( - context: ObjectContext<'_, impl ObjectAccess + ?Sized>, - family: &str, -) -> ReadResult { - let mut array = context.array()?; - let found = array.array().len(); - if found != 2 { - return Err(ColorSpaceError::InvalidColorSpace { - description: format!("/{family} requires 2 elements, found {found}"), - } - .into()); - } - array.at(1) -} - #[cfg(test)] mod tests { use pdf_object_reader::{object_resolver::PassthroughResolver, object_variant::ObjectVariant}; diff --git a/crates/pdf-color-space/src/device_n_color_space.rs b/crates/pdf-color-space/src/device_n_color_space.rs index b03ccab8..82decbad 100644 --- a/crates/pdf-color-space/src/device_n_color_space.rs +++ b/crates/pdf-color-space/src/device_n_color_space.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use pdf_function::{function::Function, function_interpolation_error::FunctionInterpolationError}; use pdf_graphics::color::Color; use pdf_object_reader::{FromPdfObject, ObjectAccess, ObjectContext, ReadResult}; @@ -14,7 +16,7 @@ use crate::{color_space::ColorSpace, error::ColorSpaceError}; #[derive(Debug, Clone)] pub struct DeviceNColorSpace { /// Ordered list of colorant names (e.g., `["Cyan", "Magenta"]`). - pub names: Vec>, + pub names: Vec>, /// Fallback color space used when the colorants are not available. pub alternate_space: Box, /// Tint transform function used to map DeviceN components into the alternate space. @@ -27,7 +29,7 @@ pub struct DeviceNColorSpace { impl FromPdfObject for DeviceNColorSpace { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { let mut array = context.array()?; - let [_, names, _, _, ..] = array.array() else { + let [_, _, _, _, ..] = array.array() else { return Err(ColorSpaceError::InvalidColorSpace { description: format!( "/DeviceN requires [/DeviceN names alternateSpace tintTransform] with an optional attributes dictionary; found {} element(s)", @@ -37,14 +39,8 @@ impl FromPdfObject for DeviceNColorSpace { .into()); }; - let objects = array.source(); - let names = names - .try_array(objects)? - .iter() - .map(|name| name.try_bytes(objects).map(Vec::from)) - .collect::, _>>()?; Ok(Self { - names, + names: array.at(1)?, alternate_space: Box::new(array.at(2)?), tint_transform: array.at(3)?, }) @@ -96,7 +92,7 @@ mod tests { ObjectReadError, ObjectReader, ReadResult, dictionary::Dictionary, object_resolver::PassthroughResolver, object_variant::ObjectVariant, stream::StreamObject, }; - use std::collections::BTreeMap; + use std::{collections::BTreeMap, sync::Arc}; fn name(value: &str) -> ObjectVariant { pdf_object_reader::pdf_string::PdfString::from( @@ -166,7 +162,7 @@ mod tests { assert_eq!( device_n.names, - vec![Vec::from(b"Cyan"), Vec::from(b"Magenta")] + vec![Arc::from(&b"Cyan"[..]), Arc::from(&b"Magenta"[..])] ); assert!(matches!(*device_n.alternate_space, ColorSpace::DeviceRGB)); assert!(matches!( @@ -192,7 +188,7 @@ mod tests { panic!("expected DeviceN color space"); }; - assert_eq!(device_n.names, vec![Vec::from(b"Spot")]); + assert_eq!(device_n.names, vec![Arc::from(&b"Spot"[..])]); assert!(matches!(*device_n.alternate_space, ColorSpace::DeviceCMYK)); } diff --git a/crates/pdf-color-space/src/indexed_color_space.rs b/crates/pdf-color-space/src/indexed_color_space.rs index edd4abcf..3fdeebf9 100644 --- a/crates/pdf-color-space/src/indexed_color_space.rs +++ b/crates/pdf-color-space/src/indexed_color_space.rs @@ -1,8 +1,8 @@ use bytes::Bytes; use pdf_graphics::color::Color; use pdf_object_reader::{ - FromPdfObject, ObjectAccess, ObjectContext, ReadResult, object_resolver::ObjectResolver, - object_variant::ObjectVariant, + FromPdfObject, ObjectAccess, ObjectContext, ReadResult, object_kind::ObjectKind, + pdf_string::PdfString, stream::StreamObject, }; use crate::{color_space::ColorSpace, error::ColorSpaceError}; @@ -42,26 +42,16 @@ impl FromPdfObject for IndexedColorSpace { Ok(Self { base: Box::new(array.at(1)?), hival: array.at(2)?, - lookup: extract_lookup_table(array.source(), lookup)?, + // Streams must be matched first: a string read would also accept a stream's raw, + // possibly still encoded, bytes. + lookup: match array.resolve(lookup)?.kind() { + ObjectKind::Stream => array.at::(3)?.shared_data(), + _ => Bytes::copy_from_slice(array.at::(3)?.as_bytes()), + }, }) } } -/// Extracts the lookup table bytes from an Indexed color space. -/// -/// The lookup table can be either a string/hex-string or a stream. -fn extract_lookup_table( - objects: &dyn ObjectResolver, - lookup: &ObjectVariant, -) -> Result { - // Streams must be matched first: `try_bytes` also accepts streams but - // exposes their raw (possibly still encoded) bytes. - if let Ok(stream) = lookup.try_stream(objects) { - return Ok(stream.shared_data()); - } - Ok(Bytes::copy_from_slice(lookup.try_bytes(objects)?)) -} - /// Converts a raw palette entry (byte slice) to a [`Color`] using the given base color space. /// /// Each byte in `entry` encodes a color component in the range 0–255. @@ -157,11 +147,26 @@ mod tests { use std::collections::BTreeMap; use pdf_object_reader::{ - dictionary::Dictionary, object_resolver::PassthroughResolver, stream::StreamObject, + ObjectReader, dictionary::Dictionary, object_resolver::PassthroughResolver, + object_variant::ObjectVariant, stream::StreamObject, }; use super::*; + /// Reads `[/Indexed /DeviceGray 0 lookup]` and returns its lookup table. + fn read_lookup(lookup: ObjectVariant) -> Bytes { + let array = vec![ + ObjectVariant::name_from_bytes(b"Indexed"), + ObjectVariant::name_from_bytes(b"DeviceGray"), + ObjectVariant::Integer(0), + lookup, + ]; + ObjectReader::new(&PassthroughResolver) + .read::(&ObjectVariant::Array(array.into())) + .expect("lookup table should parse") + .lookup + } + #[test] fn stream_lookup_shares_decoded_stream_bytes() { let stream = StreamObject::new( @@ -173,8 +178,7 @@ mod tests { let stream_data = stream.shared_data(); let lookup = ObjectVariant::Stream(stream); - let extracted = extract_lookup_table(&PassthroughResolver, &lookup) - .expect("stream lookup table should parse"); + let extracted = read_lookup(lookup); assert_eq!(extracted.as_ptr(), stream_data.as_ptr()); } @@ -195,8 +199,7 @@ mod tests { .try_bytes(&PassthroughResolver) .expect("test lookup should contain bytes") .to_vec(); - let extracted = extract_lookup_table(&PassthroughResolver, &lookup) - .expect("string lookup table should parse"); + let extracted = read_lookup(lookup); assert_eq!(extracted.as_ref(), expected.as_slice()); } diff --git a/crates/pdf-color-space/src/separation_color_space.rs b/crates/pdf-color-space/src/separation_color_space.rs index ebfbb44c..6c2e6bca 100644 --- a/crates/pdf-color-space/src/separation_color_space.rs +++ b/crates/pdf-color-space/src/separation_color_space.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use pdf_function::function::Function; use pdf_graphics::color::Color; use pdf_object_reader::{FromPdfObject, ObjectAccess, ObjectContext, ReadResult}; @@ -11,7 +13,7 @@ use crate::{color_space::ColorSpace, error::ColorSpaceError}; #[derive(Debug, Clone)] pub struct SeparationColorSpace { /// The name of the colorant (e.g., `/All`, `/None`, or a custom name). - pub name: Vec, + pub name: Arc<[u8]>, /// The alternate color space to use if the separation is not supported. pub alternate_space: Box, /// The tint transform function (transforms tint 0.0-1.0 to alternate space). @@ -23,7 +25,7 @@ pub struct SeparationColorSpace { impl FromPdfObject for SeparationColorSpace { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { let mut array = context.array()?; - let [_, name, _, _] = array.array() else { + let [_, _, _, _] = array.array() else { return Err(ColorSpaceError::InvalidColorSpace { description: format!( "/Separation requires 4 elements, found {}", @@ -34,7 +36,7 @@ impl FromPdfObject for SeparationColorSpace { }; Ok(Self { - name: Vec::from(name.try_bytes(array.source())?), + name: array.at(1)?, alternate_space: Box::new(array.at(2)?), tint_transform: array.at(3)?, }) @@ -45,7 +47,7 @@ impl SeparationColorSpace { pub fn apply(&self, components: &[f32]) -> Result { // If the name is "None", it represents the absence of all colorants. // Produce fully transparent output regardless of tint value. - if self.name == b"None" { + if &*self.name == b"None" { return Ok(Color::TRANSPARENT); } diff --git a/crates/pdf-content-stream-operators/src/lib.rs b/crates/pdf-content-stream-operators/src/lib.rs index 7b8e6f12..c7c4bd2b 100644 --- a/crates/pdf-content-stream-operators/src/lib.rs +++ b/crates/pdf-content-stream-operators/src/lib.rs @@ -3,9 +3,6 @@ pub mod error; pub mod marked_content_operators; pub mod variants; -pub mod pdf_operator_backend; -pub mod recording_pdf_operator_backend; - 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 5a565284..4f8f7dfc 100644 --- a/crates/pdf-content-stream-operators/src/marked_content_operators.rs +++ b/crates/pdf-content-stream-operators/src/marked_content_operators.rs @@ -2,10 +2,7 @@ use std::sync::Arc; use pdf_object_reader::{ReadResult, object_variant::ObjectVariant, pdf_string::PdfString}; -use crate::{ - OperandContext, - pdf_operator_backend::{BackendError, PdfOperatorBackend}, -}; +use crate::OperandContext; /// 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 @@ -34,11 +31,14 @@ impl BeginMarkedContentWithProps { Ok(Self::new(tag, properties)) } - /// Dispatches the operator to `backend`. - pub(crate) fn call( - &self, - backend: &mut T, - ) -> Result<(), BackendError> { - backend.begin_marked_content_with_properties(&self.tag, self.properties.as_deref()) + /// The tag indicating the role or nature of the marked-content sequence. + pub fn tag(&self) -> &[u8] { + &self.tag + } + + /// The name of the property list in the `/Properties` resource subdictionary, or `None` + /// when the property list is an inline dictionary. + pub fn properties(&self) -> Option<&[u8]> { + self.properties.as_deref() } } diff --git a/crates/pdf-content-stream-operators/src/pdf_operator_backend.rs b/crates/pdf-content-stream-operators/src/pdf_operator_backend.rs deleted file mode 100644 index 7447f53a..00000000 --- a/crates/pdf-content-stream-operators/src/pdf_operator_backend.rs +++ /dev/null @@ -1,850 +0,0 @@ -//! Defines traits for processing categorized PDF content stream operators. -//! Implementors of these traits can define how to handle specific groups of -//! PDF drawing, text, and state commands, allowing for different backends -//! (e.g., renderers, text extractors) to selectively implement functionality. - -use pdf_graphics::{LineCap, LineJoin, TextRenderingMode, transform::Transform}; - -use crate::PdfTextItem; - -/// Defines methods for handling PDF path construction operators. -/// -/// These operators are used to define shapes and paths before they are painted. -pub trait PathConstructionOps { - /// The error type that can be returned by operator handling methods. - type ErrorType: std::fmt::Debug + std::fmt::Display; - /// Moves the current point to the specified coordinates (x, y), starting a new subpath. - /// - /// # Parameters - /// - /// - `x`: The x-coordinate of the new current point. - /// - `y`: The y-coordinate of the new current point. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn move_to(&mut self, x: f32, y: f32) -> Result<(), Self::ErrorType>; - - /// Appends a straight line segment from the current point to (x, y). - /// The new current point becomes (x, y). - /// - /// # Parameters - /// - /// - `x`: The x-coordinate of the line's end point. - /// - `y`: The y-coordinate of the line's end point. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn line_to(&mut self, x: f32, y: f32) -> Result<(), Self::ErrorType>; - - /// 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 control points. - /// The new current point becomes (x3, y3). - /// - /// # Parameters - /// - /// - `x1`: The x-coordinate of the first control point. - /// - `y1`: The y-coordinate of the first control point. - /// - `x2`: The x-coordinate of the second control point. - /// - `y2`: The y-coordinate of the second control point. - /// - `x3`: The x-coordinate of the curve's end point. - /// - `y3`: The y-coordinate of the curve's end point. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn curve_to( - &mut self, - x1: f32, - y1: f32, - x2: f32, - y2: f32, - x3: f32, - y3: f32, - ) -> Result<(), Self::ErrorType>; - - /// Appends a cubic Bézier curve to the current path, where the current point is the first control point. - /// (x2, y2) is the second control point, and (x3, y3) is the end point. - /// The new current point becomes (x3, y3). - /// - /// # Parameters - /// - /// - `x2`: The x-coordinate of the second control point. - /// - `y2`: The y-coordinate of the second control point. - /// - `x3`: The x-coordinate of the curve's end point. - /// - `y3`: The y-coordinate of the curve's end point. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn curve_to_v(&mut self, x2: f32, y2: f32, x3: f32, y3: f32) -> Result<(), Self::ErrorType>; - - /// Appends a cubic Bézier curve to the current path, where the endpoint (x3, y3) is also the second control point. - /// (x1, y1) is the first control point. - /// The new current point becomes (x3, y3). - /// - /// # Parameters - /// - /// - `x1`: The x-coordinate of the first control point. - /// - `y1`: The y-coordinate of the first control point. - /// - `x3`: The x-coordinate of the curve's end point (and second control point). - /// - `y3`: The y-coordinate of the curve's end point (and second control point). - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn curve_to_y(&mut self, x1: f32, y1: f32, x3: f32, y3: f32) -> Result<(), Self::ErrorType>; - - /// Closes the current subpath by appending a line segment from the current point to the subpath's starting point. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn close_path(&mut self) -> Result<(), Self::ErrorType>; - - /// Appends a rectangle to the current path as a complete subpath. - /// The rectangle is defined by its bottom-left corner (x, y), width, and height. - /// - /// # Parameters - /// - /// - `x`: The x-coordinate of the rectangle's bottom-left corner. - /// - `y`: The y-coordinate of the rectangle's bottom-left corner. - /// - `width`: The width of the rectangle. - /// - `height`: The height of the rectangle. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn rectangle(&mut self, x: f32, y: f32, width: f32, height: f32) - -> Result<(), Self::ErrorType>; -} - -/// Defines methods to handle PDF Path Painting operators. -pub trait PathPaintingOps { - /// The error type that can be returned by operator handling methods. - type ErrorType: std::fmt::Debug + std::fmt::Display; - /// Strokes the current path using the current color and line style. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn stroke_path(&mut self) -> Result<(), Self::ErrorType>; - - /// Closes the current subpath and then strokes it. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn close_and_stroke_path(&mut self) -> Result<(), Self::ErrorType>; - - /// Fills the current path using the non-zero winding number rule. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn fill_path_nonzero_winding(&mut self) -> Result<(), Self::ErrorType>; - - /// Fills the current path using the even-odd rule. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn fill_path_even_odd(&mut self) -> Result<(), Self::ErrorType>; - - /// Fills and then strokes the current path, using the non-zero winding number rule for filling. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn fill_and_stroke_path_nonzero_winding(&mut self) -> Result<(), Self::ErrorType>; - - /// Fills and then strokes the current path, using the even-odd rule for filling. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn fill_and_stroke_path_even_odd(&mut self) -> Result<(), Self::ErrorType>; - - /// Closes, fills, and then strokes the current path, using the non-zero winding number rule for filling. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn close_fill_and_stroke_path_nonzero_winding(&mut self) -> Result<(), Self::ErrorType>; - - /// Closes, fills, and then strokes the current path, using the even-odd rule for filling. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn close_fill_and_stroke_path_even_odd(&mut self) -> Result<(), Self::ErrorType>; - - /// Ends the current path without filling or stroking it, effectively discarding it. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn end_path_no_op(&mut self) -> Result<(), Self::ErrorType>; -} - -/// Defines methods to handle PDF Clipping Path operators. -pub trait ClippingPathOps { - /// The error type that can be returned by operator handling methods. - type ErrorType: std::fmt::Debug + std::fmt::Display; - /// Modifies the current clipping path by intersecting it with the current path, - /// using the non-zero winding number rule. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn clip_path_nonzero_winding(&mut self) -> Result<(), Self::ErrorType>; - - /// Modifies the current clipping path by intersecting it with the current path, - /// using the even-odd rule. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn clip_path_even_odd(&mut self) -> Result<(), Self::ErrorType>; -} - -/// Defines methods to handle PDF Graphics State operators. -pub trait GraphicsStateOps { - /// The error type that can be returned by operator handling methods. - type ErrorType: std::fmt::Debug + std::fmt::Display; - /// Saves the current graphics state onto the graphics state stack. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn save_graphics_state(&mut self) -> Result<(), Self::ErrorType>; - - /// Restores the graphics state by removing the most recently saved state from the stack. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn restore_graphics_state(&mut self) -> Result<(), Self::ErrorType>; - - /// Modifies the current transformation matrix (CTM) by concatenating it with the specified matrix. - /// The matrix is `[a b c d e f]`. - /// - /// # Parameters - /// - /// - `transform`: The transformation matrix to concatenate with the current CTM. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn concat_matrix(&mut self, transform: &Transform) -> Result<(), Self::ErrorType>; - - /// Sets the line width for path stroking. - /// - /// # Parameters - /// - /// - `width`: The new line width in user space units. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_line_width(&mut self, width: f32) -> Result<(), Self::ErrorType>; - - /// Sets the line cap style for path stroking. - /// - /// # Parameters - /// - /// - `cap_style`: An integer representing the cap style (e.g., 0 for butt, 1 for - /// round, 2 for projecting square). - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_line_cap(&mut self, cap_style: LineCap) -> Result<(), Self::ErrorType>; - - /// Sets the line join style for path stroking. - /// - /// # Parameters - /// - /// - `join_style`: An integer representing the join style (e.g., 0 for miter, 1 for round, 2 for bevel). - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_line_join(&mut self, join_style: LineJoin) -> Result<(), Self::ErrorType>; - - /// Sets the miter limit for path stroking. - /// - /// # Parameters - /// - /// - `limit`: The miter limit. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_miter_limit(&mut self, limit: f32) -> Result<(), Self::ErrorType>; - - /// Sets the dash pattern for path stroking. - /// - /// # Parameters - /// - /// - `dash_array`: A slice representing the lengths of alternating dashes and gaps. - /// - `dash_phase`: The distance into the dash pattern at which to start. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_dash_pattern( - &mut self, - dash_array: &[f32], - dash_phase: f32, - ) -> Result<(), Self::ErrorType>; - - /// Sets the rendering intent for color reproduction. - /// - /// # Parameters - /// - /// - `intent`: A string representing the rendering intent. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_rendering_intent(&mut self, intent: &[u8]) -> Result<(), Self::ErrorType>; - - /// Sets the flatness tolerance, controlling the accuracy of curve rendering. - /// - /// # Parameters - /// - /// - `tolerance`: The flatness tolerance value. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_flatness_tolerance(&mut self, tolerance: f32) -> Result<(), Self::ErrorType>; - - /// Sets multiple graphics state parameters from a named graphics state parameter dictionary. - /// The dictionary is expected to be in the resource dictionary. - /// - /// # Parameters - /// - /// - `dict_name`: The name of the graphics state parameter dictionary. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_graphics_state_from_dict(&mut self, dict_name: &[u8]) -> Result<(), Self::ErrorType>; -} - -/// Defines methods to handle PDF Color operators. -pub trait ColorOps { - /// The error type that can be returned by operator handling methods. - type ErrorType: std::fmt::Debug + std::fmt::Display; - /// Sets the color space for subsequent stroking operations. - /// - /// # Parameters - /// - /// - `name`: The name of the color space (e.g., "DeviceGray", "DeviceRGB", "DeviceCMYK"). - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_stroking_color_space(&mut self, name: &[u8]) -> Result<(), Self::ErrorType>; - - /// Sets the color space for subsequent non-stroking (fill) operations. - /// - /// # Parameters - /// - /// - `name`: The name of the color space (e.g., "DeviceGray", "DeviceRGB", "DeviceCMYK"). - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_non_stroking_color_space(&mut self, name: &[u8]) -> Result<(), Self::ErrorType>; - - /// Sets the color for subsequent stroking operations, using the current stroking color space. - /// The number of components depends on the active color space. - /// - /// # Parameters - /// - /// - `components`: A slice of color components. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_stroking_color(&mut self, components: &[f32]) -> Result<(), Self::ErrorType>; - - /// Sets the color for stroking operations, potentially with a pattern name. - /// Used for color spaces like ICCBased, Separation, DeviceN, or Pattern. - /// - /// # Parameters - /// - /// - `components`: A slice of color components. - /// - `pattern_name`: A name of a pattern,. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_stroking_color_extended( - &mut self, - components: &[f32], - pattern_name: &[u8], - ) -> Result<(), Self::ErrorType>; - - /// Sets the color for subsequent non-stroking (fill) operations, using the current non-stroking color space. - /// The number of components depends on the active color space. - /// - /// # Parameters - /// - /// - `components`: A slice of color components. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_non_stroking_color(&mut self, components: &[f32]) -> Result<(), Self::ErrorType>; - - /// Sets the color for non-stroking (fill) operations, potentially with a pattern name. - /// Used for color spaces like ICCBased, Separation, DeviceN, or Pattern. - /// - /// # Parameters - /// - /// - `components`: A slice of color components. - /// - `pattern_name`: A name of a pattern. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_non_stroking_color_extended( - &mut self, - components: &[f32], - pattern_name: &[u8], - ) -> Result<(), Self::ErrorType>; - - /// Sets the stroking color to a grayscale value. - /// Assumes DeviceGray color space or similar. - /// - /// # Parameters - /// - /// - `gray`: The gray level (0.0 for black, 1.0 for white). - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_stroking_gray(&mut self, gray: f32) -> Result<(), Self::ErrorType>; - - /// Sets the non-stroking (fill) color to a grayscale value. - /// Assumes DeviceGray color space or similar. - /// - /// # Parameters - /// - /// - `gray`: The gray level (0.0 for black, 1.0 for white). - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_non_stroking_gray(&mut self, gray: f32) -> Result<(), Self::ErrorType>; - - /// Sets the stroking color to an RGB value. - /// Assumes DeviceRGB color space or similar. - /// - /// # Parameters - /// - /// - `r`: Red component (0.0 to 1.0). - /// - `g`: Green component (0.0 to 1.0). - /// - `b`: Blue component (0.0 to 1.0). - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_stroking_rgb(&mut self, r: f32, g: f32, b: f32) -> Result<(), Self::ErrorType>; - - /// Sets the non-stroking (fill) color to an RGB value. - /// Assumes DeviceRGB color space or similar. - /// - /// # Parameters - /// - /// - `r`: Red component (0.0 to 1.0). - /// - `g`: Green component (0.0 to 1.0). - /// - `b`: Blue component (0.0 to 1.0). - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_non_stroking_rgb(&mut self, r: f32, g: f32, b: f32) -> Result<(), Self::ErrorType>; - - /// Sets the stroking color to a CMYK value. - /// Assumes DeviceCMYK color space or similar. - /// - /// # Parameters - /// - /// - `c`: Cyan component (0.0 to 1.0). - /// - `m`: Magenta component (0.0 to 1.0). - /// - `y`: Yellow component (0.0 to 1.0). - /// - `k`: Black (Key) component (0.0 to 1.0). - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_stroking_cmyk(&mut self, c: f32, m: f32, y: f32, k: f32) -> Result<(), Self::ErrorType>; - - /// Sets the non-stroking (fill) color to a CMYK value. - /// Assumes DeviceCMYK color space or similar. - /// - /// # Parameters - /// - /// - `c`: Cyan component (0.0 to 1.0). - /// - `m`: Magenta component (0.0 to 1.0). - /// - `y`: Yellow component (0.0 to 1.0). - /// - `k`: Black (Key) component (0.0 to 1.0). - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_non_stroking_cmyk( - &mut self, - c: f32, - m: f32, - y: f32, - k: f32, - ) -> Result<(), Self::ErrorType>; -} - -/// Defines methods to handle PDF Text Object operators. -pub trait TextObjectOps { - /// The error type that can be returned by operator handling methods. - type ErrorType: std::fmt::Debug + std::fmt::Display; - /// Begins a text object, initializing the text transformation matrices. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn begin_text_object(&mut self) -> Result<(), Self::ErrorType>; - - /// Ends a text object, discarding the text transformation matrices. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn end_text_object(&mut self) -> Result<(), Self::ErrorType>; -} - -/// Defines methods to handle PDF Text State operators. -pub trait TextStateOps { - /// The error type that can be returned by operator handling methods. - type ErrorType: std::fmt::Debug + std::fmt::Display; - /// Sets the character spacing. - /// - /// # Parameters - /// - /// - `spacing`: The character spacing in unscaled text space units. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_character_spacing(&mut self, spacing: f32) -> Result<(), Self::ErrorType>; - - /// Sets the word spacing. - /// - /// # Parameters - /// - /// - `spacing`: The word spacing in unscaled text space units. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_word_spacing(&mut self, spacing: f32) -> Result<(), Self::ErrorType>; - - /// Sets the horizontal scaling for text. - /// - /// # Parameters - /// - /// - `scale_percent`: The horizontal scaling factor as a percentage (e.g., 100.0 for 100%). - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_horizontal_text_scaling(&mut self, scale_percent: f32) -> Result<(), Self::ErrorType>; - - /// Sets the text leading (vertical distance between baselines). - /// - /// # Parameters - /// - /// - `leading`: The text leading in unscaled text space units. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_text_leading(&mut self, leading: f32) -> Result<(), Self::ErrorType>; - - /// Sets the text font and size. - /// - /// # Parameters - /// - /// - `font_name`: The name of the font resource. - /// - `size`: The font size in unscaled text space units. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_font_and_size(&mut self, font_name: &[u8], size: f32) -> Result<(), Self::ErrorType>; - - /// Sets the text rendering mode (e.g., fill, stroke, clip). - /// - /// # Parameters - /// - /// - `mode`: An integer representing the rendering mode. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_text_rendering_mode(&mut self, mode: TextRenderingMode) -> Result<(), Self::ErrorType>; - - /// Sets the text rise (vertical baseline offset). - /// - /// # Parameters - /// - /// - `rise`: The text rise in unscaled text space units. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_text_rise(&mut self, rise: f32) -> Result<(), Self::ErrorType>; -} - -/// Defines methods to handle PDF Text Positioning operators. -pub trait TextPositioningOps { - /// The error type that can be returned by operator handling methods. - type ErrorType: std::fmt::Debug + std::fmt::Display; - /// Moves the text position to the start of the next line, offset by (tx, ty). - /// - /// # Parameters - /// - /// - `tx`: The horizontal offset in unscaled text space units. - /// - `ty`: The vertical offset in unscaled text space units. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn move_text_position(&mut self, tx: f32, ty: f32) -> Result<(), Self::ErrorType>; - - /// Moves the text position and sets the text leading. - /// The leading is set to -ty. - /// - /// # Parameters - /// - /// - `tx`: The horizontal offset in unscaled text space units. - /// - `ty`: The vertical offset in unscaled text space units; leading is set to -ty. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn move_text_position_and_set_leading( - &mut self, - tx: f32, - ty: f32, - ) -> Result<(), Self::ErrorType>; - - /// Sets the text matrix and text line matrix. - /// - /// # Parameters - /// - /// - `transform`: The transformation matrix to set as the text matrix and text line matrix. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_text_matrix(&mut self, transform: &Transform) -> Result<(), Self::ErrorType>; - - /// Moves to the start of the next text line, using the current text leading. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn move_to_start_of_next_line(&mut self) -> Result<(), Self::ErrorType>; -} - -/// Defines methods to handle PDF Text Showing operators. -pub trait TextShowingOps { - /// The error type that can be returned by operator handling methods. - type ErrorType: std::fmt::Debug + std::fmt::Display; - /// Shows a text string at the current text position. - /// - /// # Parameters - /// - /// - `text`: The text item to show, typically encoded according to the current font. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn show_text(&mut self, text: &PdfTextItem) -> Result<(), Self::ErrorType>; - - /// Shows text, allowing individual adjustments to glyph positions. - /// - /// # Parameters - /// - /// - `elements`: A slice of `PdfTextItem`s, which can be strings or numeric adjustments. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn show_text_with_glyph_positioning( - &mut self, - elements: &[PdfTextItem], - ) -> Result<(), Self::ErrorType>; - - /// Moves to the next line and shows a text string. - /// - /// # Parameters - /// - /// - `text`: The text item to show. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn move_to_next_line_and_show_text( - &mut self, - text: &PdfTextItem, - ) -> Result<(), Self::ErrorType>; - - /// Sets word and character spacing, moves to the next line, and shows a text string. - /// - /// # Parameters - /// - /// - `word_spacing`: The word spacing to set. - /// - `char_spacing`: The character spacing to set. - /// - `text`: The text item to show. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn set_spacing_and_show_text( - &mut self, - word_spacing: f32, - char_spacing: f32, - text: &PdfTextItem, - ) -> Result<(), Self::ErrorType>; -} - -/// Defines methods to handle PDF XObject operators. -pub trait XObjectOps { - /// The error type that can be returned by operator handling methods. - type ErrorType: std::fmt::Debug + std::fmt::Display; - /// Invokes a named external object (XObject), such as an image or a form. - /// - /// # Parameters - /// - /// - `xobject_name`: The name of the XObject resource. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn invoke_xobject(&mut self, xobject_name: &[u8]) -> Result<(), Self::ErrorType>; - - /// Paints an inline image. - fn paint_inline_image(&mut self, image: &pdf_image::InlineImage) - -> Result<(), Self::ErrorType>; -} - -/// Defines methods to handle PDF Shading operators. -pub trait ShadingOps { - /// The error type that can be returned by operator handling methods. - type ErrorType: std::fmt::Debug + std::fmt::Display; - /// Paints an area defined by a named shading pattern. - /// - /// # Parameters - /// - /// - `shading_name`: The name of the shading resource. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn paint_shading(&mut self, shading_name: &[u8]) -> Result<(), Self::ErrorType>; -} - -/// Defines methods to handle PDF Marked Content operators. -pub trait MarkedContentOps { - /// The error type that can be returned by operator handling methods. - type ErrorType: std::fmt::Debug + std::fmt::Display; - /// Defines a marked-content point, associating it with a tag. - /// - /// # Parameters - /// - /// - `tag`: The tag for the marked-content point. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn mark_point(&mut self, tag: &[u8]) -> Result<(), Self::ErrorType>; - - /// Defines a marked-content point, associating it with a tag and a property list. - /// - /// # Parameters - /// - /// - `tag`: The tag for the marked-content point. - /// - `properties_name_or_dict`: The name of a property list in the resource dictionary or an inline dictionary. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn mark_point_with_properties( - &mut self, - tag: &[u8], - properties_name_or_dict: &[u8], - ) -> Result<(), Self::ErrorType>; - - /// Begins a marked-content sequence with a tag. - /// - /// # Parameters - /// - /// - `tag`: The tag for the marked-content sequence. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn begin_marked_content(&mut self, tag: &[u8]) -> Result<(), Self::ErrorType>; - - /// Begins a marked-content sequence with a tag and associated properties. - /// - /// # Parameters - /// - /// - `tag`: The tag for the marked-content sequence. - /// - `properties`: The name of a property list in the `/Properties` resource - /// subdictionary, or `None` when the property list is an inline dictionary. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn begin_marked_content_with_properties( - &mut self, - tag: &[u8], - properties: Option<&[u8]>, - ) -> Result<(), Self::ErrorType>; - - /// Ends a marked-content sequence. - /// - /// # Returns - /// - /// A `Result` indicating success or an `ErrorType` on failure. - fn end_marked_content(&mut self) -> Result<(), Self::ErrorType>; -} - -/// The error type produced by a [`PdfOperatorBackend`] implementor. -pub type BackendError = ::ErrorType; - -/// A comprehensive backend that implements all operator categories. -/// This can be used as a blanket implementation if a backend supports everything, -/// or as a way to group all the specialized traits. -pub trait PdfOperatorBackend: - PathConstructionOps - + PathPaintingOps::ErrorType> - + ClippingPathOps::ErrorType> - + GraphicsStateOps::ErrorType> - + ColorOps::ErrorType> - + TextObjectOps::ErrorType> - + TextStateOps::ErrorType> - + TextPositioningOps::ErrorType> - + TextShowingOps::ErrorType> - + XObjectOps::ErrorType> - + ShadingOps::ErrorType> - + MarkedContentOps::ErrorType> -{ -} diff --git a/crates/pdf-content-stream-operators/src/recording_pdf_operator_backend.rs b/crates/pdf-content-stream-operators/src/recording_pdf_operator_backend.rs deleted file mode 100644 index ddc34c8d..00000000 --- a/crates/pdf-content-stream-operators/src/recording_pdf_operator_backend.rs +++ /dev/null @@ -1,752 +0,0 @@ -use bytes::Bytes; -use pdf_graphics::LineCap; -use pdf_graphics::LineJoin; -use pdf_graphics::TextRenderingMode; -use pdf_graphics::transform::Transform; -use pdf_image::InlineImage; -use thiserror::Error; - -use crate::PdfTextItem; -use crate::pdf_operator_backend::*; - -/// Represents a recorded operation with its parameters. -#[derive(Debug, Clone, PartialEq)] -pub enum RecordedOperation { - MoveTo { - x: f32, - y: f32, - }, - LineTo { - x: f32, - y: f32, - }, - CurveTo { - x1: f32, - y1: f32, - x2: f32, - y2: f32, - x3: f32, - y3: f32, - }, - CurveToV { - x2: f32, - y2: f32, - x3: f32, - y3: f32, - }, - CurveToY { - x1: f32, - y1: f32, - x3: f32, - y3: f32, - }, - ClosePath, - Rectangle { - x: f32, - y: f32, - width: f32, - height: f32, - }, - StrokePath, - CloseAndStrokePath, - FillPathNonZeroWinding, - FillPathEvenOdd, - FillAndStrokePathNonZeroWinding, - FillAndStrokePathEvenOdd, - CloseFillAndStrokePathNonZeroWinding, - CloseFillAndStrokePathEvenOdd, - EndPathNoOp, - ClipPathNonZeroWinding, - ClipPathEvenOdd, - SaveGraphicsState, - RestoreGraphicsState, - ConcatMatrix { - transform: Transform, - }, - SetLineWidth { - width: f32, - }, - SetLineCap { - cap_style: LineCap, - }, - SetLineJoin { - join_style: LineJoin, - }, - SetMiterLimit { - limit: f32, - }, - SetDashPattern { - dash_array: Vec, - dash_phase: f32, - }, - SetRenderingIntent { - intent: Vec, - }, - SetFlatnessTolerance { - tolerance: f32, - }, - SetGraphicsStateFromDict { - dict_name: Vec, - }, - SetStrokingColorSpace { - name: Vec, - }, - SetNonStrokingColorSpace { - name: Vec, - }, - SetStrokingColor { - components: Vec, - }, - SetStrokingColorExtended { - components: Vec, - pattern_name: Vec, - }, - SetNonStrokingColor { - components: Vec, - }, - SetNonStrokingColorExtended { - components: Vec, - pattern_name: Vec, - }, - SetStrokingGray { - gray: f32, - }, - SetNonStrokingGray { - gray: f32, - }, - SetStrokingRgb { - r: f32, - g: f32, - b: f32, - }, - SetNonStrokingRgb { - r: f32, - g: f32, - b: f32, - }, - SetStrokingCmyk { - c: f32, - m: f32, - y: f32, - k: f32, - }, - SetNonStrokingCmyk { - c: f32, - m: f32, - y: f32, - k: f32, - }, - BeginTextObject, - EndTextObject, - SetCharacterSpacing { - spacing: f32, - }, - SetWordSpacing { - spacing: f32, - }, - SetHorizontalTextScaling { - scale_percent: f32, - }, - SetTextLeading { - leading: f32, - }, - SetFontAndSize { - font_name: Vec, - size: f32, - }, - SetTextRenderingMode { - mode: TextRenderingMode, - }, - SetTextRise { - rise: f32, - }, - MoveTextPosition { - tx: f32, - ty: f32, - }, - MoveTextPositionAndSetLeading { - tx: f32, - ty: f32, - }, - SetTextMatrix { - transform: Transform, - }, - MoveToStartOfNextLine, - ShowText { - text: PdfTextItem, - }, - ShowTextWithGlyphPositioning { - elements: Vec, - }, - MoveToNextLineAndShowText { - text: PdfTextItem, - }, - SetSpacingAndShowText { - word_spacing: f32, - char_spacing: f32, - text: PdfTextItem, - }, - InvokeXObject { - xobject_name: Vec, - }, - PaintInlineImage { - data: Bytes, - }, - PaintShading { - shading_name: Vec, - }, - MarkPoint { - tag: Vec, - }, - MarkPointWithProperties { - tag: Vec, - properties_name_or_dict: Vec, - }, - BeginMarkedContent { - tag: Vec, - }, - BeginMarkedContentWithProperties { - tag: Vec, - properties: Option>, - }, - EndMarkedContent, -} - -/// A [`PdfOperatorBackend`] implementation that records all operations called on it. -#[derive(Debug, Default, Clone, PartialEq)] -pub struct RecordingBackend { - pub operations: Vec, -} - -impl RecordingBackend { - pub fn new() -> Self { - Default::default() - } -} - -#[derive(Debug, Error)] -pub enum PdfRecordingCanvasError {} - -impl PathConstructionOps for RecordingBackend { - type ErrorType = PdfRecordingCanvasError; - fn move_to(&mut self, x: f32, y: f32) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::MoveTo { x, y }); - Ok(()) - } - - fn line_to(&mut self, x: f32, y: f32) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::LineTo { x, y }); - Ok(()) - } - - fn curve_to( - &mut self, - x1: f32, - y1: f32, - x2: f32, - y2: f32, - x3: f32, - y3: f32, - ) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::CurveTo { - x1, - y1, - x2, - y2, - x3, - y3, - }); - Ok(()) - } - - fn curve_to_v(&mut self, x2: f32, y2: f32, x3: f32, y3: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::CurveToV { x2, y2, x3, y3 }); - Ok(()) - } - - fn curve_to_y(&mut self, x1: f32, y1: f32, x3: f32, y3: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::CurveToY { x1, y1, x3, y3 }); - Ok(()) - } - - fn close_path(&mut self) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::ClosePath); - Ok(()) - } - - fn rectangle( - &mut self, - x: f32, - y: f32, - width: f32, - height: f32, - ) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::Rectangle { - x, - y, - width, - height, - }); - Ok(()) - } -} - -impl PathPaintingOps for RecordingBackend { - type ErrorType = PdfRecordingCanvasError; - fn stroke_path(&mut self) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::StrokePath); - Ok(()) - } - - fn close_and_stroke_path(&mut self) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::CloseAndStrokePath); - Ok(()) - } - - fn fill_path_nonzero_winding(&mut self) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::FillPathNonZeroWinding); - Ok(()) - } - - fn fill_path_even_odd(&mut self) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::FillPathEvenOdd); - Ok(()) - } - - fn fill_and_stroke_path_nonzero_winding(&mut self) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::FillAndStrokePathNonZeroWinding); - Ok(()) - } - - fn fill_and_stroke_path_even_odd(&mut self) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::FillAndStrokePathEvenOdd); - Ok(()) - } - - fn close_fill_and_stroke_path_nonzero_winding(&mut self) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::CloseFillAndStrokePathNonZeroWinding); - Ok(()) - } - - fn close_fill_and_stroke_path_even_odd(&mut self) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::CloseFillAndStrokePathEvenOdd); - Ok(()) - } - - fn end_path_no_op(&mut self) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::EndPathNoOp); - Ok(()) - } -} - -impl ClippingPathOps for RecordingBackend { - type ErrorType = PdfRecordingCanvasError; - fn clip_path_nonzero_winding(&mut self) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::ClipPathNonZeroWinding); - Ok(()) - } - - fn clip_path_even_odd(&mut self) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::ClipPathEvenOdd); - Ok(()) - } -} - -impl GraphicsStateOps for RecordingBackend { - type ErrorType = PdfRecordingCanvasError; - fn save_graphics_state(&mut self) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::SaveGraphicsState); - Ok(()) - } - - fn restore_graphics_state(&mut self) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::RestoreGraphicsState); - Ok(()) - } - - fn concat_matrix(&mut self, transform: &Transform) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::ConcatMatrix { - transform: *transform, - }); - Ok(()) - } - - fn set_line_width(&mut self, width: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetLineWidth { width }); - Ok(()) - } - - fn set_line_cap(&mut self, cap_style: LineCap) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetLineCap { cap_style }); - Ok(()) - } - - fn set_line_join(&mut self, join_style: LineJoin) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetLineJoin { join_style }); - Ok(()) - } - - fn set_miter_limit(&mut self, limit: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetMiterLimit { limit }); - Ok(()) - } - - fn set_dash_pattern( - &mut self, - dash_array: &[f32], - dash_phase: f32, - ) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::SetDashPattern { - dash_array: dash_array.to_vec(), - dash_phase, - }); - Ok(()) - } - - fn set_rendering_intent(&mut self, intent: &[u8]) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::SetRenderingIntent { - intent: intent.to_vec(), - }); - Ok(()) - } - - fn set_flatness_tolerance(&mut self, tolerance: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetFlatnessTolerance { tolerance }); - Ok(()) - } - - fn set_graphics_state_from_dict(&mut self, dict_name: &[u8]) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetGraphicsStateFromDict { - dict_name: dict_name.to_vec(), - }); - Ok(()) - } -} - -impl ColorOps for RecordingBackend { - type ErrorType = PdfRecordingCanvasError; - fn set_stroking_color_space(&mut self, name: &[u8]) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetStrokingColorSpace { - name: name.to_vec(), - }); - Ok(()) - } - - fn set_non_stroking_color_space(&mut self, name: &[u8]) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetNonStrokingColorSpace { - name: name.to_vec(), - }); - Ok(()) - } - - fn set_stroking_color(&mut self, components: &[f32]) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::SetStrokingColor { - components: components.to_vec(), - }); - Ok(()) - } - - fn set_stroking_color_extended( - &mut self, - components: &[f32], - pattern_name: &[u8], - ) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetStrokingColorExtended { - components: components.to_vec(), - pattern_name: pattern_name.to_vec(), - }); - Ok(()) - } - - fn set_non_stroking_color(&mut self, components: &[f32]) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetNonStrokingColor { - components: components.to_vec(), - }); - Ok(()) - } - - fn set_non_stroking_color_extended( - &mut self, - components: &[f32], - pattern_name: &[u8], - ) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetNonStrokingColorExtended { - components: components.to_vec(), - pattern_name: pattern_name.to_vec(), - }); - Ok(()) - } - - fn set_stroking_gray(&mut self, gray: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetStrokingGray { gray }); - Ok(()) - } - - fn set_non_stroking_gray(&mut self, gray: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetNonStrokingGray { gray }); - Ok(()) - } - - fn set_stroking_rgb(&mut self, r: f32, g: f32, b: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetStrokingRgb { r, g, b }); - Ok(()) - } - - fn set_non_stroking_rgb(&mut self, r: f32, g: f32, b: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetNonStrokingRgb { r, g, b }); - Ok(()) - } - - fn set_stroking_cmyk(&mut self, c: f32, m: f32, y: f32, k: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetStrokingCmyk { c, m, y, k }); - Ok(()) - } - - fn set_non_stroking_cmyk( - &mut self, - c: f32, - m: f32, - y: f32, - k: f32, - ) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetNonStrokingCmyk { c, m, y, k }); - Ok(()) - } -} - -impl TextObjectOps for RecordingBackend { - type ErrorType = PdfRecordingCanvasError; - fn begin_text_object(&mut self) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::BeginTextObject); - Ok(()) - } - - fn end_text_object(&mut self) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::EndTextObject); - Ok(()) - } -} - -impl TextStateOps for RecordingBackend { - type ErrorType = PdfRecordingCanvasError; - fn set_character_spacing(&mut self, spacing: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetCharacterSpacing { spacing }); - Ok(()) - } - - fn set_word_spacing(&mut self, spacing: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetWordSpacing { spacing }); - Ok(()) - } - - fn set_horizontal_text_scaling(&mut self, scale_percent: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetHorizontalTextScaling { scale_percent }); - Ok(()) - } - - fn set_text_leading(&mut self, leading: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetTextLeading { leading }); - Ok(()) - } - - fn set_font_and_size(&mut self, font_name: &[u8], size: f32) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::SetFontAndSize { - font_name: font_name.to_vec(), - size, - }); - Ok(()) - } - - fn set_text_rendering_mode(&mut self, mode: TextRenderingMode) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetTextRenderingMode { mode }); - Ok(()) - } - - fn set_text_rise(&mut self, rise: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetTextRise { rise }); - Ok(()) - } -} - -impl TextPositioningOps for RecordingBackend { - type ErrorType = PdfRecordingCanvasError; - fn move_text_position(&mut self, tx: f32, ty: f32) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::MoveTextPosition { tx, ty }); - Ok(()) - } - - fn move_text_position_and_set_leading( - &mut self, - tx: f32, - ty: f32, - ) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::MoveTextPositionAndSetLeading { tx, ty }); - Ok(()) - } - - fn set_text_matrix(&mut self, transform: &Transform) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::SetTextMatrix { - transform: *transform, - }); - Ok(()) - } - - fn move_to_start_of_next_line(&mut self) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::MoveToStartOfNextLine); - Ok(()) - } -} - -impl TextShowingOps for RecordingBackend { - type ErrorType = PdfRecordingCanvasError; - fn show_text(&mut self, text: &PdfTextItem) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::ShowText { text: text.clone() }); - Ok(()) - } - - fn show_text_with_glyph_positioning( - &mut self, - elements: &[PdfTextItem], - ) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::ShowTextWithGlyphPositioning { - elements: elements.to_vec(), - }); - Ok(()) - } - - fn move_to_next_line_and_show_text( - &mut self, - text: &PdfTextItem, - ) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::MoveToNextLineAndShowText { text: text.clone() }); - Ok(()) - } - - fn set_spacing_and_show_text( - &mut self, - word_spacing: f32, - char_spacing: f32, - text: &PdfTextItem, - ) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::SetSpacingAndShowText { - word_spacing, - char_spacing, - text: text.clone(), - }); - Ok(()) - } -} - -impl XObjectOps for RecordingBackend { - type ErrorType = PdfRecordingCanvasError; - fn invoke_xobject(&mut self, xobject_name: &[u8]) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::InvokeXObject { - xobject_name: xobject_name.to_vec(), - }); - Ok(()) - } - - fn paint_inline_image(&mut self, image: &InlineImage) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::PaintInlineImage { - data: image.shared_data(), - }); - Ok(()) - } -} - -impl ShadingOps for RecordingBackend { - type ErrorType = PdfRecordingCanvasError; - fn paint_shading(&mut self, shading_name: &[u8]) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::PaintShading { - shading_name: shading_name.to_vec(), - }); - Ok(()) - } -} - -impl MarkedContentOps for RecordingBackend { - type ErrorType = PdfRecordingCanvasError; - fn mark_point(&mut self, tag: &[u8]) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::MarkPoint { tag: tag.to_vec() }); - Ok(()) - } - - fn mark_point_with_properties( - &mut self, - tag: &[u8], - properties_name_or_dict: &[u8], - ) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::MarkPointWithProperties { - tag: tag.to_vec(), - properties_name_or_dict: properties_name_or_dict.to_vec(), - }); - Ok(()) - } - - fn begin_marked_content(&mut self, tag: &[u8]) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::BeginMarkedContent { tag: tag.to_vec() }); - Ok(()) - } - - fn begin_marked_content_with_properties( - &mut self, - tag: &[u8], - properties: Option<&[u8]>, - ) -> Result<(), Self::ErrorType> { - self.operations - .push(RecordedOperation::BeginMarkedContentWithProperties { - tag: tag.to_vec(), - properties: properties.map(<[u8]>::to_vec), - }); - Ok(()) - } - - fn end_marked_content(&mut self) -> Result<(), Self::ErrorType> { - self.operations.push(RecordedOperation::EndMarkedContent); - Ok(()) - } -} - -impl PdfOperatorBackend for RecordingBackend {} diff --git a/crates/pdf-content-stream-operators/src/variants.rs b/crates/pdf-content-stream-operators/src/variants.rs index b68386f4..e8630d4f 100644 --- a/crates/pdf-content-stream-operators/src/variants.rs +++ b/crates/pdf-content-stream-operators/src/variants.rs @@ -1,14 +1,12 @@ use std::sync::Arc; use pdf_graphics::{LineCap, LineJoin, TextRenderingMode, transform::Transform}; -use pdf_image::InlineImage; +use pdf_image::PdfImage; use pdf_object_reader::{ReadResult, pdf_string::PdfString}; use crate::{ - OperandContext, PdfTextItem, - color_operators::ColorOperands, + OperandContext, PdfTextItem, color_operators::ColorOperands, marked_content_operators::BeginMarkedContentWithProps, - pdf_operator_backend::{BackendError, PdfOperatorBackend}, }; /// One PDF content-stream operation with its decoded operands. @@ -352,7 +350,7 @@ pub enum PdfOperatorVariant { /// by higher-level logic. Backends may choose to track nesting depth, /// but by default these operators are no-ops. BeginCompatibility, - InlineImage(Arc), + InlineImage(Arc), /// Ends a compatibility section (EX). EndCompatibility, /// Paints the shape and color shading defined by a shading dictionary resource. @@ -595,131 +593,18 @@ impl PdfOperatorVariant { }; Ok(Some(operator)) } - - /// Dispatches the operation to `backend`. - pub fn call(&self, backend: &mut T) -> Result<(), BackendError> { - match self { - 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 { - 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(), - Self::FillPathEvenOdd => backend.fill_path_even_odd(), - Self::FillAndStrokePathNonZero => backend.fill_and_stroke_path_nonzero_winding(), - Self::FillAndStrokePathEvenOdd => backend.fill_and_stroke_path_even_odd(), - Self::CloseFillAndStrokePathNonZero => { - backend.close_fill_and_stroke_path_nonzero_winding() - } - Self::CloseFillAndStrokePathEvenOdd => backend.close_fill_and_stroke_path_even_odd(), - Self::EndPath => backend.end_path_no_op(), - Self::ClipNonZero => backend.clip_path_nonzero_winding(), - Self::ClipEvenOdd => backend.clip_path_even_odd(), - 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 { 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 { 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 { 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 { 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, + ArrayContext, ObjectReader, 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()); @@ -776,38 +661,4 @@ mod tests { 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/src/content_stream.rs b/crates/pdf-content-stream/src/content_stream.rs index ef9a2cc1..aed745e4 100644 --- a/crates/pdf-content-stream/src/content_stream.rs +++ b/crates/pdf-content-stream/src/content_stream.rs @@ -74,8 +74,7 @@ mod tests { use pdf_content_stream_operators::{ PdfTextItem, - recording_pdf_operator_backend::{RecordedOperation, RecordingBackend}, - variants::PdfOperatorVariant, + variants::PdfOperatorVariant::{self, *}, }; use pdf_object_reader::{ dictionary::Dictionary, object_error::ObjectError, object_resolver::ObjectResolver, @@ -106,16 +105,6 @@ mod tests { ) } - fn recorded_operations(operators: &[PdfOperatorVariant]) -> Vec { - let mut backend = RecordingBackend::default(); - for operator in operators { - operator - .call(&mut backend) - .expect("operator should dispatch"); - } - backend.operations - } - struct MapResolver { objects: BTreeMap, } @@ -164,20 +153,14 @@ mod tests { assert_eq!(parsed.id, 0); assert!(matches!( - parsed.operators.first(), - Some(PdfOperatorVariant::BeginCompatibility) - )); - assert!(matches!( - parsed.operators.get(1), - Some(PdfOperatorVariant::EndCompatibility) - )); - assert_eq!( - recorded_operations(&parsed.operators), - vec![ - RecordedOperation::MoveTo { x: 10.0, y: 20.0 }, - RecordedOperation::LineTo { x: 30.0, y: 40.0 }, + parsed.operators.as_slice(), + [ + BeginCompatibility, + EndCompatibility, + MoveTo { x: 10.0, y: 20.0 }, + LineTo { x: 30.0, y: 40.0 } ] - ); + )); } #[test] @@ -222,7 +205,7 @@ mod tests { } #[test] - fn parsed_inline_image_can_be_dispatched() { + fn content_stream_read_parses_inline_image() { let reader = pdf_object_reader::ObjectReader::new(&PassthroughResolver); let parsed = reader .read::(&ObjectVariant::Stream(stream_object( @@ -230,29 +213,8 @@ mod tests { b"BI /W 1 /H 1 /BPC 8 /CS /G ID \x00 EI", ))) .expect("inline image should parse"); - let inline_image = parsed - .operators - .first() - .and_then(|operator| match operator { - PdfOperatorVariant::InlineImage(image) => Some(image), - _ => None, - }) - .expect("expected inline image"); - - let mut backend = RecordingBackend::default(); - parsed - .operators - .first() - .expect("inline image operator") - .call(&mut backend) - .expect("dispatch should succeed"); - assert_eq!( - backend.operations, - vec![RecordedOperation::PaintInlineImage { - data: inline_image.shared_data(), - }] - ); + assert!(matches!(parsed.operators.as_slice(), [InlineImage(_)])); } #[test] @@ -262,10 +224,7 @@ mod tests { .read::(&ObjectVariant::Stream(stream_object(1, b"@ q"))) .expect("stream should parse"); - assert_eq!( - recorded_operations(&parsed.operators), - vec![RecordedOperation::SaveGraphicsState] - ); + assert!(matches!(parsed.operators.as_slice(), [SaveGraphicsState])); } #[test] @@ -290,10 +249,10 @@ mod tests { .expect("contents array should parse"); assert_eq!(parsed.id, 0); - assert_eq!( - recorded_operations(&parsed.operators), - vec![RecordedOperation::MoveTo { x: 1.0, y: 2.0 }] - ); + assert!(matches!( + parsed.operators.as_slice(), + [MoveTo { x: 1.0, y: 2.0 }] + )); } #[test] @@ -336,10 +295,10 @@ mod tests { .expect("malformed inline image should be skipped"); assert_eq!(parsed.id, 0); - assert_eq!( - recorded_operations(&parsed.operators), - vec![RecordedOperation::RestoreGraphicsState] - ); + assert!(matches!( + parsed.operators.as_slice(), + [RestoreGraphicsState] + )); assert_eq!( reader .content_stream_ids() @@ -394,13 +353,10 @@ mod tests { .expect("page should have a content stream"); assert_eq!(content_stream.id, 0); - assert_eq!( - recorded_operations(&content_stream.operators), - vec![ - RecordedOperation::SaveGraphicsState, - RecordedOperation::RestoreGraphicsState, - ] - ); + assert!(matches!( + content_stream.operators.as_slice(), + [SaveGraphicsState, RestoreGraphicsState] + )); let next = reader .read::(&ObjectVariant::Stream(stream_object(3, b"q"))) diff --git a/crates/pdf-content-stream/tests/parser_api.rs b/crates/pdf-content-stream/tests/parser_api.rs index 27d4dab1..262dc01a 100644 --- a/crates/pdf-content-stream/tests/parser_api.rs +++ b/crates/pdf-content-stream/tests/parser_api.rs @@ -4,8 +4,7 @@ use std::sync::Arc; use pdf_content_stream::ContentStream; use pdf_content_stream_operators::{ PdfTextItem, - recording_pdf_operator_backend::{RecordedOperation, RecordingBackend}, - variants::PdfOperatorVariant, + variants::PdfOperatorVariant::{self, *}, }; use pdf_object_reader::{ dictionary::Dictionary, object_error::ObjectError, object_resolver::ObjectResolver, @@ -34,16 +33,6 @@ fn stream_object(object_number: usize, data: &[u8]) -> StreamObject { ) } -fn recorded_operations(operators: &[PdfOperatorVariant]) -> Vec { - let mut backend = RecordingBackend::default(); - for operator in operators { - operator - .call(&mut backend) - .expect("operator should dispatch"); - } - backend.operations -} - struct MapResolver { objects: BTreeMap, } @@ -92,20 +81,14 @@ fn content_stream_read_returns_expected_operators_and_assigns_ids() { assert_eq!(parsed.id, 0); assert!(matches!( - parsed.operators.first(), - Some(PdfOperatorVariant::BeginCompatibility) - )); - assert!(matches!( - parsed.operators.get(1), - Some(PdfOperatorVariant::EndCompatibility) - )); - assert_eq!( - recorded_operations(&parsed.operators), - vec![ - RecordedOperation::MoveTo { x: 10.0, y: 20.0 }, - RecordedOperation::LineTo { x: 30.0, y: 40.0 }, + parsed.operators.as_slice(), + [ + BeginCompatibility, + EndCompatibility, + MoveTo { x: 10.0, y: 20.0 }, + LineTo { x: 30.0, y: 40.0 } ] - ); + )); } #[test] @@ -156,17 +139,13 @@ fn content_stream_preserves_non_utf8_font_name() { .read::(&ObjectVariant::Stream(stream_object(1, b"/#FF 12 Tf"))) .expect("non-UTF-8 resource names should parse"); - assert_eq!( - recorded_operations(&parsed.operators), - vec![RecordedOperation::SetFontAndSize { - font_name: vec![0xFF], - size: 12.0, - }] + assert!( + matches!(parsed.operators.as_slice(), [SetFont { name, size: 12.0 }] if **name == [0xFF]) ); } #[test] -fn parsed_inline_image_can_be_dispatched() { +fn content_stream_read_parses_inline_image() { let reader = pdf_object_reader::ObjectReader::new(&PassthroughResolver); let parsed = reader .read::(&ObjectVariant::Stream(stream_object( @@ -174,29 +153,8 @@ fn parsed_inline_image_can_be_dispatched() { b"BI /W 1 /H 1 /BPC 8 /CS /G ID \x00 EI", ))) .expect("inline image should parse"); - let inline_image = parsed - .operators - .first() - .and_then(|operator| match operator { - PdfOperatorVariant::InlineImage(image) => Some(image), - _ => None, - }) - .expect("expected inline image"); - - let mut backend = RecordingBackend::default(); - parsed - .operators - .first() - .expect("inline image operator") - .call(&mut backend) - .expect("dispatch should succeed"); - assert_eq!( - backend.operations, - vec![RecordedOperation::PaintInlineImage { - data: inline_image.shared_data(), - }] - ); + assert!(matches!(parsed.operators.as_slice(), [InlineImage(_)])); } #[test] @@ -206,10 +164,7 @@ fn content_stream_read_skips_unknown_operator_and_recovers() { .read::(&ObjectVariant::Stream(stream_object(1, b"@ q"))) .expect("stream should parse"); - assert_eq!( - recorded_operations(&parsed.operators), - vec![RecordedOperation::SaveGraphicsState] - ); + assert!(matches!(parsed.operators.as_slice(), [SaveGraphicsState])); } #[test] @@ -257,10 +212,10 @@ fn optional_contents_parses_stream_arrays_and_allocates_monotonically() { .expect("page should have a content stream"); assert_eq!(content_stream.id, 0); - assert_eq!( - recorded_operations(&content_stream.operators), - vec![RecordedOperation::MoveTo { x: 1.0, y: 2.0 }] - ); + assert!(matches!( + content_stream.operators.as_slice(), + [MoveTo { x: 1.0, y: 2.0 }] + )); let next = reader .read::(&ObjectVariant::Stream(stream_object(3, b"q"))) @@ -308,10 +263,10 @@ fn content_stream_read_skips_malformed_inline_image_and_consumes_an_id() { .expect("malformed inline image should be skipped"); assert_eq!(parsed.id, 0); - assert_eq!( - recorded_operations(&parsed.operators), - vec![RecordedOperation::RestoreGraphicsState] - ); + assert!(matches!( + parsed.operators.as_slice(), + [RestoreGraphicsState] + )); assert_eq!( reader .content_stream_ids() diff --git a/crates/pdf-filter/src/error.rs b/crates/pdf-filter/src/error.rs index eb99d78c..5adec23d 100644 --- a/crates/pdf-filter/src/error.rs +++ b/crates/pdf-filter/src/error.rs @@ -66,6 +66,15 @@ impl From for FilterError { } } +impl From for pdf_object_reader::ObjectReadError { + fn from(source: FilterError) -> Self { + Self::Decode { + target: "stream filter", + source: Box::new(source), + } + } +} + impl From for pdf_object_reader::object_error::ObjectError { fn from(err: FilterError) -> Self { match err { diff --git a/crates/pdf-filter/src/filter.rs b/crates/pdf-filter/src/filter.rs index a6357f0f..05fabacb 100644 --- a/crates/pdf-filter/src/filter.rs +++ b/crates/pdf-filter/src/filter.rs @@ -5,7 +5,7 @@ use bytes::Bytes; use crate::{error::FilterError, image_payload::ImagePayload, jpeg_frame::JpegFrameHeader}; use pdf_object_reader::{ - FromPdfObject, ObjectAccess, ObjectContext, ObjectReader, ReadResult, dictionary::Dictionary, + DictionaryContext, FromPdfObject, ObjectAccess, ObjectContext, ObjectReader, ReadResult, object_resolver::PassthroughResolver, object_variant::ObjectVariant, stream::StreamObject, }; @@ -213,33 +213,6 @@ impl Filter { } } -/// Decodes stream data by applying the filter chain from its dictionary. -/// -/// Reads the `/Filter` entry from `dictionary` and applies each filter in order. -/// Returns shared ownership of the fully decoded bytes. -/// Unfiltered input retains the original shared allocation. -/// -/// This entry point accepts a dictionary and shared data separately so callers -/// such as inline-image decoders do not need to construct a temporary -/// [`StreamObject`]. -/// -/// A `JPXDecode` chain yields no samples, because a JPEG 2000 codestream must -/// be decoded with its own metadata; such a stream belongs to -/// [`ImagePayload::decode`] instead. -/// -/// # Errors -/// -/// Returns [`FilterError`] if a filter fails, is unsupported, or is -/// `JPXDecode`, except when an adjacent duplicate filter fails after its first -/// pass succeeded. -pub fn decode_data_with_access( - dictionary: &Dictionary, - stream_data: Bytes, - access: &mut (impl ObjectAccess + ?Sized), -) -> Result { - ImagePayload::decode(dictionary, stream_data, access)?.into_samples() -} - /// Decodes a [`StreamObject`] by applying its full filter chain. /// /// Unfiltered data shares the stream's existing allocation. Filtered data is @@ -253,7 +226,11 @@ pub fn decode_with_access( stream: &StreamObject, access: &mut (impl ObjectAccess + ?Sized), ) -> Result { - decode_data_with_access(&stream.dictionary, stream.shared_data(), access) + ImagePayload::filtered( + stream.shared_data(), + &mut DictionaryContext::new(&stream.dictionary, access), + )? + .into_samples() } /// Decodes a [`StreamObject`] by applying its full filter chain. @@ -313,9 +290,8 @@ mod tests { let dictionary = Dictionary::new(BTreeMap::, ObjectVariant>::new()); let data = Bytes::from_static(b"stream data"); - let decoded = decode_data_with_access( - &dictionary, - data.clone(), + let decoded = decode_with_access( + &StreamObject::new_encoded(1, 0, dictionary, data.clone()), &mut ObjectReader::new(PassthroughResolver).session(), ) .expect("unfiltered data should decode"); @@ -335,9 +311,8 @@ mod tests { )])); let encoded = Bytes::from_static(b"48 65 6c 6c 6f>"); - let decoded = decode_data_with_access( - &dictionary, - encoded.clone(), + let decoded = decode_with_access( + &StreamObject::new_encoded(1, 0, dictionary, encoded.clone()), &mut ObjectReader::new(PassthroughResolver).session(), ) .expect("filtered data should decode"); @@ -628,9 +603,8 @@ mod tests { ), ])); - let decoded = decode_data_with_access( - &dictionary, - encoded.into_bytes().into(), + let decoded = decode_with_access( + &StreamObject::new_encoded(1, 0, dictionary, encoded.into_bytes()), &mut ObjectReader::new(PassthroughResolver).session(), ) .expect("decode failed"); @@ -671,9 +645,8 @@ mod tests { )), )]), }; - let decoded = decode_data_with_access( - &dictionary, - compressed.into(), + let decoded = decode_with_access( + &StreamObject::new_encoded(1, 0, dictionary, compressed), &mut ObjectReader::new(&resolver).session(), ) .expect("decode failed"); diff --git a/crates/pdf-filter/src/image_payload.rs b/crates/pdf-filter/src/image_payload.rs index 576b03f3..f2651757 100644 --- a/crates/pdf-filter/src/image_payload.rs +++ b/crates/pdf-filter/src/image_payload.rs @@ -10,8 +10,8 @@ use bytes::Bytes; use pdf_ccitt::CCITTFaxParams; use pdf_object_reader::{ DictionaryContext, FromPdfObject, ObjectAccess, ObjectContext, ObjectReadError, ReadResult, - dictionary::Dictionary, object_kind::ObjectKind, object_variant::ObjectVariant, - pdf_array::PdfArray, stream::StreamObject, + object_kind::ObjectKind, object_variant::ObjectVariant, pdf_array::PdfArray, + stream::StreamObject, }; use crate::{ @@ -30,8 +30,32 @@ pub enum ImagePayload { Jpx(Bytes), } +/// Reads an image stream's payload. +/// +/// A stream whose filter chain was already applied is only classified: a +/// `JPXDecode` stream is deliberately left encoded by the object collection, +/// because only the structured image path can read a JPEG 2000 codestream, yet +/// it still reports its filters as applied. +impl FromPdfObject for ImagePayload { + fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { + let mut context = context.stream()?; + let stream = context.stream(); + let mut context = context.dictionary(); + if !stream.filters_applied() { + return Ok(Self::filtered(stream.shared_data(), &mut context)?); + } + let jpx = context + .optional::(Filters::KEY)? + .is_some_and(|filters| filters.has_jpx_filter()); + Ok(match jpx { + true => Self::Jpx(stream.shared_data()), + false => Self::Samples(stream.shared_data()), + }) + } +} + impl ImagePayload { - /// Applies the `/Filter` chain of `dictionary` to `stream_data`. + /// Applies the `/Filter` chain of the context's dictionary to `stream_data`. /// /// Filters are applied in order until the chain ends or `JPXDecode` is /// reached. PDF requires `JPXDecode` to be the last filter of an image @@ -43,12 +67,10 @@ impl ImagePayload { /// Returns [`FilterError`] if a filter fails, is unsupported, or follows /// `JPXDecode`, except when an adjacent duplicate filter fails after its /// first pass succeeded. - pub fn decode( - dictionary: &Dictionary, + pub(crate) fn filtered( stream_data: Bytes, - access: &mut (impl ObjectAccess + ?Sized), + context: &mut DictionaryContext<'_, impl ObjectAccess + ?Sized>, ) -> Result { - let mut context = DictionaryContext::new(dictionary, access); let Some(filters) = context.optional::(Filters::KEY)? else { return Ok(Self::Samples(stream_data)); }; @@ -69,7 +91,7 @@ impl ImagePayload { if let Filter::JPXDecode = filter { codestream = Some(data.clone()); } else { - match filter.apply(data.as_ref(), &decode_parms, index, &mut context) { + match filter.apply(data.as_ref(), &decode_parms, index, context) { Ok(decoded) => data = decoded, // A repeated filter can be spurious when its first pass // already produced the binary data expected by the next filter. @@ -165,7 +187,7 @@ impl Filter { /// Applies this byte-to-byte filter, the `index`-th of its chain, to `data`. /// /// `context` reads the stream dictionary, whose image entries some filters - /// need. `JPXDecode` is not a byte filter; [`ImagePayload::decode`] handles + /// need. `JPXDecode` is not a byte filter; the [`ImagePayload`] decoder handles /// it before reaching this method, and it is rejected here. fn apply( &self, diff --git a/crates/pdf-graphics/src/mask_mode.rs b/crates/pdf-graphics/src/mask_mode.rs index d611549a..1bd1df0d 100644 --- a/crates/pdf-graphics/src/mask_mode.rs +++ b/crates/pdf-graphics/src/mask_mode.rs @@ -8,13 +8,3 @@ pub enum MaskMode { /// An unrecognized soft mask mode. Unknown(Vec), } - -impl From<&[u8]> for MaskMode { - fn from(value: &[u8]) -> Self { - match value { - b"Alpha" => Self::Alpha, - b"Luminosity" => Self::Luminosity, - other => Self::Unknown(Vec::from(other)), - } - } -} diff --git a/crates/pdf-image/src/decoded_samples.rs b/crates/pdf-image/src/decoded_samples.rs index 1467fb85..ad0c94c5 100644 --- a/crates/pdf-image/src/decoded_samples.rs +++ b/crates/pdf-image/src/decoded_samples.rs @@ -488,11 +488,9 @@ mod tests { (Vec::from(b"Height"), ObjectVariant::Integer(1)), (Vec::from(b"Width"), ObjectVariant::Integer(2)), ])); - let metadata = ImageMetadata::read(&mut pdf_object_reader::DictionaryContext::new( - &dictionary, - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - )) - .expect("direct image metadata should be valid"); + let metadata = pdf_object_reader::ObjectReader::new(PassthroughResolver) + .read::(&ObjectVariant::Dictionary(dictionary)) + .expect("direct image metadata should be valid"); let samples = Bytes::from_static(&[12, 34]); let decoded = DecodedSamples::decode_direct(samples.clone(), &metadata, 1) diff --git a/crates/pdf-image/src/error.rs b/crates/pdf-image/src/error.rs index 7236f535..a5c20a6a 100644 --- a/crates/pdf-image/src/error.rs +++ b/crates/pdf-image/src/error.rs @@ -3,6 +3,7 @@ use pdf_decode::DecodeError; use pdf_filter::error::FilterError; use pdf_graphics::transform::TransformError; use pdf_jpeg2000::Jpeg2000Error; +use pdf_object_reader::ObjectReadError; use thiserror::Error; /// Errors that can occur while parsing or decoding PDF image data. @@ -11,7 +12,7 @@ pub enum PdfImageError { #[error("{0}")] Object(#[from] pdf_object_reader::object_error::ObjectError), #[error("{0}")] - Read(#[from] pdf_object_reader::ObjectReadError), + Read(pdf_object_reader::ObjectReadError), #[error("{0}")] ColorSpace(#[from] ColorSpaceError), #[error("{0}")] @@ -28,8 +29,11 @@ pub enum PdfImageError { InvalidSoftMaskXObject, #[error("{0}")] Raster(#[from] ImageRasterError), - #[error("invalid image dimensions: width={width}, height={height}")] - InvalidImageDimensions { width: usize, height: usize }, + #[error("invalid image dimensions: width={width:?}, height={height:?}")] + InvalidImageDimensions { + width: Option, + height: Option, + }, #[error( "unsupported image BitsPerComponent value: {bits_per_component} (supported: 1, 2, 4, 8, 16)" )] @@ -64,6 +68,32 @@ impl PdfImageError { } } +/// Recovers an image or filter failure that crossed a typed read. +impl From for PdfImageError { + fn from(error: ObjectReadError) -> Self { + let ObjectReadError::Decode { target, source } = error else { + return Self::Read(error); + }; + let source = match source.downcast::() { + Ok(error) => return *error, + Err(source) => source, + }; + match source.downcast::() { + Ok(error) => Self::Filter(*error), + Err(source) => Self::Read(ObjectReadError::Decode { target, source }), + } + } +} + +impl From for ObjectReadError { + fn from(source: PdfImageError) -> Self { + Self::Decode { + target: "PDF image", + source: Box::new(source), + } + } +} + impl From for PdfImageError { fn from(value: DecodeError) -> Self { match value { diff --git a/crates/pdf-image/src/image_decoder.rs b/crates/pdf-image/src/image_decoder.rs index 666f2b5a..2513c640 100644 --- a/crates/pdf-image/src/image_decoder.rs +++ b/crates/pdf-image/src/image_decoder.rs @@ -1,77 +1,14 @@ use bytes::Bytes; -use pdf_filter::{filter::Filters, image_payload::ImagePayload}; +use pdf_filter::image_payload::ImagePayload; use pdf_graphics::Image; -use pdf_object_reader::{ - DictionaryContext, ObjectAccess, dictionary::Dictionary, stream::StreamObject, -}; -use crate::InlineImage; +use crate::PdfImage; use crate::decoded_samples::DecodedSamples; use crate::error::PdfImageError; use crate::image_metadata::ImageMetadata; use crate::raster; use crate::reduced_image::{BandDecode, PixelBudget, ReduceImage}; -/// Parses an Image XObject from a PDF stream dictionary and data. -pub fn read_xobject( - dictionary: &Dictionary, - stream_data: &StreamObject, - access: &mut (impl ObjectAccess + ?Sized), - soft_mask: Option<&Image>, -) -> Result { - let metadata = ImageMetadata::read(&mut DictionaryContext::new(dictionary, access))?; - let payload = if stream_data.filters_applied() { - image_payload(stream_data.shared_data(), &metadata) - } else { - ImagePayload::decode(dictionary, stream_data.shared_data(), access)? - }; - - decode_normalized_image_with_metadata(payload, soft_mask, &metadata) -} - -/// Classifies bytes whose filter chain was applied before this call. -/// -/// A `JPXDecode` stream is deliberately left encoded by the object collection, -/// because only the structured image path can read a JPEG 2000 codestream. Such -/// a stream still reports its filters as applied, so the chain decides. -fn image_payload(data: Bytes, metadata: &ImageMetadata) -> ImagePayload { - match metadata - .filters - .as_ref() - .is_some_and(Filters::has_jpx_filter) - { - true => ImagePayload::Jpx(data), - false => ImagePayload::Samples(data), - } -} - -/// Decodes an inline image, including its filter chain and normalized sample data. -pub fn decode_inline_image( - image: &InlineImage, - soft_mask: Option<&Image>, -) -> Result { - decode_normalized_image_with_metadata( - ImagePayload::Samples(image.shared_data()), - soft_mask, - image.metadata(), - ) -} - -/// Decodes a normalized image dictionary and shared raw bytes into a raster image. -/// -/// The dictionary must already use canonical image keys such as `Width`, -/// `Height`, `BitsPerComponent`, and `ColorSpace`. -pub fn decode_normalized_image( - dictionary: &Dictionary, - raw_data: Bytes, - access: &mut (impl ObjectAccess + ?Sized), - soft_mask: Option<&Image>, -) -> Result { - let metadata = ImageMetadata::read(&mut DictionaryContext::new(dictionary, access))?; - let payload = image_payload(raw_data, &metadata); - decode_normalized_image_with_metadata(payload, soft_mask, &metadata) -} - /// Limits on the pixels a decoded image is held at. /// /// Drawing never needs far more pixels than the device shows, and backends cannot @@ -82,31 +19,31 @@ const IMAGE_BUDGET: PixelBudget = PixelBudget { side: 1 << 14, }; -fn decode_normalized_image_with_metadata( - payload: ImagePayload, - soft_mask: Option<&Image>, - metadata: &ImageMetadata, -) -> Result { - // A non-zero /SMaskInData puts the image's own opacity in charge, and PDF - // forbids an /SMask beside it, so a stray one is dropped rather than - // multiplied into alpha that already describes the image. - let soft_mask = soft_mask.filter(|_| !metadata.smask_in_data.overrides_soft_mask()); - let size = (metadata.size.width(), metadata.size.height()); - let factor = IMAGE_BUDGET.reduction_factor(size.0, size.1); - if factor > 1 { - let image = decode_reduced_image(payload, metadata, factor)?; - return match soft_mask { - Some(mask) => apply_resized_soft_mask(&image, mask), - None => Ok(image), - }; - } - let decoded_samples = DecodedSamples::decode(payload, metadata)?; - match soft_mask { - Some(mask) if (mask.width, mask.height) != size => { - let image = render_ready_image(decoded_samples, None, metadata); - apply_resized_soft_mask(&image, mask) +impl PdfImage { + /// Decodes the image into a raster, applying `soft_mask` as its opacity. + pub fn decode(&self, soft_mask: Option<&Image>) -> Result { + let (payload, metadata) = (self.payload.clone(), &self.metadata); + // A non-zero /SMaskInData puts the image's own opacity in charge, and PDF + // forbids an /SMask beside it, so a stray one is dropped rather than + // multiplied into alpha that already describes the image. + let soft_mask = soft_mask.filter(|_| !metadata.smask_in_data.overrides_soft_mask()); + let size = (metadata.size.width(), metadata.size.height()); + let factor = IMAGE_BUDGET.reduction_factor(size.0, size.1); + if factor > 1 { + let image = decode_reduced_image(payload, metadata, factor)?; + return match soft_mask { + Some(mask) => apply_resized_soft_mask(&image, mask), + None => Ok(image), + }; + } + let decoded_samples = DecodedSamples::decode(payload, metadata)?; + match soft_mask { + Some(mask) if (mask.width, mask.height) != size => { + let image = render_ready_image(decoded_samples, None, metadata); + apply_resized_soft_mask(&image, mask) + } + _ => Ok(render_ready_image(decoded_samples, soft_mask, metadata)), } - _ => Ok(render_ready_image(decoded_samples, soft_mask, metadata)), } } @@ -286,8 +223,14 @@ mod tests { use pdf_graphics::{Image, PixelFormat}; - use super::{InlineImage, decode_inline_image, decode_normalized_image, read_xobject}; - use crate::error::PdfImageError; + use crate::{PdfImage, error::PdfImageError}; + + /// Reads an image XObject stream and decodes it. + fn decode(stream: StreamObject, soft_mask: Option<&Image>) -> Result { + pdf_object_reader::ObjectReader::new(PassthroughResolver) + .read::(&ObjectVariant::Stream(stream))? + .decode(soft_mask) + } fn name(value: &str) -> ObjectVariant { pdf_object_reader::pdf_string::PdfString::from( @@ -379,13 +322,8 @@ mod tests { let dictionary = filtered_gray_dictionary(); let stream = StreamObject::new(1, 0, dictionary.clone(), vec![0x2A]); - let image = read_xobject( - &dictionary, - &stream, - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - None, - ) - .expect("predecoded image data should not be filtered again"); + let image = decode(stream.clone(), None) + .expect("predecoded image data should not be filtered again"); assert_eq!(image.data.as_ptr(), stream.data.as_ptr()); assert_eq!(image.data.as_ref(), &[0x2A]); @@ -396,13 +334,8 @@ mod tests { let dictionary = filtered_gray_dictionary(); let stream = StreamObject::new_encoded(1, 0, dictionary.clone(), b"2A>".to_vec()); - let image = read_xobject( - &dictionary, - &stream, - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - None, - ) - .expect("encoded image data should have its filter applied"); + let image = decode(stream.clone(), None) + .expect("encoded image data should have its filter applied"); assert_eq!(image.data.as_ref(), &[0x2A]); } @@ -428,10 +361,8 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(4)), ])); - let image = decode_normalized_image( - &dictionary, - vec![0b1010_0000].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), vec![0b1010_0000]), None, ) .expect("1-bpc decoded grayscale image should decode"); @@ -461,10 +392,8 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(2)), ])); - let image = decode_normalized_image( - &dictionary, - vec![0, 255].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), vec![0, 255]), None, ) .expect("8-bpc decoded grayscale image should decode"); @@ -508,10 +437,8 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(2)), ])); - let image = decode_normalized_image( - &dictionary, - vec![0b1000_0000].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), vec![0b1000_0000]), None, ) .expect("decoded indexed image should decode"); @@ -543,13 +470,8 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(1)), ])); - let image = decode_normalized_image( - &dictionary, - vec![0].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - None, - ) - .expect("Indexed DeviceN image should convert through its alternate space"); + let image = decode(StreamObject::new(1, 0, dictionary.clone(), vec![0]), None) + .expect("Indexed DeviceN image should convert through its alternate space"); assert_eq!(image.pixel_format, PixelFormat::RGBA8888); assert_eq!(image.data.as_ref(), &[255, 255, 255, 255]); @@ -564,10 +486,8 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(1)), ])); - let image = decode_normalized_image( - &dictionary, - vec![0, 255, 255, 255].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), vec![0, 255, 255, 255]), None, ) .expect("DeviceN image should convert through its tint transform"); @@ -598,13 +518,8 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(1)), ])); - let image = decode_normalized_image( - &dictionary, - vec![0].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - None, - ) - .expect("Indexed DeviceCMYK image should retain its existing conversion"); + let image = decode(StreamObject::new(1, 0, dictionary.clone(), vec![0]), None) + .expect("Indexed DeviceCMYK image should retain its existing conversion"); assert_eq!(image.data.as_ref(), &[255, 255, 255, 255]); } @@ -658,10 +573,8 @@ mod tests { interpolate: false, }; - let image = decode_normalized_image( - &dictionary, - vec![255].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), vec![255]), Some(&soft_mask), ) .expect("Separation alpha should survive soft-mask application"); @@ -688,13 +601,8 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(1)), ])); - let err = decode_normalized_image( - &dictionary, - vec![0].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - None, - ) - .expect_err("invalid /Decode length should fail"); + let err = decode(StreamObject::new(1, 0, dictionary.clone(), vec![0]), None) + .expect_err("invalid /Decode length should fail"); assert!(matches!( err, @@ -721,10 +629,8 @@ mod tests { ])); let samples = Bytes::from_static(&[12, 34]); - let image = decode_normalized_image( - &dictionary, - samples.clone(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), samples.clone()), None, ) .expect("grayscale image without /Decode should decode"); @@ -750,10 +656,8 @@ mod tests { ])); let samples = Bytes::from_static(&[12, 34, 56]); - let image = decode_normalized_image( - &dictionary, - samples.clone(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), samples.clone()), None, ) .expect("trailing samples should be ignored"); @@ -770,10 +674,8 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(4)), ])); - let image = decode_normalized_image( - &dictionary, - vec![0b1010_0000].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), vec![0b1010_0000]), None, ) .expect("image masks should default missing BitsPerComponent to 1"); @@ -796,13 +698,8 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(1)), ])); - let err = decode_normalized_image( - &dictionary, - vec![0].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - None, - ) - .expect_err("non-mask images should still require BitsPerComponent"); + let err = decode(StreamObject::new(1, 0, dictionary.clone(), vec![0]), None) + .expect_err("non-mask images should still require BitsPerComponent"); assert!(matches!( err, @@ -832,10 +729,8 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(2)), ])); - let image = decode_normalized_image( - &dictionary, - vec![10, 20, 30, 40, 50, 60].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), vec![10, 20, 30, 40, 50, 60]), None, ) .expect("DCT-decoded RGB bytes should not be validated as CMYK samples"); @@ -859,10 +754,8 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(2)), ])); - let err = decode_normalized_image( - &dictionary, - vec![10, 20, 30, 40, 50, 60].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let err = decode( + StreamObject::new(1, 0, dictionary.clone(), vec![10, 20, 30, 40, 50, 60]), None, ) .expect_err("non-DCT CMYK image data should still require four components"); @@ -898,10 +791,8 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(2)), ])); - let image = decode_normalized_image( - &dictionary, - vec![0xAA, 0x10, 0x20].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), vec![0xAA, 0x10, 0x20]), None, ) .expect("single decoded DCT pixel should expand to the declared image size"); @@ -935,10 +826,8 @@ mod tests { interpolate: false, }; - let image = decode_normalized_image( - &dictionary, - vec![0x20, 0xC0].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), vec![0x20, 0xC0]), Some(&soft_mask), ) .expect("resolved soft mask should be applied"); @@ -952,7 +841,7 @@ mod tests { #[test] fn decode_inline_image_applies_filter_chain_before_samples() { - let image = InlineImage::new( + let image = PdfImage::inline( Dictionary::new(BTreeMap::from([ (Vec::from(b"BPC"), ObjectVariant::Integer(8)), ( @@ -977,7 +866,7 @@ mod tests { ) .expect("inline image filters should decode"); - let decoded = decode_inline_image(&image, None).expect("inline image should decode"); + let decoded = image.decode(None).expect("inline image should decode"); assert_eq!(decoded.pixel_format, pdf_graphics::PixelFormat::Gray8); assert_eq!(decoded.data.as_ref(), &[0x2A]); @@ -985,7 +874,7 @@ mod tests { #[test] fn decode_inline_image_accepts_abbreviated_gray_color_space() { - let image = InlineImage::new( + let image = PdfImage::inline( Dictionary::new(BTreeMap::from([ (Vec::from(b"BPC"), ObjectVariant::Integer(1)), ( @@ -1003,7 +892,8 @@ mod tests { ) .expect("inline image should be constructed"); - let decoded = decode_inline_image(&image, None) + let decoded = image + .decode(None) .expect("inline image with abbreviated gray color space should decode"); assert_eq!(decoded.pixel_format, pdf_graphics::PixelFormat::Gray8); @@ -1012,7 +902,7 @@ mod tests { #[test] fn decode_inline_image_preserves_unfiltered_shared_samples() { - let image = InlineImage::new( + let image = PdfImage::inline( Dictionary::new(BTreeMap::from([ (Vec::from(b"BPC"), ObjectVariant::Integer(8)), ( @@ -1031,8 +921,9 @@ mod tests { .expect("inline image should be constructed"); let samples = image.shared_data(); - let decoded = - decode_inline_image(&image, None).expect("unfiltered inline image should decode"); + let decoded = image + .decode(None) + .expect("unfiltered inline image should decode"); assert_eq!(decoded.data.as_ptr(), samples.as_ptr()); assert_eq!(decoded.data.as_ref(), &[12, 34]); @@ -1040,7 +931,7 @@ mod tests { #[test] fn decode_inline_image_accepts_abbreviated_indexed_color_space() { - let image = InlineImage::new( + let image = PdfImage::inline( Dictionary::new(BTreeMap::from([ (Vec::from(b"BPC"), ObjectVariant::Integer(1)), ( @@ -1072,7 +963,8 @@ mod tests { ) .expect("inline image should be constructed"); - let decoded = decode_inline_image(&image, None) + let decoded = image + .decode(None) .expect("inline image with abbreviated indexed color space should decode"); assert_eq!(decoded.pixel_format, pdf_graphics::PixelFormat::RGBA8888); @@ -1099,10 +991,8 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(8)), ])); - let image = decode_normalized_image( - &dictionary, - vec![0b1011_0010].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), vec![0b1011_0010]), None, ) .expect("1-bpc image should decode"); @@ -1117,10 +1007,8 @@ mod tests { #[test] fn decode_normalized_indexed_image_1bpc_expands_samples() { let dictionary = indexed_dictionary(1); - let image = decode_normalized_image( - &dictionary, - vec![0b1010_0000].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), vec![0b1010_0000]), None, ) .expect("1-bpc indexed image should decode"); @@ -1137,10 +1025,8 @@ mod tests { #[test] fn decode_normalized_indexed_image_2bpc_expands_samples() { let dictionary = indexed_dictionary(2); - let image = decode_normalized_image( - &dictionary, - vec![0x1B].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), vec![0x1B]), None, ) .expect("2-bpc indexed image should decode"); @@ -1157,10 +1043,8 @@ mod tests { #[test] fn decode_normalized_indexed_image_4bpc_expands_samples() { let dictionary = indexed_dictionary(4); - let image = decode_normalized_image( - &dictionary, - vec![0x01, 0x23].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), vec![0x01, 0x23]), None, ) .expect("4-bpc indexed image should decode"); @@ -1177,10 +1061,8 @@ mod tests { #[test] fn decode_normalized_indexed_image_8bpc_continues_to_work() { let dictionary = indexed_dictionary(8); - let image = decode_normalized_image( - &dictionary, - vec![0, 1, 2, 3].into(), - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), + let image = decode( + StreamObject::new(1, 0, dictionary.clone(), vec![0, 1, 2, 3]), None, ) .expect("8-bpc indexed image should decode"); diff --git a/crates/pdf-image/src/image_metadata.rs b/crates/pdf-image/src/image_metadata.rs index 7926eb2d..cbada414 100644 --- a/crates/pdf-image/src/image_metadata.rs +++ b/crates/pdf-image/src/image_metadata.rs @@ -3,8 +3,7 @@ use pdf_decode::DecodeMap; use pdf_filter::filter::Filters; use pdf_graphics::rect::Rect; use pdf_object_reader::{ - DictionaryContext, ObjectAccess, ReadResult, object_error::ObjectError, - object_variant::ObjectVariant, + FromPdfObject, ObjectAccess, ObjectContext, ReadResult, object_error::ObjectError, }; use crate::{error::PdfImageError, smask_in_data::SMaskInData}; @@ -33,143 +32,89 @@ pub(crate) struct ImageMetadata { pub(crate) interpolate: bool, } -impl ImageMetadata { - /// Default bit depth for an image mask when `/BitsPerComponent` is omitted. - const DEFAULT_IMAGE_MASK_BITS_PER_COMPONENT: usize = 1; - - /// Default bit depth for a JPX image when `/BitsPerComponent` is omitted. - const DEFAULT_JPX_BITS_PER_COMPONENT: usize = 8; - - /// Reads and validates normalized image metadata from an image dictionary. - pub(crate) fn read( - context: &mut DictionaryContext<'_, impl ObjectAccess + ?Sized>, - ) -> Result { - Self::try_from(ImageEntries::read(context)?) - } - - /// Returns the bit depth of the samples handed to the display pipeline. - /// - /// Sixteen-bit samples are narrowed to their most significant byte, so they - /// are carried as eight-bit samples. - pub(crate) fn sample_bits(&self) -> usize { - self.bits_per_component.min(8) - } -} - -impl TryFrom for ImageMetadata { - type Error = PdfImageError; - - /// Applies the defaults PDF allows and rejects values the decoders cannot handle. - fn try_from(entries: ImageEntries) -> Result { - let missing_key = |key: &str| ObjectError::MissingRequiredKey { - key: key.to_string(), +/// Reads an image dictionary, applying the defaults PDF allows and rejecting values +/// the decoders cannot handle; null entries count as absent. +/// +/// `/ColorSpace` is decoded only for an image that is not a mask, because a mask's +/// samples carry no color and its `/ColorSpace` is never used. +impl FromPdfObject for ImageMetadata { + fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { + let mut context = context.dictionary()?; + let size = match ( + context.lenient::(b"Width"), + context.lenient::(b"Height"), + ) { + (Some(width), Some(height)) if width > 0 && height > 0 => { + Rect::::from_size(width, height) + } + (width, height) => { + return Err(PdfImageError::InvalidImageDimensions { width, height }.into()); + } + }; + let image_mask = context.optional(b"ImageMask")?.unwrap_or_default(); + let filters = context.optional::(Filters::KEY)?; + let is_jpx = filters.as_ref().is_some_and(Filters::has_jpx_filter); + let color_space = match image_mask { + true => None, + false => context.optional::(b"ColorSpace")?, }; - let width = entries.width.ok_or_else(|| missing_key("Width"))?; - let height = entries.height.ok_or_else(|| missing_key("Height"))?; - if width == 0 || height == 0 { - return Err(PdfImageError::InvalidImageDimensions { width, height }); - } - let bits_per_component = entries - .bits_per_component - .or_else(|| entries.default_bits_per_component()) - .ok_or_else(|| missing_key("BitsPerComponent"))?; + // Image masks and JPX images may omit /BitsPerComponent, with different + // defaults. Every other image must provide it. + let bits_per_component = match context.optional(b"BitsPerComponent")? { + Some(bits_per_component) => bits_per_component, + None if image_mask => Self::DEFAULT_IMAGE_MASK_BITS_PER_COMPONENT, + None if is_jpx => Self::DEFAULT_JPX_BITS_PER_COMPONENT, + None => { + return Err(PdfImageError::Object(ObjectError::MissingRequiredKey { + key: "BitsPerComponent".to_string(), + }) + .into()); + } + }; // PDF ignores /BitsPerComponent for a JPX image: the codestream states // the precision of every component, so the entry constrains nothing. - if !entries.is_jpx() { - validate_bits_per_component( - bits_per_component, - entries.image_mask, - entries.color_space.as_ref(), - )?; + if !is_jpx { + validate_bits_per_component(bits_per_component, image_mask, color_space.as_ref())?; } - let smask_in_data = entries - .smask_in_data + let smask_in_data = context + .optional::(b"SMaskInData")? .map_or_else(|| Ok(SMaskInData::default()), SMaskInData::try_from)?; - let num_color_components = entries - .color_space + let num_color_components = color_space .as_ref() .map_or(1, ColorSpace::num_color_components); - let decode = entries - .decode + let decode = context + .optional::>(b"Decode")? .map(|values| DecodeMap::new(&values, num_color_components)) - .transpose()?; + .transpose() + .map_err(PdfImageError::from)?; Ok(Self { - size: Rect::::from_size(width, height), + size, bits_per_component, - color_space: entries.color_space, - image_mask: entries.image_mask, - filters: entries.filters, + color_space, + image_mask, + filters, decode, smask_in_data, - interpolate: entries.interpolate, + interpolate: context.optional(b"Interpolate")?.unwrap_or_default(), }) } } -/// The entries of an image dictionary, read in one pass before validation. -#[derive(Default)] -struct ImageEntries { - width: Option, - height: Option, - bits_per_component: Option, - image_mask: bool, - color_space: Option, - filters: Option, - decode: Option>, - smask_in_data: Option, - interpolate: bool, -} - -impl ImageEntries { - /// Reads every image entry of the context's dictionary; null entries count as absent. - /// - /// `/ColorSpace` is decoded only for an image that is not a mask, because a - /// mask's samples carry no color and its `/ColorSpace` is never used. - fn read(context: &mut DictionaryContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - let mut entries = Self::default(); - let mut color_space: Option<&ObjectVariant> = None; - for (key, value) in context.dictionary().iter() { - if context.is_absent(value)? { - continue; - } - match key { - b"Width" => entries.width = Some(context.read(value)?), - b"Height" => entries.height = Some(context.read(value)?), - b"BitsPerComponent" => entries.bits_per_component = Some(context.read(value)?), - b"ImageMask" => entries.image_mask = context.read(value)?, - b"ColorSpace" => color_space = Some(value), - b"Filter" => entries.filters = Some(context.read(value)?), - b"Decode" => entries.decode = Some(context.read(value)?), - b"SMaskInData" => entries.smask_in_data = Some(context.read(value)?), - b"Interpolate" => entries.interpolate = context.read(value)?, - _ => {} - } - } - if !entries.image_mask { - entries.color_space = color_space.map(|value| context.read(value)).transpose()?; - } - Ok(entries) - } +impl ImageMetadata { + /// Default bit depth for an image mask when `/BitsPerComponent` is omitted. + const DEFAULT_IMAGE_MASK_BITS_PER_COMPONENT: usize = 1; - /// Returns whether the filter chain leaves a JPEG 2000 codestream. - fn is_jpx(&self) -> bool { - self.filters.as_ref().is_some_and(Filters::has_jpx_filter) - } + /// Default bit depth for a JPX image when `/BitsPerComponent` is omitted. + const DEFAULT_JPX_BITS_PER_COMPONENT: usize = 8; - /// Returns the bit depth used when `/BitsPerComponent` is omitted. + /// Returns the bit depth of the samples handed to the display pipeline. /// - /// Image masks and JPX images may omit the entry, with different defaults. - /// Every other image must provide it. - fn default_bits_per_component(&self) -> Option { - if self.image_mask { - Some(ImageMetadata::DEFAULT_IMAGE_MASK_BITS_PER_COMPONENT) - } else if self.is_jpx() { - Some(ImageMetadata::DEFAULT_JPX_BITS_PER_COMPONENT) - } else { - None - } + /// Sixteen-bit samples are narrowed to their most significant byte, so they + /// are carried as eight-bit samples. + pub(crate) fn sample_bits(&self) -> usize { + self.bits_per_component.min(8) } } @@ -206,28 +151,30 @@ mod tests { use pdf_filter::filter::{Filter, Filters}; use pdf_object_reader::{ - DictionaryContext, dictionary::Dictionary, object_error::ObjectError, - object_resolver::PassthroughResolver, object_variant::ObjectVariant, + dictionary::Dictionary, object_error::ObjectError, object_resolver::PassthroughResolver, + object_variant::ObjectVariant, }; use super::ImageMetadata; use crate::error::PdfImageError; + fn read(dictionary: &Dictionary) -> Result { + pdf_object_reader::ObjectReader::new(PassthroughResolver) + .read(&ObjectVariant::Dictionary(dictionary.clone())) + .map_err(PdfImageError::from) + } + #[test] fn rejects_zero_dimensions() { let dictionary = direct_dictionary(0, 1, 8); - let error = ImageMetadata::read(&mut DictionaryContext::new( - &dictionary, - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - )) - .expect_err("zero width should be rejected"); + let error = read(&dictionary).expect_err("zero width should be rejected"); assert!(matches!( error, PdfImageError::InvalidImageDimensions { - width: 0, - height: 1 + width: Some(0), + height: Some(1) } )); } @@ -240,11 +187,7 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(1)), ])); - let metadata = ImageMetadata::read(&mut DictionaryContext::new( - &dictionary, - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - )) - .expect("image mask metadata should be valid"); + let metadata = read(&dictionary).expect("image mask metadata should be valid"); assert_eq!(metadata.bits_per_component, 1); assert!(metadata.color_space.is_none()); @@ -261,11 +204,7 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(1)), ])); - let error = ImageMetadata::read(&mut DictionaryContext::new( - &dictionary, - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - )) - .expect_err("8-bpc image mask should be rejected"); + let error = read(&dictionary).expect_err("8-bpc image mask should be rejected"); assert!(matches!( error, @@ -290,11 +229,7 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(1)), ])); - let metadata = ImageMetadata::read(&mut DictionaryContext::new( - &dictionary, - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - )) - .expect("image mask filter should parse"); + let metadata = read(&dictionary).expect("image mask filter should parse"); assert_eq!( metadata.filters, @@ -306,11 +241,7 @@ mod tests { fn indexed_images_accept_two_bit_components() { let dictionary = indexed_dictionary(2); - let metadata = ImageMetadata::read(&mut DictionaryContext::new( - &dictionary, - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - )) - .expect("2-bpc indexed metadata should be valid"); + let metadata = read(&dictionary).expect("2-bpc indexed metadata should be valid"); assert_eq!(metadata.bits_per_component, 2); } @@ -319,11 +250,7 @@ mod tests { fn indexed_images_reject_unsupported_bit_components() { let dictionary = indexed_dictionary(3); - let error = ImageMetadata::read(&mut DictionaryContext::new( - &dictionary, - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - )) - .expect_err("3-bpc indexed metadata should be rejected"); + let error = read(&dictionary).expect_err("3-bpc indexed metadata should be rejected"); assert!(matches!( error, @@ -337,11 +264,7 @@ mod tests { fn direct_images_reject_unsupported_bit_components() { let dictionary = direct_dictionary(1, 1, 3); - let error = ImageMetadata::read(&mut DictionaryContext::new( - &dictionary, - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - )) - .expect_err("3-bpc direct metadata should be rejected"); + let error = read(&dictionary).expect_err("3-bpc direct metadata should be rejected"); assert!(matches!( error, @@ -365,11 +288,8 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(1)), ])); - let metadata = ImageMetadata::read(&mut DictionaryContext::new( - &dictionary, - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - )) - .expect("JPX metadata should default missing BitsPerComponent"); + let metadata = + read(&dictionary).expect("JPX metadata should default missing BitsPerComponent"); assert_eq!(metadata.bits_per_component, 8); assert_eq!( @@ -398,11 +318,7 @@ mod tests { ), ); - let metadata = ImageMetadata::read(&mut DictionaryContext::new( - &dictionary, - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - )) - .expect("ordered filter chain should parse"); + let metadata = read(&dictionary).expect("ordered filter chain should parse"); assert_eq!( metadata.filters, @@ -420,11 +336,8 @@ mod tests { (Vec::from(b"Width"), ObjectVariant::Integer(1)), ])); - let error = ImageMetadata::read(&mut DictionaryContext::new( - &dictionary, - &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), - )) - .expect_err("non-JPX metadata should require BitsPerComponent"); + let error = + read(&dictionary).expect_err("non-JPX metadata should require BitsPerComponent"); assert!(matches!( error, diff --git a/crates/pdf-image/src/lib.rs b/crates/pdf-image/src/lib.rs index 11fe5f3e..e28ea901 100644 --- a/crates/pdf-image/src/lib.rs +++ b/crates/pdf-image/src/lib.rs @@ -1,13 +1,12 @@ mod decoded_samples; pub mod error; -pub mod image_decoder; +mod image_decoder; mod image_metadata; -pub mod inline_image; mod jpx_image; +pub mod pdf_image; pub mod raster; mod reduced_image; mod smask_in_data; pub use error::{ImageRasterError, PdfImageError}; -pub use image_decoder::{decode_inline_image, decode_normalized_image, read_xobject}; -pub use inline_image::InlineImage; +pub use pdf_image::PdfImage; diff --git a/crates/pdf-image/src/inline_image.rs b/crates/pdf-image/src/pdf_image.rs similarity index 80% rename from crates/pdf-image/src/inline_image.rs rename to crates/pdf-image/src/pdf_image.rs index 62b478cb..38cfd3f5 100644 --- a/crates/pdf-image/src/inline_image.rs +++ b/crates/pdf-image/src/pdf_image.rs @@ -1,52 +1,74 @@ use std::collections::BTreeMap; use bytes::Bytes; -use pdf_filter::filter::{Filters, decode_data_with_access}; +use pdf_filter::image_payload::ImagePayload; use pdf_object_reader::{ - DictionaryContext, ObjectAccess, dictionary::Dictionary, object_variant::ObjectVariant, + FromPdfObject, ObjectAccess, ObjectContext, ObjectReadError, ReadResult, + dictionary::Dictionary, object_variant::ObjectVariant, stream::StreamObject, }; use crate::{error::PdfImageError, image_metadata::ImageMetadata}; -/// Canonical parsed representation of a PDF inline image. +/// An image stream's metadata and filtered payload, ready to decode into a raster. +/// +/// Image XObjects are read from their stream; inline images from their dictionary +/// and data, see [`PdfImage::inline`]. #[derive(Debug)] -pub struct InlineImage { - metadata: ImageMetadata, - data: Bytes, +pub struct PdfImage { + pub(crate) metadata: ImageMetadata, + pub(crate) payload: ImagePayload, } -impl InlineImage { - /// Creates an inline image from its parsed dictionary and encoded payload bytes. - pub fn new( +impl FromPdfObject for PdfImage { + fn from_pdf_object( + mut context: ObjectContext<'_, impl ObjectAccess + ?Sized>, + ) -> ReadResult { + let object = context.object(); + Ok(Self { + metadata: context.read(object)?, + payload: context.read(object)?, + }) + } +} + +impl PdfImage { + /// Reads an inline image from its parsed dictionary and encoded payload bytes. + /// + /// ISO 32000 restricts inline images to the filters that need no out-of-band + /// metadata; JPXDecode is not among them. The filter chain stops at JPXDecode, + /// so such an image is rejected without decoding its codestream. + pub fn inline( dictionary: Dictionary, data: impl Into, access: &mut (impl ObjectAccess + ?Sized), ) -> Result { let dictionary = normalize_inline_image_dictionary(&dictionary); - let metadata = ImageMetadata::read(&mut DictionaryContext::new(&dictionary, access))?; - // ISO 32000 restricts inline images to the filters that need no - // out-of-band metadata; JPXDecode is not among them. Reject it before - // filtering so the payload is never handed to a decoder that cannot - // report the image's own components and colour. - if metadata - .filters - .as_ref() - .is_some_and(Filters::has_jpx_filter) - { - return Err(PdfImageError::InlineJpxFilter); + let stream = StreamObject::new_encoded(0, 0, dictionary, data); + let image = access.read::(&ObjectVariant::Stream(stream))?; + match image.payload { + ImagePayload::Jpx(_) => Err(PdfImageError::InlineJpxFilter), + ImagePayload::Samples(_) => Ok(image), } - let data = decode_data_with_access(&dictionary, data.into(), access)?; - - Ok(Self { metadata, data }) } - /// Returns shared ownership of the filter-decoded inline-image samples. - pub fn shared_data(&self) -> Bytes { - self.data.clone() + /// Returns whether a failed read leaves the image without samples to draw: its + /// dimensions are missing or invalid, or its filter chain failed. + pub fn is_unreadable(error: &ObjectReadError) -> bool { + let ObjectReadError::Decode { source, .. } = error else { + return false; + }; + source.is::() + || matches!( + source.downcast_ref::(), + Some(PdfImageError::InvalidImageDimensions { .. }) + ) } - pub(crate) fn metadata(&self) -> &ImageMetadata { - &self.metadata + /// Returns shared ownership of the filtered payload bytes. + pub fn shared_data(&self) -> Bytes { + match &self.payload { + ImagePayload::Samples(data) | ImagePayload::Jpx(data) => data.clone(), + } } } @@ -120,7 +142,7 @@ mod tests { dictionary::Dictionary, object_resolver::PassthroughResolver, object_variant::ObjectVariant, }; - use super::{InlineImage, normalize_inline_image_dictionary}; + use super::{PdfImage, normalize_inline_image_dictionary}; #[test] fn normalize_inline_image_dictionary_expands_abbreviations() { @@ -260,7 +282,7 @@ mod tests { #[test] fn inline_image_shares_unfiltered_samples() { let data = Bytes::from_static(&[1, 2]); - let image = InlineImage::new( + let image = PdfImage::inline( gray_dictionary(), data.clone(), &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), @@ -281,7 +303,7 @@ mod tests { ), ); - let image = InlineImage::new( + let image = PdfImage::inline( dictionary, b"2A>".to_vec(), &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), @@ -293,14 +315,17 @@ mod tests { #[test] fn invalid_metadata_is_rejected_during_construction() { - let error = InlineImage::new( + let error = PdfImage::inline( Dictionary::new(BTreeMap::, ObjectVariant>::new()), vec![1], &mut pdf_object_reader::ObjectReader::new(PassthroughResolver).session(), ) .expect_err("invalid metadata should prevent construction"); - assert!(matches!(error, crate::error::PdfImageError::Object(_))); + assert!(matches!( + error, + crate::error::PdfImageError::InvalidImageDimensions { .. } + )); } fn gray_dictionary() -> Dictionary { diff --git a/crates/pdf-object-reader/src/decode.rs b/crates/pdf-object-reader/src/decode.rs index 3e27c242..617ca43f 100644 --- a/crates/pdf-object-reader/src/decode.rs +++ b/crates/pdf-object-reader/src/decode.rs @@ -1,7 +1,7 @@ //! Extensible typed decoding with one object-bound context per invocation. use num_traits::FromPrimitive; -use pdf_graphics::{LineCap, LineJoin, TextRenderingMode}; +use pdf_graphics::{LineCap, LineJoin, MaskMode, TextRenderingMode}; use crate::context::ObjectContext; use crate::error::{ObjectReadError, ReadResult}; @@ -14,7 +14,7 @@ use crate::pdf_string::PdfString; use crate::reader::ObjectAccess; use crate::resolved_object::ResolvedObject; use crate::{dictionary::Dictionary, stream::StreamObject}; -use pdf_graphics::interval::Interval; +use pdf_graphics::{interval::Interval, rect::Rect, transform::Transform}; /// Decodes a resolved PDF value using one object-bound context. /// @@ -260,6 +260,35 @@ impl FromPdfObject for Interval { } } +/// Reads a rectangle `[x1 y1 x2 y2]` in its original coordinate order. +/// +/// Trailing elements are ignored, like the `[T; N]` decoder. +impl FromPdfObject for Rect { + fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { + Ok(Self::from(context.array()?.read_array::()?)) + } +} + +/// Reads an affine matrix `[a b c d e f]`. +/// +/// Trailing elements are ignored, like the `[T; N]` decoder. +impl FromPdfObject for Transform { + fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { + Ok(Self::from(context.array()?.read_array::()?)) + } +} + +/// Reads a soft mask's `/S` subtype. Unrecognized names are kept as `Unknown`. +impl FromPdfObject for MaskMode { + fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { + Ok(match context.object().try_bytes(context.source())? { + b"Alpha" => Self::Alpha, + b"Luminosity" => Self::Luminosity, + other => Self::Unknown(Vec::from(other)), + }) + } +} + /// Reads the bytes of a name or string, accepting producer substitutions between them. /// /// Use `PdfName` or `PdfString` when the PDF object kind must be enforced. diff --git a/crates/pdf-object-reader/src/error.rs b/crates/pdf-object-reader/src/error.rs index a7334acb..19785878 100644 --- a/crates/pdf-object-reader/src/error.rs +++ b/crates/pdf-object-reader/src/error.rs @@ -154,6 +154,8 @@ pub enum ObjectReadError { #[source] source: BoxedError, }, + #[error("unsupported XObject /Subtype: '{subtype}'")] + UnsupportedXObjectSubtype { subtype: String }, } impl From for ObjectReadError { diff --git a/crates/pdf-parser/src/inline_image.rs b/crates/pdf-parser/src/inline_image.rs index 4e6502a0..e7d0ab77 100644 --- a/crates/pdf-parser/src/inline_image.rs +++ b/crates/pdf-parser/src/inline_image.rs @@ -1,4 +1,4 @@ -use pdf_image::InlineImage; +use pdf_image::PdfImage; use pdf_object_reader::{ObjectAccess, dictionary::Dictionary, object_resolver::ObjectResolver}; use crate::{error::ParserError, parser::PdfParser}; @@ -9,16 +9,16 @@ const INLINE_IMAGE_DATA_END: &[u8] = b"EI"; impl PdfParser<'_> { /// Parses an inline image after the `BI` operator has already been consumed. /// - /// Returns the canonical `pdf_image::InlineImage` representation. + /// Returns the canonical `pdf_image::PdfImage` representation. pub fn parse_inline_image( &mut self, access: &mut (impl ObjectAccess + ?Sized), - ) -> Result { + ) -> Result { let dictionary = self.parse_inline_image_dictionary(access.source())?; self.read_keyword_with_optional_eol(INLINE_IMAGE_DATA_BEGIN, false)?; self.consume_inline_image_data_separator()?; let data = self.read_inline_image_data_until_end(&dictionary, access.source())?; - InlineImage::new(dictionary, data, access) + PdfImage::inline(dictionary, data, access) .map_err(|error| ParserError::InlineImageError(error.to_string())) } diff --git a/crates/pdf-resources/src/error.rs b/crates/pdf-resources/src/error.rs index 9cc27e5b..991f0029 100644 --- a/crates/pdf-resources/src/error.rs +++ b/crates/pdf-resources/src/error.rs @@ -36,9 +36,9 @@ pub enum PdfPagesError { )] InvalidExtGStateSoftMask, #[error( - "invalid /ExtGState entry '/SMask': group XObject must have /Subtype /Form, found /{subtype}" + "invalid /ExtGState entry '/SMask': group XObject must have /Subtype /Form, found /Image" )] - SoftMaskGroupNotForm { subtype: String }, + SoftMaskGroupNotForm, #[error("invalid /ExtGState entry '/SMask': /TR must produce one finite output")] InvalidSoftMaskTransfer, #[error("invalid /PaintType value: {value}")] @@ -49,8 +49,6 @@ pub enum PdfPagesError { InvalidTilingType { value: i32 }, #[error("invalid /ShadingType value: {value}")] InvalidShadingType { value: i32 }, - #[error("unsupported XObject /Subtype: '{subtype}'")] - UnsupportedXObjectSubtype { subtype: String }, #[error("failed to process image: {0}")] Image(#[from] PdfImageError), #[error("{0}")] diff --git a/crates/pdf-resources/src/form.rs b/crates/pdf-resources/src/form.rs index 0189aa48..5c616828 100644 --- a/crates/pdf-resources/src/form.rs +++ b/crates/pdf-resources/src/form.rs @@ -32,14 +32,14 @@ impl FromPdfObject for FormXObject { }, _ => context.read::(object)?, }; - let bbox = context.dictionary().optional_bbox(context.source())?; - let matrix = context.dictionary().optional_matrix(context.source())?; - // Retain deferred handles so recursive resource graphs can finish decoding. - let resources = context.optional_shared::(b"Resources")?; Ok(Self { - bbox, - matrix, - resources, + // A malformed /BBox leaves the form unclipped. + bbox: context + .lenient::(b"BBox") + .map(|bbox| bbox.normalized()), + matrix: context.optional(b"Matrix")?, + // Retain deferred handles so recursive resource graphs can finish decoding. + resources: context.optional_shared(b"Resources")?, content_stream, }) } diff --git a/crates/pdf-resources/src/pattern.rs b/crates/pdf-resources/src/pattern.rs index 969542e4..6828af6b 100644 --- a/crates/pdf-resources/src/pattern.rs +++ b/crates/pdf-resources/src/pattern.rs @@ -120,12 +120,12 @@ impl FromPdfObject for Pattern { let object = context.object(); // Tiling patterns are streams and shading patterns are dictionaries; both expose the same entries. let mut context = context.dictionary()?; - let matrix = context.dictionary().optional_matrix(context.source())?; + let matrix = context.optional::(b"Matrix")?; match context.required::(b"PatternType")? { PatternType::Tiling => Ok(Pattern::Tiling { paint_type: context.required(b"PaintType")?, tiling_type: context.required(b"TilingType")?, - bbox: context.dictionary().required_bbox(context.source())?, + bbox: context.required::(b"BBox")?.normalized(), x_step: context.required(b"XStep")?, y_step: context.required(b"YStep")?, matrix, diff --git a/crates/pdf-resources/src/soft_mask.rs b/crates/pdf-resources/src/soft_mask.rs index 65165ef8..9c2a6f99 100644 --- a/crates/pdf-resources/src/soft_mask.rs +++ b/crates/pdf-resources/src/soft_mask.rs @@ -3,10 +3,9 @@ use crate::{error::PdfPagesError, form::FormXObject, xobject::XObjectSubtype}; use num_traits::ToPrimitive; use pdf_function::function::Function; use pdf_graphics::MaskMode; -use pdf_object_reader::object_lookup::ObjectLookupExt; -use pdf_object_reader::object_resolver::ObjectResolver; use pdf_object_reader::{ - FromPdfObject, ObjectAccess, ObjectContext, ObjectHandle, ReadResult, dictionary::Dictionary, + FromPdfObject, ObjectAccess, ObjectContext, ObjectHandle, ObjectReadError, ReadResult, + object_kind::ObjectKind, object_variant::ObjectVariant, }; use std::sync::Arc; @@ -16,62 +15,81 @@ pub struct SoftMask { pub mask_type: MaskMode, /// The transparency group. Recursive groups remain deferred until painting. pub shape: ObjectHandle, - /// Optional `/TR` mapping, sampled for the renderer's 8-bit mask coverage. - pub transfer: Option>, + /// The `/TR` mapping applied to the mask coverage. + pub transfer: MaskTransfer, } impl FromPdfObject for SoftMask { + /// The `/G` group must be a form XObject stream. An absent `/TR` is the identity. fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { let mut context = context.dictionary()?; - let mask_type = context - .dictionary() - .required_bytes(b"S", context.source()) - .map(MaskMode::from)?; - - Self::require_form_group(context.dictionary(), context.source())?; - let shape = context.required_shared(b"G")?; - // `/Identity` leaves coverage unchanged, so it needs no table. - let transfer = match context.dictionary().get(b"TR") { - Some(value) if !value.is_named(b"Identity", context.source()) => { - Some(Self::transfer_table(&context.read::(value)?)?) + // A dictionary-only form would read as an empty group and mask out everything + // painted under it, so the group must be a stream. The group may be the form being + // decoded, so it is checked on its resolved value: an eager read of /G would reenter + // the active path. + if let Some(group) = context.dictionary().get(b"G") { + let group = context.resolve(group)?; + let actual = group.kind(); + if actual != ObjectKind::Stream { + return Err(ObjectReadError::TypeMismatch { + expected: ObjectKind::Stream, + actual, + }); } - _ => None, - }; + if let XObjectSubtype::Image = context.read::(group.value())? { + return Err(PdfPagesError::SoftMaskGroupNotForm.into()); + } + } Ok(Self { - mask_type, - shape, - transfer, + mask_type: context.required(b"S")?, + shape: context.required_shared(b"G")?, + transfer: context.optional(b"TR")?.unwrap_or_default(), }) } } -impl SoftMask { - /// Rejects a `/G` transparency group that is not a form XObject. - fn require_form_group( - dictionary: &Dictionary, - objects: &dyn ObjectResolver, - ) -> Result<(), PdfPagesError> { - let group = dictionary.get_or_err(b"G")?.try_stream(objects)?; - let subtype = group.dictionary.required_bytes(b"Subtype", objects)?; - if matches!(XObjectSubtype::try_from(subtype), Ok(XObjectSubtype::Form)) { - return Ok(()); +/// A soft mask's `/TR` transfer function. +#[derive(Default)] +pub enum MaskTransfer { + /// `/Identity` leaves coverage unchanged, so it needs no table. + #[default] + Identity, + /// The function sampled for the renderer's 8-bit mask coverage. + Table(Arc<[u8; 256]>), +} + +impl FromPdfObject for MaskTransfer { + fn from_pdf_object( + mut context: ObjectContext<'_, impl ObjectAccess + ?Sized>, + ) -> ReadResult { + let object = context.object(); + match object { + ObjectVariant::String(name) if name.as_bytes() == b"Identity" => Ok(Self::Identity), + _ => Ok(Self::Table(Self::sample(&context.read(object)?)?)), + } + } +} + +impl MaskTransfer { + /// Returns the lookup table, or `None` when coverage is unchanged. + pub fn table(&self) -> Option<&Arc<[u8; 256]>> { + match self { + Self::Identity => None, + Self::Table(table) => Some(table), } - Err(PdfPagesError::SoftMaskGroupNotForm { - subtype: String::from_utf8_lossy(subtype).into_owned(), - }) } - /// Samples a `/TR` transfer function into a lookup table. - fn transfer_table(function: &Function) -> Result, PdfPagesError> { + /// Samples a transfer function into a lookup table. + fn sample(function: &Function) -> Result, PdfPagesError> { let mut table = [0; 256]; for (input, output) in (0_u8..=255).zip(&mut table) { - *output = Self::sample_transfer(function, input)?; + *output = Self::sample_one(function, input)?; } Ok(Arc::new(table)) } /// Maps one 8-bit mask coverage value through the transfer function. - fn sample_transfer(function: &Function, input: u8) -> Result { + fn sample_one(function: &Function, input: u8) -> Result { match function.apply(&[f32::from(input) / 255.0])?.as_slice() { [value] if value.is_finite() => (value.clamp(0.0, 1.0) * 255.0).round().to_u8(), _ => None, @@ -93,7 +111,7 @@ mod tests { use crate::error::PdfPagesError; - use super::SoftMask; + use super::{MaskTransfer, SoftMask}; fn soft_mask_dictionary(stream_object_number: usize, subtype: &str) -> Dictionary { let form_dictionary = Dictionary::new(BTreeMap::from([ @@ -161,7 +179,7 @@ mod tests { let mask = reader .read::(&ObjectVariant::Dictionary(dictionary)) .unwrap(); - let table = mask.transfer.unwrap(); + let table = mask.transfer.table().unwrap(); for (input, output) in (0_u8..=255).zip(table.iter()) { assert_eq!(*output, 255 - input); } @@ -177,7 +195,7 @@ mod tests { let mask = reader .read::(&ObjectVariant::Dictionary(dictionary)) .unwrap(); - assert!(mask.transfer.is_none()); + assert!(matches!(mask.transfer, MaskTransfer::Identity)); } #[test] @@ -263,7 +281,7 @@ mod tests { assert!(matches!( error, - pdf_object_reader::ObjectReadError::Decode { source, .. } if matches!(source.downcast_ref::(), Some(PdfPagesError::SoftMaskGroupNotForm { .. })) + pdf_object_reader::ObjectReadError::Decode { source, .. } if matches!(source.downcast_ref::(), Some(PdfPagesError::SoftMaskGroupNotForm)) )); } } diff --git a/crates/pdf-resources/src/xobject.rs b/crates/pdf-resources/src/xobject.rs index 30c8a151..bb212ca3 100644 --- a/crates/pdf-resources/src/xobject.rs +++ b/crates/pdf-resources/src/xobject.rs @@ -1,9 +1,8 @@ use crate::{error::PdfPagesError, form::FormXObject, resource::Resource}; use pdf_font::PdfFontSpec; use pdf_graphics::Image; -use pdf_image::{PdfImageError, read_xobject as decode_image_xobject}; -use pdf_object_reader::object_error::ObjectError; -use pdf_object_reader::object_lookup::ObjectLookupExt; +use pdf_image::{PdfImage, PdfImageError}; +use pdf_object_reader::pdf_string::PdfString; use pdf_object_reader::{ FromPdfObject, ObjectAccess, ObjectContext, ObjectReadError, ReadResult, object_variant::ObjectVariant, @@ -19,31 +18,20 @@ pub(crate) enum XObjectSubtype { Image, } -impl TryFrom<&[u8]> for XObjectSubtype { - type Error = PdfPagesError; - - fn try_from(subtype: &[u8]) -> Result { - match subtype { +/// Reads the required `/Subtype` of an XObject stream or dictionary. +impl FromPdfObject for XObjectSubtype { + fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { + let subtype = context.dictionary()?.required::(b"Subtype")?; + match subtype.as_ref() { b"Form" => Ok(Self::Form), b"Image" => Ok(Self::Image), - _ => Err(PdfPagesError::UnsupportedXObjectSubtype { - subtype: String::from_utf8_lossy(subtype).into_owned(), + other => Err(ObjectReadError::UnsupportedXObjectSubtype { + subtype: String::from_utf8_lossy(other).into_owned(), }), } } } -/// Reads the required `/Subtype` of an XObject stream or dictionary. -impl FromPdfObject for XObjectSubtype { - fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - let subtype = context - .object() - .dictionary()? - .required_bytes(b"Subtype", context.source())?; - Ok(Self::try_from(subtype)?) - } -} - impl FromPdfObject for Resource { /// Streams must name a supported XObject subtype. A dictionary without one describes /// a font. @@ -83,19 +71,13 @@ impl Resource { /// Decodes an image XObject, or marks it unavailable when its dimensions are malformed /// or its stream data cannot be decoded into samples. fn read_image(context: &mut ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { - let object = context.object(); - let dictionary = object.dictionary()?; - if !dictionary - .required_size(context.source()) - .is_ok_and(|size| size.is_valid()) - { - return Ok(Self::UnavailableImage); - } - let ObjectVariant::Stream(stream) = object else { - return Err(ObjectError::TypeMismatch("Stream", object.name()).into()); + let image = match context.read::(context.object()) { + Ok(image) => image, + Err(error) if PdfImage::is_unreadable(&error) => return Ok(Self::UnavailableImage), + Err(error) => return Err(error), }; let soft_mask = Self::read_soft_mask_image(context)?; - match decode_image_xobject(dictionary, stream, context.access(), soft_mask.as_deref()) { + match image.decode(soft_mask.as_deref()) { Ok(image) => Ok(Self::from(image)), Err(error) if error.is_unreadable_data() => Ok(Self::UnavailableImage), Err(error) => Err(PdfPagesError::from(error).into()), @@ -129,12 +111,12 @@ impl Resource { mod tests { use pdf_object_collection::object_collection::ObjectCollection; use pdf_object_reader::{ - dictionary::Dictionary, object_error::ObjectError, object_id::ObjectId, + ObjectReadError, dictionary::Dictionary, object_error::ObjectError, object_id::ObjectId, object_resolver::ObjectResolver, object_variant::ObjectVariant, stream::StreamObject, }; use std::collections::BTreeMap; - use crate::{error::PdfPagesError, resource::Resource}; + use crate::resource::Resource; fn object_id(number: usize) -> ObjectId { ObjectId { @@ -335,8 +317,7 @@ mod tests { assert!(matches!( result, - Err(pdf_object_reader::ObjectReadError::Decode { source, .. }) - if matches!(source.downcast_ref::(), Some(PdfPagesError::UnsupportedXObjectSubtype { subtype }) if subtype == "Unsupported") + Err(ObjectReadError::UnsupportedXObjectSubtype { subtype }) if subtype == "Unsupported" )); }