//! Path
//! Good reading <https://razrfalcon.github.io/notes-on-svg-parsing/path-data.html>
use std::fmt::{Display, Formatter, Write};
use svgtypes::{PathParser, PathSegment};
/// Represents a single SVG path segment.
#[derive(Debug, Clone)]
pub struct SvgItem {
/// Represents a single SVG path segment.
pub inner: PathSegment,
}
/// Round a value
fn round_to(value: f64, decimals: u64) -> f64 {
let decimals = i32::try_from(decimals).unwrap_or(1);
let factor = 10f64.powi(decimals);
(value * factor).round() / factor
}
impl SvgItem {
/// Returns the letter
#[must_use]
pub fn get_letter(&self) -> char {
self.inner.command() as char
}
/// Returns the value of the segment as a string.
/// # Errors
/// Returns an error if the segment cannot be parsed.
pub fn set_value(&mut self, to_add: SvgItem) {
*self = to_add;
}
}
impl Display for SvgItem {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let fmt = match self.inner {
PathSegment::MoveTo { abs: _, x, y } | PathSegment::LineTo { abs: _, x, y } => {
// move to
format!("{}{} {}", self.get_letter(), x, y)
}
PathSegment::HorizontalLineTo { abs: _, x } => {
format!("{}{}", self.get_letter(), x)
}
PathSegment::VerticalLineTo { abs: _, y } => {
format!("{}{}", self.get_letter(), y)
}
PathSegment::CurveTo {
abs: _,
x1,
y1,
x2,
y2,
x,
y,
} => format!(
"{}{} {} {} {} {} {}",
self.get_letter(),
x1,
y1,
x2,
y2,
x,
y
),
PathSegment::SmoothCurveTo {
abs: _,
x2,
y2,
x,
y,
} => {
format!("{}{} {} {} {}", self.get_letter(), x2, y2, x, y)
}
PathSegment::Quadratic {
abs: _,
x1,
y1,
x,
y,
} => {
format!("{}{} {} {} {}", self.get_letter(), x1, y1, x, y)
}
PathSegment::SmoothQuadratic { abs: _, x, y } => {
// smooth quadratic
format!("{}{} {}", self.get_letter(), x, y)
}
PathSegment::EllipticalArc {
abs: _,
rx,
ry,
x_axis_rotation,
large_arc,
sweep,
x,
y,
} => format!(
"{}{} {} {} {} {} {} {}",
self.get_letter(),
rx,
ry,
x_axis_rotation,
if large_arc { "1" } else { "0" },
if sweep { "1" } else { "0" },
x,
y
),
PathSegment::ClosePath { abs } => {
if abs {
"Z".to_string()
} else {
"z".to_string()
}
}
};
write!(f, "{fmt}")?;
Ok(())
}
}
/// Represents an SVG path, which is a collection of SVG path items.
#[derive(Debug, Clone)]
pub struct SvgPath {
/// A vector of SVG path items.
pub items: Vec<SvgItem>,
}
impl SvgPath {
/// Parses a string into an SVG path.
/// # Errors
/// Returns an error if the string cannot be parsed into a valid SVG path.
pub fn parse(path_str: &str) -> Result<Self, String> {
let s = PathParser::from(path_str);
let mut items = Vec::new();
for segment in s {
let one_segment = segment
.map_err(|e| format!("Failed to parse SVG path segment: {e} in {path_str}"))?;
let to_add = SvgItem { inner: one_segment };
items.push(to_add);
}
Ok(SvgPath { items })
}
/// Converts the path to absolute coordinates.
#[allow(clippy::too_many_lines)]
pub fn absolute(&mut self) {
let mut pos = (0.0, 0.0);
let mut subpath_start = (0.0, 0.0);
for item in &mut self.items {
match &mut item.inner {
PathSegment::MoveTo { abs, x, y } => {
if !*abs {
*x += pos.0;
*y += pos.1;
*abs = true;
}
pos.0 = *x;
pos.1 = *y;
subpath_start = pos;
}
PathSegment::LineTo { abs, x, y } => {
if !*abs {
*x += pos.0;
*y += pos.1;
*abs = true;
}
pos.0 = *x;
pos.1 = *y;
}
PathSegment::HorizontalLineTo { abs, x } => {
if !*abs {
*x += pos.0;
*abs = true;
}
pos.0 = *x;
}
PathSegment::VerticalLineTo { abs, y } => {
if !*abs {
*y += pos.1;
*abs = true;
}
pos.1 = *y;
}
PathSegment::CurveTo {
abs,
x1,
y1,
x2,
y2,
x,
y,
} => {
if !*abs {
*x1 += pos.0;
*y1 += pos.1;
*x2 += pos.0;
*y2 += pos.1;
*x += pos.0;
*y += pos.1;
*abs = true;
}
pos.0 = *x;
pos.1 = *y;
}
PathSegment::SmoothCurveTo { abs, x2, y2, x, y } => {
if !*abs {
*x2 += pos.0;
*y2 += pos.1;
*x += pos.0;
*y += pos.1;
*abs = true;
}
pos.0 = *x;
pos.1 = *y;
}
PathSegment::Quadratic { abs, x1, y1, x, y } => {
if !*abs {
*x1 += pos.0;
*y1 += pos.1;
*x += pos.0;
*y += pos.1;
*abs = true;
}
pos.0 = *x;
pos.1 = *y;
}
PathSegment::SmoothQuadratic { abs, x, y } => {
// quadratic
if !*abs {
*x += pos.0;
*y += pos.1;
*abs = true;
}
pos.0 = *x;
pos.1 = *y;
}
PathSegment::EllipticalArc {
abs,
rx: _,
ry: _,
x_axis_rotation: _,
large_arc: _,
sweep: _,
x,
y,
} => {
// Ellipse
if !*abs {
*x += pos.0;
*y += pos.1;
*abs = true;
}
pos.0 = *x;
pos.1 = *y;
}
PathSegment::ClosePath { abs } => {
*abs = true;
pos = subpath_start;
}
}
}
}
/// Converts the path to relative coordinates.
#[allow(clippy::too_many_lines)]
pub fn relative(&mut self) {
let mut pos = (0.0, 0.0);
let mut subpath_start = (0.0, 0.0);
for item in &mut self.items {
match &mut item.inner {
PathSegment::MoveTo { abs, x, y } => {
if *abs {
let rel = (*x - pos.0, *y - pos.1);
*x = rel.0;
*y = rel.1;
*abs = false;
}
pos.0 += *x;
pos.1 += *y;
subpath_start = pos;
}
PathSegment::LineTo { abs, x, y } => {
if *abs {
let rel = (*x - pos.0, *y - pos.1);
*x = rel.0;
*y = rel.1;
*abs = false;
}
pos.0 += *x;
pos.1 += *y;
}
PathSegment::HorizontalLineTo { abs, x } => {
if *abs {
let rel = *x - pos.0;
*x = rel;
*abs = false;
}
pos.0 += *x;
}
PathSegment::VerticalLineTo { abs, y } => {
if *abs {
let rel = *y - pos.1;
*y = rel;
*abs = false;
}
pos.1 += *y;
}
PathSegment::CurveTo {
abs,
x1,
y1,
x2,
y2,
x,
y,
} => {
if *abs {
*x1 -= pos.0;
*y1 -= pos.1;
*x2 -= pos.0;
*y2 -= pos.1;
*x -= pos.0;
*y -= pos.1;
*abs = false;
}
pos.0 += *x;
pos.1 += *y;
}
PathSegment::SmoothCurveTo { abs, x2, y2, x, y } => {
if *abs {
*x2 -= pos.0;
*y2 -= pos.1;
*x -= pos.0;
*y -= pos.1;
*abs = false;
}
pos.0 += *x;
pos.1 += *y;
}
PathSegment::Quadratic { abs, x1, y1, x, y } => {
if *abs {
*x1 -= pos.0;
*y1 -= pos.1;
*x -= pos.0;
*y -= pos.1;
*abs = false;
}
pos.0 += *x;
pos.1 += *y;
}
PathSegment::SmoothQuadratic { abs, x, y } => {
// smooth quadratic
if *abs {
*x -= pos.0;
*y -= pos.1;
*abs = false;
}
pos.0 += *x;
pos.1 += *y;
}
PathSegment::EllipticalArc {
abs,
rx: _,
ry: _,
x_axis_rotation: _,
large_arc: _,
sweep: _,
x,
y,
} => {
if *abs {
*x -= pos.0;
*y -= pos.1;
*abs = false;
}
pos.0 += *x;
pos.1 += *y;
}
PathSegment::ClosePath { abs } => {
*abs = false;
pos = subpath_start;
}
}
}
}
/// Translates the path by (dx, dy).
pub fn translate(&mut self, dx: f64, dy: f64) {
for item in &mut self.items {
match &mut item.inner {
PathSegment::MoveTo { x, y, .. }
| PathSegment::LineTo { x, y, .. }
| PathSegment::SmoothQuadratic { x, y, .. }
| PathSegment::EllipticalArc { x, y, .. } => {
*x += dx;
*y += dy;
}
PathSegment::HorizontalLineTo { x, .. } => {
*x += dx;
}
PathSegment::VerticalLineTo { y, .. } => {
*y += dy;
}
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x,
y,
..
} => {
*x1 += dx;
*y1 += dy;
*x2 += dx;
*y2 += dy;
*x += dx;
*y += dy;
}
PathSegment::SmoothCurveTo { x2, y2, x, y, .. } => {
*x2 += dx;
*y2 += dy;
*x += dx;
*y += dy;
}
PathSegment::Quadratic { x1, y1, x, y, .. } => {
*x1 += dx;
*y1 += dy;
*x += dx;
*y += dy;
}
PathSegment::ClosePath { .. } => {} // No coordinates to modify
}
}
}
/// Scales the path by (sx, sy) relative to the origin (0, 0).
pub fn scale(&mut self, sx: f64, sy: f64) {
for item in &mut self.items {
match &mut item.inner {
PathSegment::MoveTo { x, y, .. }
| PathSegment::LineTo { x, y, .. }
| PathSegment::SmoothQuadratic { x, y, .. } => {
*x *= sx;
*y *= sy;
}
PathSegment::HorizontalLineTo { x, .. } => {
*x *= sx;
}
PathSegment::VerticalLineTo { y, .. } => {
*y *= sy;
}
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x,
y,
..
} => {
*x1 *= sx;
*y1 *= sy;
*x2 *= sx;
*y2 *= sy;
*x *= sx;
*y *= sy;
}
PathSegment::SmoothCurveTo { x2, y2, x, y, .. } => {
*x2 *= sx;
*y2 *= sy;
*x *= sx;
*y *= sy;
}
PathSegment::Quadratic { x1, y1, x, y, .. } => {
*x1 *= sx;
*y1 *= sy;
*x *= sx;
*y *= sy;
}
PathSegment::EllipticalArc { rx, ry, x, y, .. } => {
*rx *= sx.abs();
*ry *= sy.abs();
*x *= sx;
*y *= sy;
}
PathSegment::ClosePath { .. } => {}
}
}
}
/// Rotates the path around (cx, cy) by `angle_deg` degrees.
pub fn rotate(&mut self, angle_deg: f64, cx: f64, cy: f64) {
let angle_rad = angle_deg.to_radians();
let (sin, cos) = angle_rad.sin_cos();
let rotate_point = |x: &mut f64, y: &mut f64| {
let dx = *x - cx;
let dy = *y - cy;
let x_new = dx * cos - dy * sin + cx;
let y_new = dx * sin + dy * cos + cy;
*x = x_new;
*y = y_new;
};
for item in &mut self.items {
match &mut item.inner {
PathSegment::MoveTo { x, y, .. }
| PathSegment::LineTo { x, y, .. }
| PathSegment::SmoothQuadratic { x, y, .. } => {
rotate_point(x, y);
}
PathSegment::HorizontalLineTo { .. } | PathSegment::VerticalLineTo { .. } => {
// You could convert these to LineTo or skip them
}
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x,
y,
..
} => {
rotate_point(x1, y1);
rotate_point(x2, y2);
rotate_point(x, y);
}
PathSegment::SmoothCurveTo { x2, y2, x, y, .. } => {
rotate_point(x2, y2);
rotate_point(x, y);
}
PathSegment::Quadratic { x1, y1, x, y, .. } => {
rotate_point(x1, y1);
rotate_point(x, y);
}
PathSegment::EllipticalArc {
x,
y,
x_axis_rotation,
..
} => {
rotate_point(x, y);
*x_axis_rotation += angle_deg;
}
#[allow(clippy::match_same_arms)]
PathSegment::ClosePath { .. } => {
// close path
}
}
}
}
/// Rounds all coordinates and values in the path to the given number of decimal places.
pub fn round(&mut self, decimal: u64) {
for item in &mut self.items {
match &mut item.inner {
PathSegment::MoveTo { x, y, .. }
| PathSegment::LineTo { x, y, .. }
| PathSegment::SmoothQuadratic { x, y, .. } => {
*x = round_to(*x, decimal);
*y = round_to(*y, decimal);
}
PathSegment::HorizontalLineTo { x, .. } => {
*x = round_to(*x, decimal);
}
PathSegment::VerticalLineTo { y, .. } => {
*y = round_to(*y, decimal);
}
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x,
y,
..
} => {
*x1 = round_to(*x1, decimal);
*y1 = round_to(*y1, decimal);
*x2 = round_to(*x2, decimal);
*y2 = round_to(*y2, decimal);
*x = round_to(*x, decimal);
*y = round_to(*y, decimal);
}
PathSegment::SmoothCurveTo { x2, y2, x, y, .. } => {
*x2 = round_to(*x2, decimal);
*y2 = round_to(*y2, decimal);
*x = round_to(*x, decimal);
*y = round_to(*y, decimal);
}
PathSegment::Quadratic { x1, y1, x, y, .. } => {
*x1 = round_to(*x1, decimal);
*y1 = round_to(*y1, decimal);
*x = round_to(*x, decimal);
*y = round_to(*y, decimal);
}
PathSegment::EllipticalArc {
rx,
ry,
x_axis_rotation,
x,
y,
..
} => {
*rx = round_to(*rx, decimal);
*ry = round_to(*ry, decimal);
*x_axis_rotation = round_to(*x_axis_rotation, decimal);
*x = round_to(*x, decimal);
*y = round_to(*y, decimal);
}
PathSegment::ClosePath { .. } => {}
}
}
}
/// Toggles the coordinate type (absolute/relative) of the path segment at the given index.
#[allow(clippy::too_many_lines)]
#[allow(unused_assignments)] // broken lint
pub fn toggle_coord_type_at(&mut self, index: usize) {
let mut pos = (0.0, 0.0);
let mut subpath_start = (0.0, 0.0);
// We need to recalculate the position up to item i
for (j, item) in self.items.iter_mut().enumerate() {
if j == index {
match &mut item.inner {
PathSegment::MoveTo { abs, x, y } => {
if *abs {
*x -= pos.0;
*y -= pos.1;
} else {
*x += pos.0;
*y += pos.1;
}
*abs = !*abs;
pos.0 = if *abs { *x } else { pos.0 + *x };
pos.1 = if *abs { *y } else { pos.1 + *y };
// subpath_start = pos;
}
PathSegment::LineTo { abs, x, y } => {
// line to
if *abs {
*x -= pos.0;
*y -= pos.1;
} else {
*x += pos.0;
*y += pos.1;
}
*abs = !*abs;
pos.0 = if *abs { *x } else { pos.0 + *x };
pos.1 = if *abs { *y } else { pos.1 + *y };
}
PathSegment::HorizontalLineTo { abs, x } => {
if *abs {
*x -= pos.0;
} else {
*x += pos.0;
}
*abs = !*abs;
pos.0 = if *abs { *x } else { pos.0 + *x };
}
PathSegment::VerticalLineTo { abs, y } => {
if *abs {
*y -= pos.1;
} else {
*y += pos.1;
}
*abs = !*abs;
pos.1 = if *abs { *y } else { pos.1 + *y };
}
PathSegment::CurveTo {
abs,
x1,
y1,
x2,
y2,
x,
y,
} => {
if *abs {
*x1 -= pos.0;
*y1 -= pos.1;
*x2 -= pos.0;
*y2 -= pos.1;
*x -= pos.0;
*y -= pos.1;
} else {
*x1 += pos.0;
*y1 += pos.1;
*x2 += pos.0;
*y2 += pos.1;
*x += pos.0;
*y += pos.1;
}
*abs = !*abs;
pos.0 = if *abs { *x } else { pos.0 + *x };
pos.1 = if *abs { *y } else { pos.1 + *y };
}
PathSegment::SmoothCurveTo { abs, x2, y2, x, y } => {
if *abs {
*x2 -= pos.0;
*y2 -= pos.1;
*x -= pos.0;
*y -= pos.1;
} else {
*x2 += pos.0;
*y2 += pos.1;
*x += pos.0;
*y += pos.1;
}
*abs = !*abs;
pos.0 = if *abs { *x } else { pos.0 + *x };
pos.1 = if *abs { *y } else { pos.1 + *y };
}
PathSegment::Quadratic { abs, x1, y1, x, y } => {
if *abs {
*x1 -= pos.0;
*y1 -= pos.1;
*x -= pos.0;
*y -= pos.1;
} else {
*x1 += pos.0;
*y1 += pos.1;
*x += pos.0;
*y += pos.1;
}
*abs = !*abs;
pos.0 = if *abs { *x } else { pos.0 + *x };
pos.1 = if *abs { *y } else { pos.1 + *y };
}
PathSegment::SmoothQuadratic { abs, x, y } => {
// smooth quadratic
if *abs {
*x -= pos.0;
*y -= pos.1;
} else {
*x += pos.0;
*y += pos.1;
}
*abs = !*abs;
pos.0 = if *abs { *x } else { pos.0 + *x };
pos.1 = if *abs { *y } else { pos.1 + *y };
}
PathSegment::EllipticalArc { abs, x, y, .. } => {
if *abs {
*x -= pos.0;
*y -= pos.1;
} else {
*x += pos.0;
*y += pos.1;
}
*abs = !*abs;
pos.0 = if *abs { *x } else { pos.0 + *x };
pos.1 = if *abs { *y } else { pos.1 + *y };
}
PathSegment::ClosePath { abs } => {
*abs = !*abs;
// pos = subpath_start;
}
}
break;
}
// Update the position so we know the current point at index i
match &item.inner {
PathSegment::MoveTo { abs, x, y } => {
if *abs {
pos = (*x, *y);
} else {
pos.0 += *x;
pos.1 += *y;
}
subpath_start = pos;
}
PathSegment::LineTo { abs, x, y }
| PathSegment::SmoothQuadratic { abs, x, y }
| PathSegment::Quadratic {
abs,
x1: _,
y1: _,
x,
y,
}
| PathSegment::SmoothCurveTo {
abs,
x2: _,
y2: _,
x,
y,
}
| PathSegment::CurveTo {
abs,
x1: _,
y1: _,
x2: _,
y2: _,
x,
y,
}
| PathSegment::EllipticalArc { abs, x, y, .. } => {
if *abs {
pos = (*x, *y);
} else {
pos.0 += *x;
pos.1 += *y;
}
}
PathSegment::HorizontalLineTo { abs, x } => {
pos.0 = if *abs { *x } else { pos.0 + *x };
}
PathSegment::VerticalLineTo { abs, y } => {
pos.1 = if *abs { *y } else { pos.1 + *y };
}
PathSegment::ClosePath { .. } => {
pos = subpath_start;
}
}
}
}
/// Try replace element
/// # Errors
/// Error if it's invalid
pub fn try_replace_element_at(&mut self, idx: usize, val: &str) -> Result<String, String> {
if idx < self.items.len() {
let mut result = String::new();
for (index, one_item) in self.items.iter().enumerate() {
let segment_str = if index == idx {
val
} else {
&one_item.to_string()
};
result.push_str(segment_str);
}
match SvgPath::parse(&result) {
Ok(path) => {
return Ok(path.to_string());
}
Err(e) => {
return Err(e);
}
}
}
Err("Invalid index".to_string())
}
/// Reverse the path
#[allow(clippy::too_many_lines)]
#[must_use]
pub fn reversed(&self) -> SvgPath {
let mut path = self.clone();
path.absolute();
let mut contours: Vec<Vec<SvgItem>> = Vec::new();
let mut current = Vec::new();
for item in path.items {
match item.inner {
PathSegment::MoveTo { .. } => {
if !current.is_empty() {
contours.push(current);
current = Vec::new();
}
current.push(item);
}
PathSegment::ClosePath { .. } => {
current.push(item);
contours.push(current);
current = Vec::new();
}
_ => current.push(item),
}
}
if !current.is_empty() {
contours.push(current);
}
let mut out = Vec::new();
let mut carry_point = (0.0, 0.0);
let mut reversed_contours = Vec::new();
for contour in &contours {
let (rev, end_point) = Self::reverse_contour(contour, carry_point);
carry_point = end_point;
reversed_contours.push(rev);
}
for rev in reversed_contours.into_iter().rev() {
out.extend(rev);
}
SvgPath { items: out }
}
/// Reverse the contours
#[allow(clippy::too_many_lines)]
fn reverse_contour(contour: &[SvgItem], carry_point: (f64, f64)) -> (Vec<SvgItem>, (f64, f64)) {
if contour.is_empty() {
return (vec![], carry_point);
}
let mut pts = Vec::<(f64, f64)>::new();
let mut start = carry_point;
let mut cur = carry_point;
pts.push(cur);
let mut forward_ctrl2: Vec<Option<(f64, f64)>> = vec![None];
for item in contour {
match item.inner {
PathSegment::MoveTo { x, y, .. } => {
start = (x, y);
cur = start;
forward_ctrl2.push(None);
}
PathSegment::LineTo { x, y, .. }
| PathSegment::Quadratic { x, y, .. }
| PathSegment::SmoothQuadratic { x, y, .. }
| PathSegment::EllipticalArc { x, y, .. } => {
cur = (x, y);
forward_ctrl2.push(None);
}
PathSegment::CurveTo { x, y, x2, y2, .. }
| PathSegment::SmoothCurveTo { x, y, x2, y2, .. } => {
cur = (x, y);
forward_ctrl2.push(Some((x2, y2)));
}
PathSegment::HorizontalLineTo { x, .. } => {
cur.0 = x;
forward_ctrl2.push(None);
}
PathSegment::VerticalLineTo { y, .. } => {
cur.1 = y;
forward_ctrl2.push(None);
}
PathSegment::ClosePath { .. } => {
cur = start;
forward_ctrl2.push(None);
}
}
pts.push(cur);
}
let closed = matches!(
contour.last().map(|c| &c.inner),
Some(PathSegment::ClosePath { .. })
);
// pts[0] = contour start (carried-in or from this contour's own MoveTo)
// pts[k] = point after contour[k-1] runs, for k = 1..=contour.len()
let last_idx = if closed { pts.len() - 2 } else { pts.len() - 1 };
let forward_end_point = pts[contour.len()];
if last_idx == 0 {
// Degenerate contour (e.g. a lone MoveTo): nothing to reverse.
return (vec![], forward_end_point);
}
let mut out = Vec::new();
// -------------------------
// 2. start reversed path
// -------------------------
out.push(SvgItem {
inner: PathSegment::MoveTo {
abs: true,
x: pts[last_idx].0,
y: pts[last_idx].1,
},
});
// -------------------------
// 3. reverse walk
// -------------------------
// `i` indexes into `contour` (0-based) and corresponds to pts[i+1]
// being its end point and pts[i] being its start point.
for i in (0..last_idx).rev() {
let seg_start = pts[i]; // forward start of contour[i]
let seg_end = pts[i + 1]; // forward end of contour[i] == prev in old code
match contour[i].inner {
PathSegment::LineTo { .. }
| PathSegment::HorizontalLineTo { .. }
| PathSegment::VerticalLineTo { .. } => {
out.push(SvgItem {
inner: PathSegment::LineTo {
abs: true,
x: seg_start.0,
y: seg_start.1,
},
});
}
PathSegment::CurveTo { x1, y1, x2, y2, .. } => {
out.push(SvgItem {
inner: PathSegment::CurveTo {
abs: true,
x1: x2,
y1: y2,
x2: x1,
y2: y1,
x: seg_start.0,
y: seg_start.1,
},
});
}
PathSegment::SmoothCurveTo { x2, y2, .. } => {
let prev_ctrl2 = forward_ctrl2[i].unwrap_or(seg_start);
let rx = 2.0 * seg_start.0 - prev_ctrl2.0;
let ry = 2.0 * seg_start.1 - prev_ctrl2.1;
out.push(SvgItem {
inner: PathSegment::CurveTo {
abs: true,
x1: x2,
y1: y2,
x2: rx,
y2: ry,
x: seg_start.0,
y: seg_start.1,
},
});
}
PathSegment::Quadratic { x1, y1, .. } => {
out.push(SvgItem {
inner: PathSegment::Quadratic {
abs: true,
x1,
y1,
x: seg_start.0,
y: seg_start.1,
},
});
}
PathSegment::EllipticalArc {
rx,
ry,
x_axis_rotation,
large_arc,
sweep,
..
} => {
out.push(SvgItem {
inner: PathSegment::EllipticalArc {
abs: true,
rx,
ry,
x_axis_rotation,
large_arc,
sweep: !sweep,
x: seg_start.0,
y: seg_start.1,
},
});
}
_ => {}
}
let _ = seg_end; // (kept for clarity; equals pts[i+1])
}
if closed {
out.push(SvgItem {
inner: PathSegment::ClosePath { abs: true },
});
}
(out, forward_end_point)
}
}
impl Display for SvgPath {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut result = String::new();
for item in &self.items {
let segment_str = item.to_string();
if !segment_str.is_empty() {
result.push_str(&segment_str);
}
}
write!(f, "{result}")
}
}
/// Convert a polyline string to an SVG path data string
/// # Errors
/// Fails if fails to write to `path_data`
pub fn polyline_to_path(points: &str) -> Result<String, std::fmt::Error> {
let mut path_data = String::new();
let mut first = true;
let all_points = points.split_whitespace().collect::<Vec<&str>>();
for idx in (0..all_points.len()).step_by(2) {
let coords1 = all_points[idx];
let coords2 = all_points[idx + 1];
if first {
write!(path_data, "M {coords1} {coords2}")?;
first = false;
} else {
write!(path_data, " L {coords1} {coords2}")?;
}
}
Ok(path_data)
}
/// Convert a line string to an SVG path data string
#[must_use]
pub fn line_to_path(x1: &str, y1: &str, x2: &str, y2: &str) -> String {
format!("M {x1} {y1} L {x2} {y2}")
}
/// Convert a polygon string to an SVG path data string
/// # Errors
/// Fails if fails to write to `path_data`
pub fn polygon_to_path(points: &str) -> Result<String, std::fmt::Error> {
let mut path_data = String::new();
let mut first = true;
let all_points = points.split_whitespace().collect::<Vec<&str>>();
for idx in (0..all_points.len()).step_by(2) {
let coords1 = all_points[idx];
let coords2 = all_points[idx + 1];
if first {
write!(path_data, "M {coords1} {coords2}")?;
first = false;
} else {
write!(path_data, " L {coords1} {coords2}")?;
}
}
path_data.push('Z'); // Close the polygon
Ok(path_data)
}
/// Convert a svg rect to a path data string
#[must_use]
pub fn rect_to_path(x: &str, y: &str, width: &str, height: &str) -> String {
format!("M {x} {y} h {width} v {height} h -{width} Z")
}
/// Convert a circle to a path data string
/// # Errors
/// Return error if fail to parse
pub fn circle_to_path(cx: &str, cy: &str, r: &str) -> Result<String, String> {
let r = r
.parse::<f32>()
.map_err(|e| format!("Error formatting r : {e}"))?;
let double_r = r * 2.0;
let formatted =
format!("M {cx} {cy} m {r}, 0a {r},{r} 0 1,0 -{double_r},0 a {r},{r} 0 1,0 {double_r},0");
Ok(formatted)
}
/// Convert an ellipse to a path data string
/// # Errors
/// Return error if fail to parse
pub fn ellipse_to_path(cx: &str, cy: &str, rx: &str, ry: &str) -> Result<String, String> {
let rx = rx
.parse::<f32>()
.map_err(|e| format!("Error formatting rx : {e}"))?;
let cx = cx
.parse::<f32>()
.map_err(|e| format!("Error formatting cx : {e}"))?;
let double_rx = rx * 2.0;
let start_x = cx - rx;
let formatted =
format!("M{start_x} {cy} a{rx} {ry} 0 1,0 {double_rx} 0 a{rx} {ry} 0 1,0 -{double_rx} 0 Z");
Ok(formatted)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn test_path_parsed() {
let mut parsed = SvgPath::parse("M174 270C113 271 84 237 80 207 78 189 79 172 76 172 46 154 34 142 21 109-4 49 45 62 55 66 71 72 79 83 90 94 103 104 160 104 174 104ZM110 150C104 153 98 160 95 166S92 184 95 190C103 208 126 216 142 205 149 200 153 195 155 188 161 172 150 153 133 147 128 145 115 147 110 150ZM174 270C236 271 265 237 269 207 271 189 270 172 273 172 303 154 315 142 328 109 353 49 304 62 294 66 278 72 270 83 259 94 246 104 189 104 174 104ZM239 150C245 153 251 160 254 166S257 184 254 190C246 208 223 216 207 205 200 200 196 195 194 188 188 172 199 153 216 147 221 145 234 147 239 150Z").unwrap();
assert_eq!(
parsed.to_string(),
"M174 270C113 271 84 237 80 207C78 189 79 172 76 172C46 154 34 142 21 109C-4 49 45 62 55 66C71 72 79 83 90 94C103 104 160 104 174 104ZM110 150C104 153 98 160 95 166S92 184 95 190C103 208 126 216 142 205C149 200 153 195 155 188C161 172 150 153 133 147C128 145 115 147 110 150ZM174 270C236 271 265 237 269 207C271 189 270 172 273 172C303 154 315 142 328 109C353 49 304 62 294 66C278 72 270 83 259 94C246 104 189 104 174 104ZM239 150C245 153 251 160 254 166S257 184 254 190C246 208 223 216 207 205C200 200 196 195 194 188C188 172 199 153 216 147C221 145 234 147 239 150Z"
);
parsed.absolute();
assert_eq!(
parsed.to_string(),
"M174 270C113 271 84 237 80 207C78 189 79 172 76 172C46 154 34 142 21 109C-4 49 45 62 55 66C71 72 79 83 90 94C103 104 160 104 174 104ZM110 150C104 153 98 160 95 166S92 184 95 190C103 208 126 216 142 205C149 200 153 195 155 188C161 172 150 153 133 147C128 145 115 147 110 150ZM174 270C236 271 265 237 269 207C271 189 270 172 273 172C303 154 315 142 328 109C353 49 304 62 294 66C278 72 270 83 259 94C246 104 189 104 174 104ZM239 150C245 153 251 160 254 166S257 184 254 190C246 208 223 216 207 205C200 200 196 195 194 188C188 172 199 153 216 147C221 145 234 147 239 150Z"
);
parsed.relative();
assert_eq!(
parsed.to_string(),
"m174 270c-61 1 -90 -33 -94 -63c-2 -18 -1 -35 -4 -35c-30 -18 -42 -30 -55 -63c-25 -60 24 -47 34 -43c16 6 24 17 35 28c13 10 70 10 84 10zm-64 -120c-6 3 -12 10 -15 16s-3 18 0 24c8 18 31 26 47 15c7 -5 11 -10 13 -17c6 -16 -5 -35 -22 -41c-5 -2 -18 0 -23 3zm64 120c62 1 91 -33 95 -63c2 -18 1 -35 4 -35c30 -18 42 -30 55 -63c25 -60 -24 -47 -34 -43c-16 6 -24 17 -35 28c-13 10 -70 10 -85 10zm65 -120c6 3 12 10 15 16s3 18 0 24c-8 18 -31 26 -47 15c-7 -5 -11 -10 -13 -17c-6 -16 5 -35 22 -41c5 -2 18 0 23 3z"
);
let mut parsed_relative = SvgPath::parse("m174 270c-61 1 -90 -33 -94 -63c-2 -18 -1 -35 -4 -35c-30 -18 -42 -30 -55 -63c-25 -60 24 -47 34 -43c16 6 24 17 35 28c13 10 70 10 84 10zm-64 -120c-6 3 -12 10 -15 16s-3 18 0 24c8 18 31 26 47 15c7 -5 11 -10 13 -17c6 -16 -5 -35 -22 -41c-5 -2 -18 0 -23 3zm64 120c62 1 91 -33 95 -63c2 -18 1 -35 4 -35c30 -18 42 -30 55 -63c25 -60 -24 -47 -34 -43c-16 6 -24 17 -35 28c-13 10 -70 10 -85 10zm65 -120c6 3 12 10 15 16s3 18 0 24c-8 18 -31 26 -47 15c-7 -5 -11 -10 -13 -17c-6 -16 5 -35 22 -41c5 -2 18 0 23 3z").unwrap();
assert_eq!(
parsed_relative.to_string(),
"m174 270c-61 1 -90 -33 -94 -63c-2 -18 -1 -35 -4 -35c-30 -18 -42 -30 -55 -63c-25 -60 24 -47 34 -43c16 6 24 17 35 28c13 10 70 10 84 10zm-64 -120c-6 3 -12 10 -15 16s-3 18 0 24c8 18 31 26 47 15c7 -5 11 -10 13 -17c6 -16 -5 -35 -22 -41c-5 -2 -18 0 -23 3zm64 120c62 1 91 -33 95 -63c2 -18 1 -35 4 -35c30 -18 42 -30 55 -63c25 -60 -24 -47 -34 -43c-16 6 -24 17 -35 28c-13 10 -70 10 -85 10zm65 -120c6 3 12 10 15 16s3 18 0 24c-8 18 -31 26 -47 15c-7 -5 -11 -10 -13 -17c-6 -16 5 -35 22 -41c5 -2 18 0 23 3z"
);
parsed_relative.absolute();
assert_eq!(
parsed_relative.to_string(),
"M174 270C113 271 84 237 80 207C78 189 79 172 76 172C46 154 34 142 21 109C-4 49 45 62 55 66C71 72 79 83 90 94C103 104 160 104 174 104ZM110 150C104 153 98 160 95 166S92 184 95 190C103 208 126 216 142 205C149 200 153 195 155 188C161 172 150 153 133 147C128 145 115 147 110 150ZM174 270C236 271 265 237 269 207C271 189 270 172 273 172C303 154 315 142 328 109C353 49 304 62 294 66C278 72 270 83 259 94C246 104 189 104 174 104ZM239 150C245 153 251 160 254 166S257 184 254 190C246 208 223 216 207 205C200 200 196 195 194 188C188 172 199 153 216 147C221 145 234 147 239 150Z"
);
}
#[test]
fn test_toggle_coord_type_at() {
let mut parsed = SvgPath::parse("M174 270C113 271 84 237 80 207 78 189 79 172 76 172 46 154 34 142 21 109-4 49 45 62 55 66 71 72 79 83 90 94 103 104 160 104 174 104Z").unwrap();
let len = parsed.items.len();
assert_eq!(len, 8);
for i in 0..len {
parsed.toggle_coord_type_at(i);
}
assert_eq!(
parsed.to_string(),
"m174 270c-61 1 -90 -33 -94 -63c-2 -18 -1 -35 -4 -35c-30 -18 -42 -30 -55 -63c-25 -60 24 -47 34 -43c16 6 24 17 35 28c13 10 70 10 84 10z"
);
}
#[test]
fn test_polyline_to_path() {
// <polyline points="0,100 50,25 50,75 100,0" />
let path_data = polyline_to_path("0,100 50,25 50,75 100,0").unwrap();
assert_eq!(path_data, "M 0,100 50,25 L 50,75 100,0");
}
#[test]
fn test_line_to_path() {
// <line x1="0" y1="80" x2="100" y2="20" />
let path_data = line_to_path("0", "0", "80", "20");
assert_eq!(path_data, "M 0 0 L 80 20");
}
#[test]
fn test_polygon_to_path() {
// <polygon points="0,100 50,25 50,75 100,0" />
let path_data = polygon_to_path("0,100 50,25 50,75 100,0").unwrap();
assert_eq!(path_data, "M 0,100 50,25 L 50,75 100,0Z");
}
#[test]
fn test_circle_to_path() {
// <circle cx="50" cy="50" r="50" />
let path_data = circle_to_path("50", "50", "50").unwrap();
assert_eq!(
path_data,
"M 50 50 m 50, 0a 50,50 0 1,0 -100,0 a 50,50 0 1,0 100,0"
);
}
#[test]
fn test_ellipse_to_path() {
// <ellipse cx="100" cy="50" rx="100" ry="50" />
let path_data = ellipse_to_path("100", "50", "100", "50").unwrap();
assert_eq!(
path_data,
"M0 50 a100 50 0 1,0 200 0 a100 50 0 1,0 -200 0 Z"
);
}
#[test]
fn test_path_reverse() {
let parsed = SvgPath::parse("M174 270C113 271 84 237 80 207 78 189 79 172 76 172 46 154 34 142 21 109-4 49 45 62 55 66 71 72 79 83 90 94 103 104 160 104 174 104Z").unwrap();
eprintln!("{parsed}");
assert_eq!(
parsed.reversed().to_string(),
"M174 104C160 104 103 104 90 94C79 83 71 72 55 66C45 62 -4 49 21 109C34 142 46 154 76 172C79 172 78 189 80 207C84 237 113 271 174 270Z"
);
}
#[test]
fn test_path_reversed_1() {
let parsed = SvgPath::parse("M0 50 a100 50 0 1,0 200 0 a100 50 0 1,0 -200 0 Z").unwrap();
eprintln!("{parsed}");
assert_eq!(
parsed.reversed().to_string(),
"M0 50A100 50 0 1 1 200 50A100 50 0 1 1 0 50Z"
);
}
#[test]
fn test_path_reversed_2() {
let parsed = SvgPath::parse("M 5 2 L 9 2 L 9 6 Z L 1 7 L 4 9 Z L 6 9 L 8 7 Z").unwrap();
eprintln!("{parsed}");
assert_eq!(
parsed.reversed().to_string(),
"M8 7L6 9L5 2ZM4 9L1 7L5 2ZM9 6L9 2L5 2Z"
);
}
#[test]
fn test_path_reversed_3() {
let parsed = SvgPath::parse("M133 185C128 185 126 183 126 179S128 172 133 172 140 175 140 179 137 185 133 185ZM116 202C125 205 134 204 141 197 149 190 152 181 149 171 147 165 141 158 136 155 116 145 94 163 100 184 102 191 109 199 116 202Z").unwrap();
eprintln!("{parsed}");
assert_eq!(
parsed.reversed().to_string(),
"M116 202C109 199 102 191 100 184C94 163 116 145 136 155C141 158 147 165 149 171C152 181 149 190 141 197C134 204 125 205 116 202ZM133 185C137 185 140 183 140 179C140 175 138 172 133 172C128 172 126 175 126 179C126 183 128 185 133 185Z"
);
}
}