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>>,
oc_layer_stack: Vec<Option<String>>,
}
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,
oc_layer_stack: Vec::new(),
}
}
pub fn push_oc_layer(&mut self, layer: Option<String>) {
let effective = layer.or_else(|| self.oc_layer_stack.last().cloned().flatten());
self.oc_layer_stack.push(effective);
}
pub fn pop_oc_layer(&mut self) {
self.oc_layer_stack.pop();
}
pub(crate) fn oc_layer_depth(&self) -> usize {
self.oc_layer_stack.len()
}
pub(crate) fn truncate_oc_layers(&mut self, depth: usize) {
self.oc_layer_stack.truncate(depth);
}
fn current_layer(&self) -> Option<String> {
self.oc_layer_stack.last().cloned().flatten()
}
pub fn set_resources(&mut self, resources: crate::object::Object) {
self.resources = Some(resources);
}
pub(crate) fn current_resources(&self) -> Option<&crate::object::Object> {
self.resources.as_ref()
}
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 * self.ctm.stroke_scale();
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;
}
path.layer = self.current_layer();
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;