Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions crates/pdf-canvas/src/canvas_clip_ops.rs
Original file line number Diff line number Diff line change
@@ -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<B: CanvasBackend> ClippingPathOps for PdfCanvas<'_, B> {
type ErrorType = PdfCanvasError;
fn clip_path_nonzero_winding(&mut self) -> Result<(), Self::ErrorType> {
impl<B: CanvasBackend> 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)
}
}
Expand Down
68 changes: 50 additions & 18 deletions crates/pdf-canvas/src/canvas_color_ops.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -28,17 +27,22 @@ fn apply_content_stream_color(
}
}

impl<B: CanvasBackend> ColorOps for PdfCanvas<'_, B> {
type ErrorType = PdfCanvasError;
fn set_stroking_color_space(&mut self, name: &[u8]) -> Result<(), Self::ErrorType> {
impl<B: CanvasBackend> 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;

Expand All @@ -52,7 +56,11 @@ impl<B: CanvasBackend> 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;

Expand All @@ -66,11 +74,12 @@ impl<B: CanvasBackend> 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;
Expand All @@ -91,11 +100,12 @@ impl<B: CanvasBackend> 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;
Expand All @@ -116,53 +126,75 @@ impl<B: CanvasBackend> 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;
state.stroke_color_space = Some(Arc::new(CanvasState::DEVICE_GRAY_COLOR_SPACE));
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;
state.fill_color_space = Some(Arc::new(CanvasState::DEVICE_GRAY_COLOR_SPACE));
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;
state.stroke_color_space = Some(Arc::new(CanvasState::DEVICE_RGB_COLOR_SPACE));
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;
state.fill_color_space = Some(Arc::new(CanvasState::DEVICE_RGB_COLOR_SPACE));
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;
state.stroke_color_space = Some(Arc::new(CanvasState::DEVICE_CMYK_COLOR_SPACE));
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;
Expand Down
13 changes: 6 additions & 7 deletions crates/pdf-canvas/src/canvas_external_object_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
//! Images are placed by handing the backend the full image-space to device-space
//! matrix, so rotation, shear and mirroring in the CTM are all preserved.

use pdf_content_stream_operators::pdf_operator_backend::XObjectOps;
use pdf_graphics::{Image, PixelFormat, transform::Transform};
use pdf_image::{InlineImage, decode_inline_image, raster::stencil_image};
use pdf_image::{PdfImage, raster::stencil_image};
use pdf_resources::resource::Resource;

use crate::{canvas_backend::CanvasBackend, error::PdfCanvasError, pdf_canvas::PdfCanvas};
Expand All @@ -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<B: CanvasBackend> XObjectOps for PdfCanvas<'_, B> {
type ErrorType = PdfCanvasError;
impl<B: CanvasBackend> PdfCanvas<'_, B> {
/// Invokes (renders) an XObject by name from the current resource dictionary.
///
/// This method handles two types of XObjects:
Expand All @@ -41,7 +39,7 @@ impl<B: CanvasBackend> 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
Expand Down Expand Up @@ -79,8 +77,9 @@ impl<B: CanvasBackend> XObjectOps for PdfCanvas<'_, B> {
}

/// Decodes inline image samples before applying the current painting state.
fn paint_inline_image(&mut self, image: &InlineImage) -> Result<(), Self::ErrorType> {
let decoded = decode_inline_image(image, None)
pub(crate) fn paint_inline_image(&mut self, image: &PdfImage) -> Result<(), PdfCanvasError> {
let decoded = image
.decode(None)
.map_err(|e| PdfCanvasError::InvalidImageData(e.to_string()))?;

self.render_decoded_image(&decoded, true)
Expand Down
45 changes: 13 additions & 32 deletions crates/pdf-canvas/src/canvas_graphics_state_ops.rs
Original file line number Diff line number Diff line change
@@ -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<B: CanvasBackend> 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<B: CanvasBackend> 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.
//
Expand All @@ -31,51 +19,44 @@ impl<B: CanvasBackend> 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
Expand Down Expand Up @@ -163,7 +144,7 @@ impl<B: CanvasBackend> 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;
Expand Down
28 changes: 8 additions & 20 deletions crates/pdf-canvas/src/canvas_marked_content_ops.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,20 @@
use pdf_content_stream_operators::pdf_operator_backend::MarkedContentOps;

use crate::{canvas_backend::CanvasBackend, error::PdfCanvasError, pdf_canvas::PdfCanvas};

impl<B: CanvasBackend> 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<B: CanvasBackend> 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(());
};
Expand All @@ -40,7 +27,8 @@ impl<B: CanvasBackend> 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();
}
Expand Down
Loading
Loading