diff --git a/crates/pdf-canvas/src/canvas_graphics_state_ops.rs b/crates/pdf-canvas/src/canvas_graphics_state_ops.rs index f5c0ed9..cf8f3e0 100644 --- a/crates/pdf-canvas/src/canvas_graphics_state_ops.rs +++ b/crates/pdf-canvas/src/canvas_graphics_state_ops.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use pdf_content_stream_operators::pdf_operator_backend::GraphicsStateOps; -use pdf_graphics::{DashPattern, LineCap, LineJoin, transform::Transform}; +use pdf_graphics::{DashPattern, LineCap, LineJoin, MaskMode, transform::Transform}; use pdf_resources::{ external_graphics_state::ExternalGraphicsStateKey, resource::Resource, xobject::XObject, }; @@ -135,6 +135,10 @@ impl GraphicsStateOps for PdfCanvas<'_, B> { ExternalGraphicsStateKey::SoftMask(smask) => { // Handle the `/SMask` entry from an `ExtGState` dictionary. if let Some(smask) = smask.as_ref() { + if matches!(&smask.mask_type, MaskMode::Unknown(_)) { + continue; + } + if let XObject::Image(_) = &smask.shape { return Err(PdfCanvasError::UnsupportedFeature( "SoftMask with Image shape".into(), @@ -166,11 +170,15 @@ impl GraphicsStateOps for PdfCanvas<'_, B> { // Enable the mask on the main canvas. Subsequent drawing operations // will be modulated by this mask. - self.canvas - .begin_mask_layer(&arc, &transform, smask.mask_type)?; + self.canvas.begin_mask_layer( + &arc, + &transform, + smask.mask_type.clone(), + )?; // Store the mask in the current canvas state to be used until it's finished. - self.mask = Some((Arc::clone(&arc), smask.mask_type, transform)); + self.mask = + Some((Arc::clone(&arc), smask.mask_type.clone(), transform)); } } else if let Some((mask, mask_type, transform)) = self.mask.take() { // This branch handles the case where `/SMask` is set to `/None` in the `ExtGState`, diff --git a/crates/pdf-canvas/src/recording_canvas.rs b/crates/pdf-canvas/src/recording_canvas.rs index 96f0227..0dcbb21 100644 --- a/crates/pdf-canvas/src/recording_canvas.rs +++ b/crates/pdf-canvas/src/recording_canvas.rs @@ -163,14 +163,14 @@ impl RecordingCanvas { mask_mode, mask, } => { - backend.begin_mask_layer(mask, transform, *mask_mode)?; + backend.begin_mask_layer(mask, transform, mask_mode.clone())?; } EndMaskLayer { mask, transform, mask_mode, } => { - backend.end_mask_layer(mask, transform, *mask_mode)?; + backend.end_mask_layer(mask, transform, mask_mode.clone())?; } } } diff --git a/crates/pdf-graphics-skia/src/skia_canvas_backend.rs b/crates/pdf-graphics-skia/src/skia_canvas_backend.rs index 163408e..659c5e4 100644 --- a/crates/pdf-graphics-skia/src/skia_canvas_backend.rs +++ b/crates/pdf-graphics-skia/src/skia_canvas_backend.rs @@ -519,8 +519,12 @@ impl CanvasBackend for SkiaCanvasBackend<'_> { &mut self, mask: &Arc, transform: &Transform, - _mask_mode: MaskMode, + mask_mode: MaskMode, ) -> Result<(), PdfCanvasError> { + if matches!(mask_mode, MaskMode::Unknown(_)) { + return Ok(()); + } + self.surface.canvas().save(); let mat = to_skia_matrix(transform); let rect = skia_safe::Rect::from_xywh(0.0, 0.0, mask.width(), mask.height()); @@ -540,6 +544,10 @@ impl CanvasBackend for SkiaCanvasBackend<'_> { transform: &Transform, mask_mode: MaskMode, ) -> Result<(), PdfCanvasError> { + if matches!(&mask_mode, MaskMode::Unknown(_)) { + return Ok(()); + } + // Render mask into a temporary surface depending on the requested mask mode. // - Alpha: render directly into an A8 mask surface. // - Luminosity: render into RGBA and then convert RGB luminance into an A8 mask. @@ -555,7 +563,7 @@ impl CanvasBackend for SkiaCanvasBackend<'_> { }; // Create appropriate surface - let mut surface = match mask_mode { + let mut surface = match &mask_mode { MaskMode::Alpha => { let info = skia_safe::ImageInfo::new_a8((mask.width() as i32, mask.height() as i32)); @@ -571,6 +579,7 @@ impl CanvasBackend for SkiaCanvasBackend<'_> { ); make_surface(info)? } + MaskMode::Unknown(_) => return Ok(()), }; // Replay the recorded mask drawing operations into the temporary surface. diff --git a/crates/pdf-graphics/src/lib.rs b/crates/pdf-graphics/src/lib.rs index 2dcede5..a130ced 100644 --- a/crates/pdf-graphics/src/lib.rs +++ b/crates/pdf-graphics/src/lib.rs @@ -3,6 +3,7 @@ pub mod blend_mode; mod bounds_accumulator; pub mod color; pub mod dash_pattern; +pub mod mask_mode; pub mod pdf_path; pub mod point; pub mod rect; @@ -11,6 +12,7 @@ pub mod transform; pub use blend_mode::BlendMode; pub use bounds_accumulator::BoundsAccumulator; pub use dash_pattern::DashPattern; +pub use mask_mode::MaskMode; use num_derive::FromPrimitive; /// Specifies the shape to be used at the end of open subpaths when they are stroked. @@ -70,15 +72,6 @@ pub enum TextRenderingMode { Clip, } -/// Specifies the mode for applying a soft mask in PDF graphics. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MaskMode { - /// The soft mask is applied to the alpha channel only. - Alpha, - /// The soft mask is applied to the luminosity channel. - Luminosity, -} - /// Represents the pixel format of image data in PDF graphics. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PixelFormat { diff --git a/crates/pdf-graphics/src/mask_mode.rs b/crates/pdf-graphics/src/mask_mode.rs new file mode 100644 index 0000000..60ef283 --- /dev/null +++ b/crates/pdf-graphics/src/mask_mode.rs @@ -0,0 +1,20 @@ +/// Specifies the mode for applying a soft mask in PDF graphics. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MaskMode { + /// The soft mask is applied to the alpha channel only. + Alpha, + /// The soft mask is applied to the luminosity channel. + Luminosity, + /// An unrecognized soft mask mode. + Unknown(String), +} + +impl From<&str> for MaskMode { + fn from(value: &str) -> Self { + match value { + "Alpha" => Self::Alpha, + "Luminosity" => Self::Luminosity, + other => Self::Unknown(other.to_owned()), + } + } +} diff --git a/crates/pdf-resources/src/external_graphics_state.rs b/crates/pdf-resources/src/external_graphics_state.rs index df136dd..d98bc37 100644 --- a/crates/pdf-resources/src/external_graphics_state.rs +++ b/crates/pdf-resources/src/external_graphics_state.rs @@ -149,17 +149,6 @@ fn invalid_ext_gstate_entry_value(entry: &str, reason: impl Into) -> Pdf } } -fn parse_mask_mode(value: &str) -> Result { - match value { - "Luminosity" => Ok(MaskMode::Luminosity), - "Alpha" => Ok(MaskMode::Alpha), - other => Err(invalid_ext_gstate_entry_value( - "SMask/S", - format!("unsupported soft mask mode '{other}' (expected 'Alpha' or 'Luminosity')"), - )), - } -} - fn parse_dash_pattern( key_name: &str, value: &ObjectVariant, @@ -239,7 +228,7 @@ fn parse_soft_mask( ) -> Result { let smask = match value { ObjectVariant::Dictionary(dict) => { - let mask_type = parse_mask_mode(dict.required_str("S", objects)?)?; + let mask_type = MaskMode::from(dict.required_str("S", objects)?); // Parse the "G" key for the `XObject` let content = dict.get_or_err("G")?;