From e24a00d7c778a179b4b198a5e793d69ee6734d9c Mon Sep 17 00:00:00 2001 From: Velli20 Date: Wed, 21 Oct 2026 22:13:21 +0300 Subject: [PATCH 1/3] Read images, color spaces and soft masks through the typed object reader Image, color space, soft mask and form decoding still went back to the raw (dictionary, objects) lookup API. They are now FromPdfObject impls read through the context APIs. Inline images and image XObjects merge into one PdfImage: a FromPdfObject decoder that reads ImageMetadata and the filtered ImagePayload from a stream, and decode(soft_mask) for the raster. Inline images are read by wrapping their normalized dictionary and data in an encoded stream. read_xobject, decode_inline_image, decode_normalized_image and decode_data_with_access are removed. ImageMetadata builds its value in one pass instead of filling ImageEntries, and Resource marks an image unavailable through PdfImage::is_unreadable instead of a raw size check. Filter and image errors keep their type across reads. ColorSpace matches device names in its decoder and reads the Indexed, Separation and DeviceN families with context.read. Colorant names and the Indexed lookup are read with array.at. SoftMask reads /S as a MaskMode, checks that /G is a form XObject stream, and reads /TR as a MaskTransfer, which is Identity or a sampled table. XObjectSubtype decodes /Subtype directly. Rect and Transform gain FromPdfObject decoders, which form and pattern use for /BBox and /Matrix. Co-Authored-By: Claude Opus 5.5 --- .../src/canvas_external_object_ops.rs | 7 +- .../src/canvas_graphics_state_ops.rs | 2 +- .../tests/canvas_graphics_state_ops.rs | 4 +- crates/pdf-canvas/tests/pdf_canvas.rs | 20 +- crates/pdf-color-space/src/color_space.rs | 22 -- .../pdf-color-space/src/color_space_reader.rs | 93 +++--- .../src/device_n_color_space.rs | 20 +- .../src/indexed_color_space.rs | 49 +-- .../src/separation_color_space.rs | 10 +- .../src/pdf_operator_backend.rs | 3 +- .../src/recording_pdf_operator_backend.rs | 4 +- .../src/variants.rs | 6 +- crates/pdf-filter/src/error.rs | 9 + crates/pdf-filter/src/filter.rs | 55 +--- crates/pdf-filter/src/image_payload.rs | 40 ++- crates/pdf-graphics/src/mask_mode.rs | 10 - crates/pdf-image/src/decoded_samples.rs | 8 +- crates/pdf-image/src/error.rs | 36 ++- crates/pdf-image/src/image_decoder.rs | 302 ++++++------------ crates/pdf-image/src/image_metadata.rs | 257 +++++---------- crates/pdf-image/src/lib.rs | 7 +- .../src/{inline_image.rs => pdf_image.rs} | 91 ++++-- crates/pdf-object-reader/src/decode.rs | 33 +- crates/pdf-object-reader/src/error.rs | 2 + crates/pdf-parser/src/inline_image.rs | 8 +- crates/pdf-resources/src/error.rs | 6 +- crates/pdf-resources/src/form.rs | 14 +- crates/pdf-resources/src/pattern.rs | 4 +- crates/pdf-resources/src/soft_mask.rs | 99 +++--- crates/pdf-resources/src/xobject.rs | 53 +-- 30 files changed, 567 insertions(+), 707 deletions(-) rename crates/pdf-image/src/{inline_image.rs => pdf_image.rs} (80%) diff --git a/crates/pdf-canvas/src/canvas_external_object_ops.rs b/crates/pdf-canvas/src/canvas_external_object_ops.rs index 8fec8cb9..94a11a35 100644 --- a/crates/pdf-canvas/src/canvas_external_object_ops.rs +++ b/crates/pdf-canvas/src/canvas_external_object_ops.rs @@ -9,7 +9,7 @@ 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}; @@ -79,8 +79,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) + fn paint_inline_image(&mut self, image: &PdfImage) -> Result<(), Self::ErrorType> { + 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..b5d45f99 100644 --- a/crates/pdf-canvas/src/canvas_graphics_state_ops.rs +++ b/crates/pdf-canvas/src/canvas_graphics_state_ops.rs @@ -163,7 +163,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/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/pdf_operator_backend.rs b/crates/pdf-content-stream-operators/src/pdf_operator_backend.rs index 7447f53a..0ae455bd 100644 --- a/crates/pdf-content-stream-operators/src/pdf_operator_backend.rs +++ b/crates/pdf-content-stream-operators/src/pdf_operator_backend.rs @@ -740,8 +740,7 @@ pub trait XObjectOps { 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>; + fn paint_inline_image(&mut self, image: &pdf_image::PdfImage) -> Result<(), Self::ErrorType>; } /// Defines methods to handle PDF Shading operators. 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 index ddc34c8d..9eea69be 100644 --- a/crates/pdf-content-stream-operators/src/recording_pdf_operator_backend.rs +++ b/crates/pdf-content-stream-operators/src/recording_pdf_operator_backend.rs @@ -3,7 +3,7 @@ use pdf_graphics::LineCap; use pdf_graphics::LineJoin; use pdf_graphics::TextRenderingMode; use pdf_graphics::transform::Transform; -use pdf_image::InlineImage; +use pdf_image::PdfImage; use thiserror::Error; use crate::PdfTextItem; @@ -685,7 +685,7 @@ impl XObjectOps for RecordingBackend { Ok(()) } - fn paint_inline_image(&mut self, image: &InlineImage) -> Result<(), Self::ErrorType> { + fn paint_inline_image(&mut self, image: &PdfImage) -> Result<(), Self::ErrorType> { self.operations.push(RecordedOperation::PaintInlineImage { data: image.shared_data(), }); diff --git a/crates/pdf-content-stream-operators/src/variants.rs b/crates/pdf-content-stream-operators/src/variants.rs index b68386f4..8c741644 100644 --- a/crates/pdf-content-stream-operators/src/variants.rs +++ b/crates/pdf-content-stream-operators/src/variants.rs @@ -1,7 +1,7 @@ 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::{ @@ -352,7 +352,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. @@ -779,7 +779,7 @@ mod tests { #[test] fn inline_image_call_dispatches_to_backend_hook() { - let image = InlineImage::new( + let image = PdfImage::inline( Dictionary::new(BTreeMap::from([ (Vec::from(b"BPC"), ObjectVariant::Integer(8)), ( 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..3b2ea46b 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,78 @@ 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. + if let Some(group) = context.dictionary().get(b"G") { + let actual = context.resolve(group)?.kind(); + if actual != ObjectKind::Stream { + return Err(ObjectReadError::TypeMismatch { + expected: ObjectKind::Stream, + actual, + }); } - _ => None, - }; + } + if let XObjectSubtype::Image = context.required::(b"G")? { + 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 +108,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 +176,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 +192,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 +278,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" )); } From ae19a98b4302d8c9790f0193bdb1a437c0ed777f Mon Sep 17 00:00:00 2001 From: Velli20 Date: Wed, 21 Oct 2026 22:38:30 +0300 Subject: [PATCH 2/3] Dispatch content stream operators by matching on PdfOperatorVariant The operator backend traits (twelve per-category traits plus the PdfOperatorBackend supertrait) had one production implementor, PdfCanvas, and a test-only recorder that rebuilt a near-copy of PdfOperatorVariant. Nothing was generic over a single category trait, so the layer only added indirection over an enum that already describes every operator. PdfCanvas now dispatches through apply_operator, and its handlers are inherent methods. Tests check the parsed operators directly instead of replaying them through the recorder. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- crates/pdf-canvas/src/canvas_clip_ops.rs | 10 +- crates/pdf-canvas/src/canvas_color_ops.rs | 68 +- .../src/canvas_external_object_ops.rs | 8 +- .../src/canvas_graphics_state_ops.rs | 43 +- .../src/canvas_marked_content_ops.rs | 28 +- .../src/canvas_operator_dispatch.rs | 131 +++ crates/pdf-canvas/src/canvas_path_ops.rs | 75 +- crates/pdf-canvas/src/canvas_text_ops.rs | 84 +- crates/pdf-canvas/src/lib.rs | 7 +- crates/pdf-canvas/src/pdf_canvas.rs | 3 +- crates/pdf-canvas/src/shading.rs | 6 +- .../pdf-content-stream-operators/src/lib.rs | 3 - .../src/marked_content_operators.rs | 20 +- .../src/pdf_operator_backend.rs | 849 ------------------ .../src/recording_pdf_operator_backend.rs | 752 ---------------- .../src/variants.rs | 153 +--- .../pdf-content-stream/src/content_stream.rs | 90 +- crates/pdf-content-stream/tests/parser_api.rs | 87 +- 19 files changed, 374 insertions(+), 2045 deletions(-) create mode 100644 crates/pdf-canvas/src/canvas_operator_dispatch.rs delete mode 100644 crates/pdf-content-stream-operators/src/pdf_operator_backend.rs delete mode 100644 crates/pdf-content-stream-operators/src/recording_pdf_operator_backend.rs 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 94a11a35..255833da 100644 --- a/crates/pdf-canvas/src/canvas_external_object_ops.rs +++ b/crates/pdf-canvas/src/canvas_external_object_ops.rs @@ -7,7 +7,6 @@ //! 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::{PdfImage, raster::stencil_image}; use pdf_resources::resource::Resource; @@ -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,7 +77,7 @@ impl XObjectOps for PdfCanvas<'_, B> { } /// Decodes inline image samples before applying the current painting state. - fn paint_inline_image(&mut self, image: &PdfImage) -> Result<(), Self::ErrorType> { + 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()))?; diff --git a/crates/pdf-canvas/src/canvas_graphics_state_ops.rs b/crates/pdf-canvas/src/canvas_graphics_state_ops.rs index b5d45f99..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 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-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 0ae455bd..00000000 --- a/crates/pdf-content-stream-operators/src/pdf_operator_backend.rs +++ /dev/null @@ -1,849 +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::PdfImage) -> 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 9eea69be..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::PdfImage; -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: &PdfImage) -> 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 8c741644..e8630d4f 100644 --- a/crates/pdf-content-stream-operators/src/variants.rs +++ b/crates/pdf-content-stream-operators/src/variants.rs @@ -5,10 +5,8 @@ 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. @@ -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 = PdfImage::inline( - 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() From 0d6b2551648cdcb63f11c1717275058706b1c858 Mon Sep 17 00:00:00 2001 From: Velli20 Date: Wed, 21 Oct 2026 22:47:04 +0300 Subject: [PATCH 3/3] Check soft mask groups without reentering the active path A soft mask's /G group can be the form whose resources are being decoded. Reading its subtype eagerly through the reference pushed that form onto the active path again and failed with a cyclic reference. Check the kind and subtype on the resolved value instead. Co-Authored-By: Claude Opus 5.5 --- crates/pdf-resources/src/soft_mask.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/pdf-resources/src/soft_mask.rs b/crates/pdf-resources/src/soft_mask.rs index 3b2ea46b..9c2a6f99 100644 --- a/crates/pdf-resources/src/soft_mask.rs +++ b/crates/pdf-resources/src/soft_mask.rs @@ -24,18 +24,21 @@ impl FromPdfObject for SoftMask { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { let mut context = context.dictionary()?; // A dictionary-only form would read as an empty group and mask out everything - // painted under it, so the group must be a stream. + // 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 actual = context.resolve(group)?.kind(); + let group = context.resolve(group)?; + let actual = group.kind(); if actual != ObjectKind::Stream { return Err(ObjectReadError::TypeMismatch { expected: ObjectKind::Stream, actual, }); } - } - if let XObjectSubtype::Image = context.required::(b"G")? { - return Err(PdfPagesError::SoftMaskGroupNotForm.into()); + if let XObjectSubtype::Image = context.read::(group.value())? { + return Err(PdfPagesError::SoftMaskGroupNotForm.into()); + } } Ok(Self { mask_type: context.required(b"S")?,