use crate::compat::Vec;
pub const MAX_PATH_POINTS: usize = 1024;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Segment {
Line(Point),
Quad {
ctrl: Point,
to: Point,
},
Cubic {
ctrl1: Point,
ctrl2: Point,
to: Point,
},
}
impl Segment {
pub fn end(&self) -> Point {
match *self {
Segment::Line(to) | Segment::Quad { to, .. } | Segment::Cubic { to, .. } => to,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: f32,
pub y: f32,
}
impl Point {
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Subpath {
pub start: Point,
pub segments: Vec<Segment>,
pub closed: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathError {
UnknownCommand(char),
ExpectedNumber,
SegmentsBeforeMove,
TooManyPoints,
}
pub fn parse(input: &str) -> Result<Vec<Subpath>, PathError> {
let mut parser = Parser { bytes: input.as_bytes(), at: 0 };
let mut subpaths: Vec<Subpath> = Vec::new();
let mut current: Option<Subpath> = None;
let mut pen = Point::new(0.0, 0.0);
let mut subpath_start = Point::new(0.0, 0.0);
let mut total_points = 0usize;
let mut last_cubic_ctrl: Option<Point> = None;
let mut last_quad_ctrl: Option<Point> = None;
parser.skip_separators();
while !parser.at_end() {
let command = parser.read_command()?;
let relative = command.is_ascii_lowercase();
match command.to_ascii_uppercase() {
'Z' => {
let Some(subpath) = current.as_mut() else {
return Err(PathError::SegmentsBeforeMove);
};
subpath.closed = true;
pen = subpath_start;
parser.skip_separators();
continue;
}
'M' => {
if let Some(done) = current.take() {
subpaths.push(done);
}
let point = parser.read_point()?;
pen = if relative { Point::new(pen.x + point.x, pen.y + point.y) } else { point };
subpath_start = pen;
last_cubic_ctrl = None;
last_quad_ctrl = None;
current = Some(Subpath { start: pen, segments: Vec::new(), closed: false });
total_points += 1;
if total_points > MAX_PATH_POINTS {
return Err(PathError::TooManyPoints);
}
while parser.number_pending() {
let segments = parse_segments('L', &mut parser, relative, pen, pen)?;
total_points += segments.len();
if total_points > MAX_PATH_POINTS {
return Err(PathError::TooManyPoints);
}
let subpath = current.as_mut().expect("a move opened a subpath");
for segment in segments {
pen = segment.end();
last_cubic_ctrl = None;
last_quad_ctrl = None;
subpath.segments.push(segment);
}
}
parser.skip_separators();
continue;
}
_ => {}
}
if current.is_none() {
return Err(PathError::SegmentsBeforeMove);
}
loop {
let segments = parse_segments_with_reflection(
command,
&mut parser,
relative,
pen,
last_cubic_ctrl,
last_quad_ctrl,
)?;
total_points += segments.len();
if total_points > MAX_PATH_POINTS {
return Err(PathError::TooManyPoints);
}
let subpath = current.as_mut().expect("checked above");
for segment in segments {
pen = segment.end();
match segment {
Segment::Cubic { ctrl2, .. } => last_cubic_ctrl = Some(ctrl2),
Segment::Quad { ctrl, .. } => last_quad_ctrl = Some(ctrl),
Segment::Line(_) => {}
}
if !matches!(segment, Segment::Cubic { .. }) {
last_cubic_ctrl = None;
}
if !matches!(segment, Segment::Quad { .. }) {
last_quad_ctrl = None;
}
subpath.segments.push(segment);
}
if !parser.number_pending() {
break;
}
if parser.command_pending() {
break;
}
}
parser.skip_separators();
}
if let Some(done) = current.take() {
subpaths.push(done);
}
Ok(subpaths)
}
fn parse_segment(
command: char,
parser: &mut Parser<'_>,
relative: bool,
pen: Point,
cursor_ctrl: Point,
) -> Result<Segment, PathError> {
let pair = |parser: &mut Parser<'_>| -> Result<Point, PathError> {
let point = parser.read_point()?;
if relative {
Ok(Point::new(pen.x + point.x, pen.y + point.y))
} else {
Ok(point)
}
};
match command.to_ascii_uppercase() {
'L' => Ok(Segment::Line(pair(parser)?)),
'H' => {
let value = parser.read_number()?;
Ok(Segment::Line(Point::new(if relative { pen.x + value } else { value }, pen.y)))
}
'V' => {
let value = parser.read_number()?;
Ok(Segment::Line(Point::new(pen.x, if relative { pen.y + value } else { value })))
}
'C' => {
let ctrl1 = pair(parser)?;
let ctrl2 = pair(parser)?;
let to = pair(parser)?;
Ok(Segment::Cubic { ctrl1, ctrl2, to })
}
'S' => {
let ctrl1 = cursor_ctrl;
let ctrl2 = pair(parser)?;
let to = pair(parser)?;
Ok(Segment::Cubic { ctrl1, ctrl2, to })
}
'Q' => {
let ctrl = pair(parser)?;
let to = pair(parser)?;
Ok(Segment::Quad { ctrl, to })
}
'T' => {
let ctrl = cursor_ctrl;
let to = pair(parser)?;
Ok(Segment::Quad { ctrl, to })
}
'A' => Err(PathError::UnknownCommand('A')),
other => Err(PathError::UnknownCommand(other)),
}
}
fn parse_segments_with_reflection(
command: char,
parser: &mut Parser<'_>,
relative: bool,
pen: Point,
last_cubic_ctrl: Option<Point>,
last_quad_ctrl: Option<Point>,
) -> Result<Vec<Segment>, PathError> {
let cursor_ctrl = match command.to_ascii_uppercase() {
'S' => reflect(last_cubic_ctrl, pen),
'T' => reflect(last_quad_ctrl, pen),
_ => pen,
};
parse_segments(command, parser, relative, pen, cursor_ctrl)
}
fn reflect(control: Option<Point>, pen: Point) -> Point {
match control {
Some(control) => Point::new(2.0 * pen.x - control.x, 2.0 * pen.y - control.y),
None => pen,
}
}
fn parse_segments(
command: char,
parser: &mut Parser<'_>,
relative: bool,
pen: Point,
cursor_ctrl: Point,
) -> Result<Vec<Segment>, PathError> {
match command.to_ascii_uppercase() {
'A' => {
let rx = parser.read_number()?;
let ry = parser.read_number()?;
let x_rotation = parser.read_number()?;
let large_arc = parser.read_flag()?;
let sweep = parser.read_flag()?;
let to = if relative {
let point = parser.read_point()?;
Point::new(pen.x + point.x, pen.y + point.y)
} else {
parser.read_point()?
};
Ok(arc_to_curves(pen, rx, ry, x_rotation, large_arc, sweep, to))
}
_ => Ok(crate::compat::vec![parse_segment(command, parser, relative, pen, cursor_ctrl)?]),
}
}
fn arc_to_curves(
from: Point,
rx: f32,
ry: f32,
x_rotation_degrees: f32,
large_arc: bool,
sweep: bool,
to: Point,
) -> Vec<Segment> {
if from == to {
return Vec::new();
}
let mut rx = rx.abs();
let mut ry = ry.abs();
if rx <= f32::EPSILON || ry <= f32::EPSILON {
return crate::compat::vec![Segment::Line(to)];
}
let phi = x_rotation_degrees.to_radians();
let (sin_phi, cos_phi) = phi.sin_cos();
let dx = (from.x - to.x) * 0.5;
let dy = (from.y - to.y) * 0.5;
let x1 = cos_phi * dx + sin_phi * dy;
let y1 = -sin_phi * dx + cos_phi * dy;
let lambda = (x1 * x1) / (rx * rx) + (y1 * y1) / (ry * ry);
if lambda > 1.0 {
let scale = lambda.sqrt();
rx *= scale;
ry *= scale;
}
let rx2 = rx * rx;
let ry2 = ry * ry;
let numerator = (rx2 * ry2) - (rx2 * y1 * y1) - (ry2 * x1 * x1);
let denominator = (rx2 * y1 * y1) + (ry2 * x1 * x1);
let mut factor =
if denominator <= f32::EPSILON { 0.0 } else { (numerator / denominator).max(0.0).sqrt() };
if large_arc == sweep {
factor = -factor;
}
let cx1 = factor * rx * y1 / ry;
let cy1 = -factor * ry * x1 / rx;
let cx = cos_phi * cx1 - sin_phi * cy1 + (from.x + to.x) * 0.5;
let cy = sin_phi * cx1 + cos_phi * cy1 + (from.y + to.y) * 0.5;
let ux = (x1 - cx1) / rx;
let uy = (y1 - cy1) / ry;
let vx = (-x1 - cx1) / rx;
let vy = (-y1 - cy1) / ry;
let start = uy.atan2(ux);
let mut delta = (ux * vy - uy * vx).atan2(ux * vx + uy * vy);
if !sweep && delta > 0.0 {
delta -= core::f32::consts::TAU;
} else if sweep && delta < 0.0 {
delta += core::f32::consts::TAU;
}
let steps = ((delta.abs() / core::f32::consts::FRAC_PI_2).ceil() as usize).max(1);
let step = delta / steps as f32;
let alpha = (4.0 / 3.0) * (step * 0.25).tan();
let mut curves = Vec::with_capacity(steps);
let mut angle = start;
for index in 0..steps {
let next = angle + step;
let (sin_a, cos_a) = angle.sin_cos();
let (sin_b, cos_b) = next.sin_cos();
let ctrl1 = ellipse_point(
cx,
cy,
rx,
ry,
sin_phi,
cos_phi,
cos_a - alpha * sin_a,
sin_a + alpha * cos_a,
);
let ctrl2 = ellipse_point(
cx,
cy,
rx,
ry,
sin_phi,
cos_phi,
cos_b + alpha * sin_b,
sin_b - alpha * cos_b,
);
let end = ellipse_point(cx, cy, rx, ry, sin_phi, cos_phi, cos_b, sin_b);
let end = if index + 1 == steps { to } else { end };
curves.push(Segment::Cubic { ctrl1, ctrl2, to: end });
angle = next;
}
curves
}
#[allow(clippy::too_many_arguments)]
fn ellipse_point(
cx: f32,
cy: f32,
rx: f32,
ry: f32,
sin_phi: f32,
cos_phi: f32,
unit_x: f32,
unit_y: f32,
) -> Point {
let x = rx * unit_x;
let y = ry * unit_y;
Point::new(cx + cos_phi * x - sin_phi * y, cy + sin_phi * x + cos_phi * y)
}
struct Parser<'a> {
bytes: &'a [u8],
at: usize,
}
impl Parser<'_> {
fn at_end(&self) -> bool {
self.at >= self.bytes.len()
}
fn number_pending(&self) -> bool {
let mut at = self.at;
while let Some(byte) = self.bytes.get(at) {
if byte.is_ascii_whitespace() || *byte == b',' {
at += 1;
} else {
break;
}
}
matches!(
self.bytes.get(at),
Some(b) if b.is_ascii_digit() || matches!(b, b'+' | b'-' | b'.')
)
}
fn command_pending(&self) -> bool {
let mut at = self.at;
while let Some(byte) = self.bytes.get(at) {
if byte.is_ascii_whitespace() || *byte == b',' {
at += 1;
} else {
break;
}
}
matches!(self.bytes.get(at), Some(b) if b.is_ascii_alphabetic())
}
fn read_command(&mut self) -> Result<char, PathError> {
self.skip_separators();
let byte = self.bytes.get(self.at).copied().ok_or(PathError::ExpectedNumber)?;
let command = char::from(byte);
if !matches!(
command,
'M' | 'm'
| 'L'
| 'l'
| 'H'
| 'h'
| 'V'
| 'v'
| 'C'
| 'c'
| 'S'
| 's'
| 'Q'
| 'q'
| 'T'
| 't'
| 'A'
| 'a'
| 'Z'
| 'z'
) {
return Err(PathError::UnknownCommand(command));
}
self.at += 1;
Ok(command)
}
fn read_point(&mut self) -> Result<Point, PathError> {
let x = self.read_number()?;
let y = self.read_number()?;
Ok(Point::new(x, y))
}
fn skip_separators(&mut self) {
while let Some(byte) = self.bytes.get(self.at) {
if byte.is_ascii_whitespace() || *byte == b',' {
self.at += 1;
} else {
break;
}
}
}
fn read_number(&mut self) -> Result<f32, PathError> {
self.skip_separators();
let start = self.at;
if matches!(self.bytes.get(self.at), Some(b'+') | Some(b'-')) {
self.at += 1;
}
let mut saw_digit = false;
while matches!(self.bytes.get(self.at), Some(b) if b.is_ascii_digit()) {
self.at += 1;
saw_digit = true;
}
if self.bytes.get(self.at) == Some(&b'.') {
self.at += 1;
while matches!(self.bytes.get(self.at), Some(b) if b.is_ascii_digit()) {
self.at += 1;
saw_digit = true;
}
}
if !saw_digit {
return Err(PathError::ExpectedNumber);
}
if matches!(self.bytes.get(self.at), Some(b'e') | Some(b'E')) {
let exponent_start = self.at;
self.at += 1;
if matches!(self.bytes.get(self.at), Some(b'+') | Some(b'-')) {
self.at += 1;
}
let mut exponent_digit = false;
while matches!(self.bytes.get(self.at), Some(b) if b.is_ascii_digit()) {
self.at += 1;
exponent_digit = true;
}
if !exponent_digit {
self.at = exponent_start;
}
}
let text = core::str::from_utf8(&self.bytes[start..self.at])
.map_err(|_| PathError::ExpectedNumber)?;
text.parse::<f32>().map_err(|_| PathError::ExpectedNumber)
}
fn read_flag(&mut self) -> Result<bool, PathError> {
self.skip_separators();
match self.bytes.get(self.at) {
Some(b'0') => {
self.at += 1;
Ok(false)
}
Some(b'1') => {
self.at += 1;
Ok(true)
}
_ => Err(PathError::ExpectedNumber),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compat::Vec;
#[test]
fn a_single_move_and_line_parses() {
let subpaths = parse("M1 2 L3 4").expect("a well-formed path must parse");
assert_eq!(subpaths.len(), 1);
assert_eq!(subpaths[0].start, Point::new(1.0, 2.0));
assert_eq!(subpaths[0].segments.as_slice(), &[Segment::Line(Point::new(3.0, 4.0))]);
assert!(!subpaths[0].closed);
}
#[test]
fn a_relative_command_is_offset_from_the_pen() {
let subpaths = parse("M10 10 l5 0").expect("relative command must parse");
assert_eq!(subpaths[0].segments.as_slice(), &[Segment::Line(Point::new(15.0, 10.0))]);
}
#[test]
fn horizontal_and_vertical_keep_the_other_coordinate() {
let subpaths = parse("M4 5 H10 V20").expect("H/V must parse");
assert_eq!(
subpaths[0].segments.as_slice(),
&[Segment::Line(Point::new(10.0, 5.0)), Segment::Line(Point::new(10.0, 20.0))]
);
}
#[test]
fn repeated_argument_sets_extend_the_command() {
let subpaths = parse("M0 0 L1 1 2 2").expect("repeated arguments must parse");
assert_eq!(subpaths[0].segments.len(), 2);
}
#[test]
fn a_bare_number_after_a_command_starts_that_same_command() {
let subpaths = parse("m336-280 144-144").expect("a bare repeat must parse");
assert_eq!(
subpaths[0].segments.as_slice(),
&[Segment::Line(Point::new(480.0, -424.0))],
"the four numbers are a move and a line, not one move"
);
}
#[test]
fn an_argument_after_a_move_is_a_line() {
let subpaths = parse("M0 0 1 1").expect("the implicit line after M must parse");
assert_eq!(subpaths[0].segments.as_slice(), &[Segment::Line(Point::new(1.0, 1.0))]);
}
#[test]
fn a_smooth_cubic_reflects_the_previous_control_point() {
let subpaths = parse("M0 0 C0 1 2 1 2 0 S4 -1 4 0").expect("a smooth cubic must parse");
assert_eq!(
subpaths[0].segments.as_slice(),
&[
Segment::Cubic {
ctrl1: Point::new(0.0, 1.0),
ctrl2: Point::new(2.0, 1.0),
to: Point::new(2.0, 0.0),
},
Segment::Cubic {
ctrl1: Point::new(2.0, -1.0),
ctrl2: Point::new(4.0, -1.0),
to: Point::new(4.0, 0.0),
},
]
);
}
#[test]
fn a_smooth_command_without_a_previous_curve_reflects_the_pen() {
let subpaths = parse("M0 0 S3 3 6 0").expect("a leading smooth cubic must parse");
assert_eq!(
subpaths[0].segments.as_slice(),
&[Segment::Cubic {
ctrl1: Point::new(0.0, 0.0),
ctrl2: Point::new(3.0, 3.0),
to: Point::new(6.0, 0.0),
}]
);
}
#[test]
fn a_smooth_quadratic_reflects_and_returns_the_pen() {
let subpaths = parse("M0 0 Q1 2 2 0 T4 0").expect("a smooth quadratic must parse");
assert_eq!(
subpaths[0].segments.as_slice(),
&[
Segment::Quad { ctrl: Point::new(1.0, 2.0), to: Point::new(2.0, 0.0) },
Segment::Quad { ctrl: Point::new(3.0, -2.0), to: Point::new(4.0, 0.0) },
]
);
}
#[test]
fn a_line_between_curves_breaks_the_reflection_chain() {
let subpaths = parse("M0 0 C0 1 2 1 2 0 L2 2 S4 4 4 2").expect("mixed path must parse");
let last = subpaths[0].segments.last().expect("a smooth cubic must follow");
assert_eq!(
last,
&Segment::Cubic {
ctrl1: Point::new(2.0, 2.0),
ctrl2: Point::new(4.0, 4.0),
to: Point::new(4.0, 2.0),
}
);
}
#[test]
fn curves_carry_their_control_points() {
let cubic = parse("M0 0 C1 1 2 2 3 3").expect("cubic must parse");
assert_eq!(
cubic[0].segments.as_slice(),
&[Segment::Cubic {
ctrl1: Point::new(1.0, 1.0),
ctrl2: Point::new(2.0, 2.0),
to: Point::new(3.0, 3.0),
}]
);
let quad = parse("M0 0 Q1 1 2 2").expect("quadratic must parse");
assert_eq!(
quad[0].segments.as_slice(),
&[Segment::Quad { ctrl: Point::new(1.0, 1.0), to: Point::new(2.0, 2.0) }]
);
}
#[test]
fn an_arc_becomes_cubics_that_end_on_the_declared_point() {
let subpaths = parse("M0 0 A5 5 0 0 1 10 0").expect("arc must parse");
let segments = subpaths[0].segments.as_slice();
assert!(!segments.is_empty(), "an arc must produce geometry");
assert!(
matches!(segments.last(), Some(Segment::Cubic { .. })),
"an arc must be converted to cubics: {segments:?}"
);
assert_eq!(
segments.last().map(Segment::end),
Some(Point::new(10.0, 0.0)),
"the last curve must end on the arc's declared endpoint"
);
for segment in segments {
assert!(!matches!(segment, Segment::Line(_)), "an arc is not a line: {segment:?}");
}
}
#[test]
fn a_large_arc_is_split_into_multiple_cubics() {
let subpaths = parse("M0 0 A5 5 0 1 1 0 10").expect("a large arc must parse");
let segments = subpaths[0].segments.as_slice();
assert_eq!(segments.len(), 2, "a 180-degree sweep is two 90-degree cubics");
assert_eq!(segments.last().map(Segment::end), Some(Point::new(0.0, 10.0)));
}
#[test]
fn an_arc_with_a_zero_radius_is_a_line() {
let subpaths = parse("M0 0 A0 5 0 0 1 10 10").expect("a zero-radius arc must parse");
assert_eq!(subpaths[0].segments.as_slice(), &[Segment::Line(Point::new(10.0, 10.0))]);
}
#[test]
fn an_arc_between_identical_points_is_empty() {
let subpaths = parse("M5 5 A5 5 0 1 1 5 5").expect("a coincident arc must parse");
assert!(subpaths[0].segments.is_empty());
}
#[test]
fn an_arc_is_scale_invariant_in_the_number_of_segments() {
let absolute = parse("M2 2 A5 5 0 0 1 12 2").expect("absolute arc");
let relative = parse("M2 2 a5 5 0 0 1 10 0").expect("relative arc");
assert_eq!(absolute[0].segments.len(), relative[0].segments.len());
assert_eq!(absolute[0].segments.last().map(Segment::end), Some(Point::new(12.0, 2.0)));
assert_eq!(relative[0].segments.last().map(Segment::end), Some(Point::new(12.0, 2.0)));
}
#[test]
fn an_arc_that_rounds_to_a_zero_sweep_still_emits_one_curve() {
let subpaths = parse("M0 0 A100 100 0 0 1 0.0001 0.0001").expect("tiny arc must parse");
assert_eq!(subpaths[0].segments.len().max(1), 1);
assert_eq!(subpaths[0].segments.last().map(Segment::end), Some(Point::new(0.0001, 0.0001)));
}
#[test]
fn a_close_marks_the_subpath_and_returns_the_pen() {
let subpaths = parse("M0 0 L10 0 Z l0 5").expect("close then relative must parse");
assert!(subpaths[0].closed);
assert_eq!(subpaths[0].segments.last(), Some(&Segment::Line(Point::new(0.0, 5.0))));
}
#[test]
fn a_second_move_starts_a_new_subpath() {
let subpaths = parse("M0 0 L1 0 M5 5 L6 5").expect("two subpaths must parse");
assert_eq!(subpaths.len(), 2);
assert_eq!(subpaths[1].start, Point::new(5.0, 5.0));
}
#[test]
fn separators_include_commas_and_redundant_whitespace() {
let subpaths = parse("M 1,2\n L\t3 , 4").expect("mixed separators must parse");
assert_eq!(subpaths[0].segments.as_slice(), &[Segment::Line(Point::new(3.0, 4.0))]);
}
#[test]
fn decimals_without_a_leading_zero_parse() {
let subpaths = parse("M.5.5L1.5 2.5").expect("compact decimals must parse");
assert_eq!(subpaths[0].start, Point::new(0.5, 0.5));
assert_eq!(subpaths[0].segments.as_slice(), &[Segment::Line(Point::new(1.5, 2.5))]);
}
#[test]
fn an_unknown_command_is_refused() {
assert_eq!(parse("M0 0 X1 1"), Err(PathError::UnknownCommand('X')));
}
#[test]
fn a_missing_number_is_refused() {
assert_eq!(parse("M"), Err(PathError::ExpectedNumber));
assert_eq!(parse("M0 0 L"), Err(PathError::ExpectedNumber));
}
#[test]
fn a_segment_before_a_move_is_refused() {
assert_eq!(parse("L1 1"), Err(PathError::SegmentsBeforeMove));
assert_eq!(parse("Z"), Err(PathError::SegmentsBeforeMove));
}
#[test]
fn an_arc_flag_that_is_not_zero_or_one_is_refused() {
assert_eq!(parse("M0 0 A5 5 0 2 0 1 1"), Err(PathError::ExpectedNumber));
}
#[test]
fn an_empty_path_is_an_empty_result_not_an_error() {
assert_eq!(parse("").expect("empty is valid"), Vec::new());
assert_eq!(parse(" ").expect("blank is valid"), Vec::new());
}
#[test]
fn a_path_over_the_point_budget_is_refused() {
use crate::compat::{format, String};
let mut data = String::from("M0 0");
for i in 0..MAX_PATH_POINTS {
data.push_str(&format!(" L{i} 0"));
}
assert_eq!(parse(&data), Err(PathError::TooManyPoints));
}
}