use crate::content::graphics_state::{GraphicsState, Matrix};
use crate::elements::{LineCap, LineJoin, PathContent, PathOperation};
use crate::geometry::{Point, Rect};
use crate::layout::Color;
#[derive(Debug, Clone, Copy)]
pub(crate) struct PathGraphicsState {
pub ctm: Matrix,
pub stroke_color_rgb: (f32, f32, f32),
pub fill_color_rgb: (f32, f32, f32),
pub line_width: f32,
pub line_cap: u8,
pub line_join: u8,
}
impl PathGraphicsState {
pub fn new() -> Self {
Self {
ctm: Matrix::identity(),
stroke_color_rgb: (0.0, 0.0, 0.0),
fill_color_rgb: (0.0, 0.0, 0.0),
line_width: 1.0,
line_cap: 0,
line_join: 0,
}
}
}
pub(crate) struct PathGraphicsStateStack {
stack: Vec<PathGraphicsState>,
}
impl PathGraphicsStateStack {
pub fn new() -> Self {
Self {
stack: vec![PathGraphicsState::new()],
}
}
pub fn current(&self) -> &PathGraphicsState {
self.stack.last().expect("Stack should never be empty")
}
pub fn current_mut(&mut self) -> &mut PathGraphicsState {
self.stack.last_mut().expect("Stack should never be empty")
}
pub fn save(&mut self) {
let state = *self.current();
self.stack.push(state);
}
pub fn restore(&mut self) {
if self.stack.len() > 1 {
self.stack.pop();
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FillRule {
NonZero,
EvenOdd,
}
#[derive(Debug)]
pub struct PathExtractor {
paths: Vec<PathContent>,
current_operations: Vec<PathOperation>,
current_point: Option<Point>,
subpath_start: Option<Point>,
current_stroke_color: Option<Color>,
current_fill_color: Option<Color>,
current_line_width: f32,
current_line_cap: LineCap,
current_line_join: LineJoin,
ctm: Matrix,
resources: Option<crate::object::Object>,
xobject_processing_stack: Vec<crate::object::ObjectRef>,
processed_xobjects: std::collections::HashSet<(crate::object::ObjectRef, [i32; 6])>,
max_xobject_depth: usize,
cached_xobject_dict: Option<std::collections::HashMap<String, crate::object::ObjectRef>>,
}
impl PathExtractor {
pub fn new() -> Self {
Self {
paths: Vec::new(),
current_operations: Vec::new(),
current_point: None,
subpath_start: None,
current_stroke_color: Some(Color::black()),
current_fill_color: None,
current_line_width: 1.0,
current_line_cap: LineCap::Butt,
current_line_join: LineJoin::Miter,
ctm: Matrix::identity(),
resources: None,
xobject_processing_stack: Vec::new(),
processed_xobjects: std::collections::HashSet::new(),
max_xobject_depth: 100,
cached_xobject_dict: None,
}
}
pub fn set_resources(&mut self, resources: crate::object::Object) {
self.resources = Some(resources);
}
pub(crate) fn swap_resources(
&mut self,
new_resources: Option<crate::object::Object>,
) -> (
Option<crate::object::Object>,
Option<std::collections::HashMap<String, crate::object::ObjectRef>>,
) {
let prev_resources = std::mem::replace(&mut self.resources, new_resources);
let prev_cache = self.cached_xobject_dict.take();
(prev_resources, prev_cache)
}
pub(crate) fn restore_resources(
&mut self,
saved: (
Option<crate::object::Object>,
Option<std::collections::HashMap<String, crate::object::ObjectRef>>,
),
) {
self.resources = saved.0;
self.cached_xobject_dict = saved.1;
}
pub(crate) fn resolve_xobject_ref<F>(
&mut self,
name: &str,
mut load_object: F,
) -> Option<crate::object::ObjectRef>
where
F: FnMut(crate::object::ObjectRef) -> crate::error::Result<crate::object::Object>,
{
if self.cached_xobject_dict.is_none() {
let mut map = std::collections::HashMap::new();
let resources = self.resources.as_ref()?;
let resolved_resources = if let Some(ref_obj) = resources.as_reference() {
load_object(ref_obj).ok()?
} else {
resources.clone()
};
let resources_dict = resolved_resources.as_dict()?;
let xobject_obj = resources_dict.get("XObject")?;
let resolved_xobject_obj = if let Some(ref_obj) = xobject_obj.as_reference() {
load_object(ref_obj).ok()?
} else {
xobject_obj.clone()
};
if let Some(xobject_dict) = resolved_xobject_obj.as_dict() {
for (key, val) in xobject_dict.iter() {
if let Some(obj_ref) = val.as_reference() {
map.insert(key.clone(), obj_ref);
}
}
}
self.cached_xobject_dict = Some(map);
}
self.cached_xobject_dict.as_ref()?.get(name).copied()
}
fn ctm_fingerprint(ctm: &Matrix) -> [i32; 6] {
[
(ctm.a * 100.0).round() as i32,
(ctm.b * 100.0).round() as i32,
(ctm.c * 100.0).round() as i32,
(ctm.d * 100.0).round() as i32,
(ctm.e * 10.0).round() as i32,
(ctm.f * 10.0).round() as i32,
]
}
pub(crate) fn can_process_xobject(&self, xobject_ref: crate::object::ObjectRef) -> bool {
let key = (xobject_ref, Self::ctm_fingerprint(&self.ctm));
if self.processed_xobjects.contains(&key) {
return false;
}
if self.xobject_processing_stack.contains(&xobject_ref) {
return false;
}
if self.xobject_processing_stack.len() >= self.max_xobject_depth {
return false;
}
true
}
pub(crate) fn push_xobject(&mut self, xobject_ref: crate::object::ObjectRef) {
self.xobject_processing_stack.push(xobject_ref);
}
pub(crate) fn pop_xobject(&mut self) {
if let Some(ref_obj) = self.xobject_processing_stack.pop() {
let key = (ref_obj, Self::ctm_fingerprint(&self.ctm));
self.processed_xobjects.insert(key);
}
}
pub(crate) fn pop_xobject_failed(&mut self) {
self.xobject_processing_stack.pop();
}
pub fn set_ctm(&mut self, ctm: Matrix) {
self.ctm = ctm;
}
pub fn update_from_state(&mut self, state: &GraphicsState) {
self.ctm = state.ctm;
self.current_line_width = state.line_width;
self.current_line_cap = match state.line_cap {
1 => LineCap::Round,
2 => LineCap::Square,
_ => LineCap::Butt,
};
self.current_line_join = match state.line_join {
1 => LineJoin::Round,
2 => LineJoin::Bevel,
_ => LineJoin::Miter,
};
let (r, g, b) = state.stroke_color_rgb;
self.current_stroke_color = Some(Color::new(r, g, b));
let (r, g, b) = state.fill_color_rgb;
self.current_fill_color = Some(Color::new(r, g, b));
}
pub(crate) fn update_from_path_state(&mut self, state: &PathGraphicsState) {
self.ctm = state.ctm;
self.current_line_width = state.line_width;
self.current_line_cap = match state.line_cap {
1 => LineCap::Round,
2 => LineCap::Square,
_ => LineCap::Butt,
};
self.current_line_join = match state.line_join {
1 => LineJoin::Round,
2 => LineJoin::Bevel,
_ => LineJoin::Miter,
};
let (r, g, b) = state.stroke_color_rgb;
self.current_stroke_color = Some(Color::new(r, g, b));
let (r, g, b) = state.fill_color_rgb;
self.current_fill_color = Some(Color::new(r, g, b));
}
pub fn set_stroke_color(&mut self, color: Color) {
self.current_stroke_color = Some(color);
}
pub fn set_fill_color(&mut self, color: Color) {
self.current_fill_color = Some(color);
}
pub fn set_line_width(&mut self, width: f32) {
self.current_line_width = width;
}
pub fn set_line_cap(&mut self, cap: LineCap) {
self.current_line_cap = cap;
}
pub fn set_line_join(&mut self, join: LineJoin) {
self.current_line_join = join;
}
pub fn move_to(&mut self, x: f32, y: f32) {
let point = self.transform_point(x, y);
self.current_operations
.push(PathOperation::MoveTo(point.x, point.y));
self.current_point = Some(point);
self.subpath_start = Some(point);
}
pub fn line_to(&mut self, x: f32, y: f32) {
let point = self.transform_point(x, y);
self.current_operations
.push(PathOperation::LineTo(point.x, point.y));
self.current_point = Some(point);
}
pub fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
let p1 = self.transform_point(x1, y1);
let p2 = self.transform_point(x2, y2);
let p3 = self.transform_point(x3, y3);
self.current_operations
.push(PathOperation::CurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y));
self.current_point = Some(p3);
}
pub fn curve_to_v(&mut self, x2: f32, y2: f32, x3: f32, y3: f32) {
let p1 = self.current_point.unwrap_or(Point::new(0.0, 0.0));
let p2 = self.transform_point(x2, y2);
let p3 = self.transform_point(x3, y3);
self.current_operations
.push(PathOperation::CurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y));
self.current_point = Some(p3);
}
pub fn curve_to_y(&mut self, x1: f32, y1: f32, x3: f32, y3: f32) {
let p1 = self.transform_point(x1, y1);
let p3 = self.transform_point(x3, y3);
self.current_operations
.push(PathOperation::CurveTo(p1.x, p1.y, p3.x, p3.y, p3.x, p3.y));
self.current_point = Some(p3);
}
pub fn rectangle(&mut self, x: f32, y: f32, width: f32, height: f32) {
let p1 = self.transform_point(x, y);
let p2 = self.transform_point(x + width, y + height);
let transformed_width = p2.x - p1.x;
let transformed_height = p2.y - p1.y;
self.current_operations.push(PathOperation::Rectangle(
p1.x,
p1.y,
transformed_width,
transformed_height,
));
self.current_point = Some(p1);
self.subpath_start = Some(p1);
}
pub fn close_path(&mut self) {
self.current_operations.push(PathOperation::ClosePath);
if let Some(start) = self.subpath_start {
self.current_point = Some(start);
}
}
pub fn stroke(&mut self) {
self.finalize_path(true, false, FillRule::NonZero);
}
pub fn close_and_stroke(&mut self) {
self.close_path();
self.stroke();
}
pub fn fill(&mut self, rule: FillRule) {
self.finalize_path(false, true, rule);
}
pub fn fill_and_stroke(&mut self, rule: FillRule) {
self.finalize_path(true, true, rule);
}
pub fn close_fill_and_stroke(&mut self, rule: FillRule) {
self.close_path();
self.fill_and_stroke(rule);
}
pub fn end_path(&mut self) {
self.current_operations.clear();
self.current_point = None;
self.subpath_start = None;
}
pub fn clip_non_zero(&mut self) {
}
pub fn clip_even_odd(&mut self) {
}
fn transform_point(&self, x: f32, y: f32) -> Point {
self.ctm.transform_point(x, y)
}
fn finalize_path(&mut self, stroke: bool, fill: bool, _rule: FillRule) {
if self.current_operations.is_empty() {
return;
}
let bbox = Self::compute_bbox(&self.current_operations);
let mut path = PathContent::new(bbox);
path.operations = std::mem::take(&mut self.current_operations);
if stroke {
path.stroke_color = self.current_stroke_color;
path.stroke_width = self.current_line_width;
path.line_cap = self.current_line_cap;
path.line_join = self.current_line_join;
} else {
path.stroke_color = None;
}
if fill {
path.fill_color = self.current_fill_color;
} else {
path.fill_color = None;
}
self.paths.push(path);
self.current_point = None;
self.subpath_start = None;
}
fn compute_bbox(operations: &[PathOperation]) -> Rect {
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = f32::MIN;
let mut max_y = f32::MIN;
for op in operations {
match op {
PathOperation::MoveTo(x, y) | PathOperation::LineTo(x, y) => {
min_x = min_x.min(*x);
min_y = min_y.min(*y);
max_x = max_x.max(*x);
max_y = max_y.max(*y);
},
PathOperation::CurveTo(x1, y1, x2, y2, x3, y3) => {
for (x, y) in [(*x1, *y1), (*x2, *y2), (*x3, *y3)] {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
},
PathOperation::Rectangle(x, y, w, h) => {
min_x = min_x.min(*x);
min_y = min_y.min(*y);
max_x = max_x.max(*x + *w);
max_y = max_y.max(*y + *h);
},
PathOperation::ClosePath => {},
}
}
if min_x == f32::MAX {
Rect::new(0.0, 0.0, 0.0, 0.0)
} else {
Rect::new(min_x, min_y, max_x - min_x, max_y - min_y)
}
}
pub fn finish(self) -> Vec<PathContent> {
self.paths
}
pub fn path_count(&self) -> usize {
self.paths.len()
}
pub fn has_current_path(&self) -> bool {
!self.current_operations.is_empty()
}
}
impl Default for PathExtractor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_extractor_new() {
let extractor = PathExtractor::new();
assert_eq!(extractor.path_count(), 0);
assert!(!extractor.has_current_path());
}
#[test]
fn test_simple_line_stroke() {
let mut extractor = PathExtractor::new();
extractor.move_to(10.0, 10.0);
extractor.line_to(100.0, 10.0);
extractor.stroke();
let paths = extractor.finish();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].operations.len(), 2);
assert!(paths[0].has_stroke());
assert!(!paths[0].has_fill());
}
#[test]
fn test_rectangle_fill() {
let mut extractor = PathExtractor::new();
extractor.set_fill_color(Color::new(1.0, 0.0, 0.0));
extractor.rectangle(50.0, 50.0, 100.0, 80.0);
extractor.fill(FillRule::NonZero);
let paths = extractor.finish();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].operations.len(), 1);
assert!(!paths[0].has_stroke());
assert!(paths[0].has_fill());
assert_eq!(paths[0].bbox.x, 50.0);
assert_eq!(paths[0].bbox.y, 50.0);
assert_eq!(paths[0].bbox.width, 100.0);
assert_eq!(paths[0].bbox.height, 80.0);
}
#[test]
fn test_closed_path() {
let mut extractor = PathExtractor::new();
extractor.set_fill_color(Color::new(0.5, 0.5, 0.5));
extractor.move_to(0.0, 0.0);
extractor.line_to(100.0, 0.0);
extractor.line_to(100.0, 100.0);
extractor.line_to(0.0, 100.0);
extractor.close_path();
extractor.fill_and_stroke(FillRule::NonZero);
let paths = extractor.finish();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].operations.len(), 5); assert!(paths[0].has_stroke());
assert!(paths[0].has_fill());
}
#[test]
fn test_bezier_curve() {
let mut extractor = PathExtractor::new();
extractor.move_to(0.0, 0.0);
extractor.curve_to(25.0, 100.0, 75.0, 100.0, 100.0, 0.0);
extractor.stroke();
let paths = extractor.finish();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].operations.len(), 2);
assert!(matches!(paths[0].operations[1], PathOperation::CurveTo(_, _, _, _, _, _)));
}
#[test]
fn test_multiple_paths() {
let mut extractor = PathExtractor::new();
extractor.move_to(0.0, 0.0);
extractor.line_to(100.0, 0.0);
extractor.stroke();
extractor.move_to(50.0, 0.0);
extractor.line_to(50.0, 100.0);
extractor.stroke();
let paths = extractor.finish();
assert_eq!(paths.len(), 2);
}
#[test]
fn test_end_path_clears_operations() {
let mut extractor = PathExtractor::new();
extractor.move_to(0.0, 0.0);
extractor.line_to(100.0, 100.0);
extractor.end_path();
let paths = extractor.finish();
assert_eq!(paths.len(), 0);
}
#[test]
fn test_line_style_properties() {
let mut extractor = PathExtractor::new();
extractor.set_line_width(3.0);
extractor.set_line_cap(LineCap::Round);
extractor.set_line_join(LineJoin::Bevel);
extractor.set_stroke_color(Color::new(0.0, 0.0, 1.0));
extractor.move_to(0.0, 0.0);
extractor.line_to(100.0, 100.0);
extractor.stroke();
let paths = extractor.finish();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].stroke_width, 3.0);
assert_eq!(paths[0].line_cap, LineCap::Round);
assert_eq!(paths[0].line_join, LineJoin::Bevel);
}
#[test]
fn test_ctm_transformation() {
let mut extractor = PathExtractor::new();
extractor.set_ctm(Matrix::translation(50.0, 50.0));
extractor.move_to(0.0, 0.0);
extractor.line_to(100.0, 0.0);
extractor.stroke();
let paths = extractor.finish();
assert_eq!(paths.len(), 1);
if let PathOperation::MoveTo(x, y) = paths[0].operations[0] {
assert_eq!(x, 50.0);
assert_eq!(y, 50.0);
} else {
panic!("Expected MoveTo operation");
}
}
#[test]
fn test_bbox_calculation() {
let mut extractor = PathExtractor::new();
extractor.move_to(10.0, 20.0);
extractor.line_to(110.0, 20.0);
extractor.line_to(110.0, 120.0);
extractor.line_to(10.0, 120.0);
extractor.close_path();
extractor.stroke();
let paths = extractor.finish();
assert_eq!(paths.len(), 1);
let bbox = &paths[0].bbox;
assert_eq!(bbox.x, 10.0);
assert_eq!(bbox.y, 20.0);
assert_eq!(bbox.width, 100.0);
assert_eq!(bbox.height, 100.0);
}
#[test]
fn test_curve_to_v() {
let mut extractor = PathExtractor::new();
extractor.move_to(0.0, 0.0);
extractor.curve_to_v(50.0, 100.0, 100.0, 0.0);
extractor.stroke();
let paths = extractor.finish();
assert_eq!(paths.len(), 1);
if let PathOperation::CurveTo(x1, y1, _, _, _, _) = paths[0].operations[1] {
assert_eq!(x1, 0.0);
assert_eq!(y1, 0.0);
}
}
#[test]
fn test_curve_to_y() {
let mut extractor = PathExtractor::new();
extractor.move_to(0.0, 0.0);
extractor.curve_to_y(50.0, 100.0, 100.0, 0.0);
extractor.stroke();
let paths = extractor.finish();
assert_eq!(paths.len(), 1);
if let PathOperation::CurveTo(_, _, x2, y2, x3, y3) = paths[0].operations[1] {
assert_eq!(x2, x3);
assert_eq!(y2, y3);
}
}
#[test]
fn test_fill_even_odd() {
let mut extractor = PathExtractor::new();
extractor.set_fill_color(Color::new(0.0, 1.0, 0.0));
extractor.rectangle(0.0, 0.0, 100.0, 100.0);
extractor.fill(FillRule::EvenOdd);
let paths = extractor.finish();
assert_eq!(paths.len(), 1);
assert!(paths[0].has_fill());
}
#[test]
fn test_close_and_stroke() {
let mut extractor = PathExtractor::new();
extractor.move_to(0.0, 0.0);
extractor.line_to(100.0, 0.0);
extractor.line_to(50.0, 100.0);
extractor.close_and_stroke();
let paths = extractor.finish();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].operations.len(), 4);
assert!(matches!(paths[0].operations[3], PathOperation::ClosePath));
}
#[test]
fn test_update_from_state() {
let mut extractor = PathExtractor::new();
let mut state = GraphicsState::new();
state.line_width = 5.0;
state.line_cap = 1; state.line_join = 2; state.stroke_color_rgb = (1.0, 0.0, 0.0);
state.fill_color_rgb = (0.0, 1.0, 0.0);
extractor.update_from_state(&state);
extractor.rectangle(0.0, 0.0, 100.0, 100.0);
extractor.fill_and_stroke(FillRule::NonZero);
let paths = extractor.finish();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].stroke_width, 5.0);
assert_eq!(paths[0].line_cap, LineCap::Round);
assert_eq!(paths[0].line_join, LineJoin::Bevel);
}
#[test]
fn test_pop_xobject_marks_as_processed() {
let mut ext = PathExtractor::new();
let r = crate::object::ObjectRef::new(42, 0);
assert!(ext.can_process_xobject(r));
ext.push_xobject(r);
ext.pop_xobject(); assert!(
!ext.can_process_xobject(r),
"Successfully processed XObject should be permanently skipped"
);
}
#[test]
fn test_pop_xobject_failed_allows_retry() {
let mut ext = PathExtractor::new();
let r = crate::object::ObjectRef::new(42, 0);
assert!(ext.can_process_xobject(r));
ext.push_xobject(r);
ext.pop_xobject_failed(); assert!(ext.can_process_xobject(r), "Failed XObject should be retryable");
}
}