Lines
98.43 %
Functions
59.21 %
Branches
74.23 %
//! Parser for SVG path data.
use std::fmt;
use std::iter::Enumerate;
use std::str;
use std::str::Bytes;
use crate::path_builder::*;
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum Token {
// pub to allow benchmarking
Number(f64),
Flag(bool),
Command(u8),
Comma,
}
use crate::path_parser::Token::{Comma, Command, Flag, Number};
#[derive(Debug)]
pub struct Lexer<'a> {
input: &'a [u8],
ci: Enumerate<Bytes<'a>>,
current: Option<(usize, u8)>,
flags_required: u8,
pub enum LexError {
ParseFloatError,
UnexpectedByte(u8),
UnexpectedEof,
impl<'a> Lexer<'_> {
pub fn new(input: &'a str) -> Lexer<'a> {
let mut ci = input.bytes().enumerate();
let current = ci.next();
Lexer {
input: input.as_bytes(),
ci,
current,
flags_required: 0,
// The way Flag tokens work is a little annoying. We don't have
// any way to distinguish between numbers and flags without context
// from the parser. The only time we need to return flags is within the
// argument sequence of an elliptical arc, and then we need 2 in a row
// or it's an error. So, when the parser gets to that point, it calls
// this method and we switch from our usual mode of handling digits as
// numbers to looking for two 'flag' characters (either 0 or 1) in a row
// (with optional intervening whitespace, and possibly comma tokens.)
// Every time we find a flag we decrement flags_required.
pub fn require_flags(&mut self) {
self.flags_required = 2;
fn current_pos(&mut self) -> usize {
match self.current {
None => self.input.len(),
Some((pos, _)) => pos,
fn advance(&mut self) {
self.current = self.ci.next();
fn advance_over_whitespace(&mut self) -> bool {
let mut found_some = false;
while self.current.is_some() && self.current.unwrap().1.is_ascii_whitespace() {
found_some = true;
found_some
fn advance_over_optional(&mut self, needle: u8) -> bool {
Some((_, c)) if c == needle => {
self.advance();
true
_ => false,
fn advance_over_digits(&mut self) -> bool {
while self.current.is_some() && self.current.unwrap().1.is_ascii_digit() {
fn advance_over_simple_number(&mut self) -> bool {
let _ = self.advance_over_optional(b'-') || self.advance_over_optional(b'+');
let found_digit = self.advance_over_digits();
let _ = self.advance_over_optional(b'.');
self.advance_over_digits() || found_digit
fn match_number(&mut self) -> Result<Token, LexError> {
// remember the beginning
let (start_pos, _) = self.current.unwrap();
if !self.advance_over_simple_number() && start_pos != self.current_pos() {
None => return Err(LexError::UnexpectedEof),
Some((_pos, c)) => return Err(LexError::UnexpectedByte(c)),
if self.advance_over_optional(b'e') || self.advance_over_optional(b'E') {
let _ = self.advance_over_digits();
let end_pos = match self.current {
Some((i, _)) => i,
};
// If you need path parsing to be faster, you can do from_utf8_unchecked to
// avoid re-validating all the chars, and std::str::parse<i*> calls are
// faster than std::str::parse<f64> for numbers that are not floats.
// bare unwrap here should be safe since we've already checked all the bytes
// in the range
match std::str::from_utf8(&self.input[start_pos..end_pos])
.unwrap()
.parse::<f64>()
{
Ok(n) => Ok(Number(n)),
Err(_e) => Err(LexError::ParseFloatError),
impl Iterator for Lexer<'_> {
type Item = (usize, Result<Token, LexError>);
fn next(&mut self) -> Option<Self::Item> {
// eat whitespace
self.advance_over_whitespace();
// commas are separators
Some((pos, b',')) => {
Some((pos, Ok(Comma)))
// alphabetic chars are commands
Some((pos, c)) if c.is_ascii_alphabetic() => {
let token = Command(c);
Some((pos, Ok(token)))
Some((pos, c)) if self.flags_required > 0 && c.is_ascii_digit() => match c {
b'0' => {
self.flags_required -= 1;
Some((pos, Ok(Flag(false))))
b'1' => {
Some((pos, Ok(Flag(true))))
_ => Some((pos, Err(LexError::UnexpectedByte(c)))),
},
Some((pos, c)) if c.is_ascii_digit() || c == b'-' || c == b'+' || c == b'.' => {
Some((pos, self.match_number()))
Some((pos, c)) => {
Some((pos, Err(LexError::UnexpectedByte(c))))
None => None,
pub struct PathParser<'b> {
tokens: Lexer<'b>,
current_pos_and_token: Option<(usize, Result<Token, LexError>)>,
builder: &'b mut PathBuilder,
// Current point; adjusted at every command
current_x: f64,
current_y: f64,
// Last control point from previous cubic curve command, used to reflect
// the new control point for smooth cubic curve commands.
cubic_reflection_x: f64,
cubic_reflection_y: f64,
// Last control point from previous quadratic curve command, used to reflect
// the new control point for smooth quadratic curve commands.
quadratic_reflection_x: f64,
quadratic_reflection_y: f64,
// Start point of current subpath (i.e. position of last moveto);
// used for closepath.
subpath_start_x: f64,
subpath_start_y: f64,
// This is a recursive descent parser for path data in SVG files,
// as specified in https://www.w3.org/TR/SVG/paths.html#PathDataBNF
// Some peculiarities:
//
// - SVG allows optional commas inside coordinate pairs, and between
// coordinate pairs. So, for example, these are equivalent:
// M 10 20 30 40
// M 10, 20 30, 40
// M 10, 20, 30, 40
// - Whitespace is optional. These are equivalent:
// M10,20 30,40
// M10,20,30,40
// These are also equivalent:
// M-10,20-30-40
// M -10 20 -30 -40
// M.1-2,3E2-4
// M 0.1 -2 300 -4
impl<'b> PathParser<'b> {
pub fn new(builder: &'b mut PathBuilder, path_str: &'b str) -> PathParser<'b> {
let mut lexer = Lexer::new(path_str);
let pt = lexer.next();
PathParser {
tokens: lexer,
current_pos_and_token: pt,
builder,
current_x: 0.0,
current_y: 0.0,
cubic_reflection_x: 0.0,
cubic_reflection_y: 0.0,
quadratic_reflection_x: 0.0,
quadratic_reflection_y: 0.0,
subpath_start_x: 0.0,
subpath_start_y: 0.0,
// Our match_* methods all either consume the token we requested
// and return the unwrapped value, or return an error without
// advancing the token stream.
// You can safely use them to probe for a particular kind of token,
// fail to match it, and try some other type.
fn match_command(&mut self) -> Result<u8, ParseError> {
let result = match &self.current_pos_and_token {
Some((_, Ok(Command(c)))) => Ok(*c),
Some((pos, Ok(t))) => Err(ParseError::new(*pos, UnexpectedToken(*t))),
Some((pos, Err(e))) => Err(ParseError::new(*pos, LexError(*e))),
None => Err(ParseError::new(self.tokens.input.len(), UnexpectedEof)),
if result.is_ok() {
self.current_pos_and_token = self.tokens.next();
result
fn match_number(&mut self) -> Result<f64, ParseError> {
Some((_, Ok(Number(n)))) => Ok(*n),
fn match_number_and_flags(&mut self) -> Result<(f64, bool, bool), ParseError> {
// We can't just do self.match_number() here, because we have to
// tell the lexer, if we do find a number, to switch to looking for flags
// before we advance it to the next token. Otherwise it will treat the flag
// characters as numbers.
// So, first we do the guts of match_number...
let n = match &self.current_pos_and_token {
}?;
// Then we tell the lexer that we're going to need to find Flag tokens,
// *then* we can advance the token stream.
self.tokens.require_flags();
self.eat_optional_comma();
let f1 = self.match_flag()?;
let f2 = self.match_flag()?;
Ok((n, f1, f2))
fn match_comma(&mut self) -> Result<(), ParseError> {
Some((_, Ok(Comma))) => Ok(()),
fn eat_optional_comma(&mut self) {
let _ = self.match_comma();
// Convenience function; like match_number, but eats a leading comma if present.
fn match_comma_number(&mut self) -> Result<f64, ParseError> {
self.match_number()
fn match_flag(&mut self) -> Result<bool, ParseError> {
let result = match self.current_pos_and_token {
Some((_, Ok(Flag(f)))) => Ok(f),
Some((pos, Ok(t))) => Err(ParseError::new(pos, UnexpectedToken(t))),
Some((pos, Err(e))) => Err(ParseError::new(pos, LexError(e))),
// peek_* methods are the twins of match_*, but don't consume the token, and so
// can't return ParseError
fn peek_command(&mut self) -> Option<u8> {
match &self.current_pos_and_token {
Some((_, Ok(Command(c)))) => Some(*c),
_ => None,
fn peek_number(&mut self) -> Option<f64> {
Some((_, Ok(Number(n)))) => Some(*n),
// This is the entry point for parsing a given blob of path data.
// All the parsing just uses various match_* methods to consume tokens
// and retrieve the values.
pub fn parse(&mut self) -> Result<(), ParseError> {
if self.current_pos_and_token.is_none() {
return Ok(());
self.moveto_drawto_command_groups()
fn error(&self, kind: ErrorKind) -> ParseError {
match self.current_pos_and_token {
Some((pos, _)) => ParseError {
position: pos,
kind,
None => ParseError { position: 0, kind }, // FIXME: ???
fn coordinate_pair(&mut self) -> Result<(f64, f64), ParseError> {
Ok((self.match_number()?, self.match_comma_number()?))
fn set_current_point(&mut self, x: f64, y: f64) {
self.current_x = x;
self.current_y = y;
self.cubic_reflection_x = self.current_x;
self.cubic_reflection_y = self.current_y;
self.quadratic_reflection_x = self.current_x;
self.quadratic_reflection_y = self.current_y;
fn set_cubic_reflection_and_current_point(&mut self, x3: f64, y3: f64, x4: f64, y4: f64) {
self.cubic_reflection_x = x3;
self.cubic_reflection_y = y3;
self.current_x = x4;
self.current_y = y4;
fn set_quadratic_reflection_and_current_point(&mut self, a: f64, b: f64, c: f64, d: f64) {
self.quadratic_reflection_x = a;
self.quadratic_reflection_y = b;
self.current_x = c;
self.current_y = d;
fn emit_move_to(&mut self, x: f64, y: f64) {
self.set_current_point(x, y);
self.subpath_start_x = self.current_x;
self.subpath_start_y = self.current_y;
self.builder.move_to(self.current_x, self.current_y);
fn emit_line_to(&mut self, x: f64, y: f64) {
self.builder.line_to(self.current_x, self.current_y);
fn emit_curve_to(&mut self, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) {
self.set_cubic_reflection_and_current_point(x3, y3, x4, y4);
self.builder.curve_to(x2, y2, x3, y3, x4, y4);
fn emit_quadratic_curve_to(&mut self, a: f64, b: f64, c: f64, d: f64) {
// raise quadratic Bézier to cubic
let x2 = (self.current_x + 2.0 * a) / 3.0;
let y2 = (self.current_y + 2.0 * b) / 3.0;
let x4 = c;
let y4 = d;
let x3 = (x4 + 2.0 * a) / 3.0;
let y3 = (y4 + 2.0 * b) / 3.0;
self.set_quadratic_reflection_and_current_point(a, b, c, d);
fn emit_arc(
&mut self,
rx: f64,
ry: f64,
x_axis_rotation: f64,
large_arc: LargeArc,
sweep: Sweep,
x: f64,
y: f64,
) {
let (start_x, start_y) = (self.current_x, self.current_y);
self.builder.arc(
start_x,
start_y,
rx,
ry,
x_axis_rotation,
large_arc,
sweep,
self.current_x,
self.current_y,
);
fn moveto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> {
let (mut x, mut y) = self.coordinate_pair()?;
if !absolute {
x += self.current_x;
y += self.current_y;
self.emit_move_to(x, y);
if self.match_comma().is_ok() || self.peek_number().is_some() {
self.lineto_argument_sequence(absolute)
} else {
Ok(())
fn moveto(&mut self) -> Result<(), ParseError> {
match self.match_command()? {
b'M' => self.moveto_argument_sequence(true),
b'm' => self.moveto_argument_sequence(false),
c => Err(self.error(ErrorKind::UnexpectedCommand(c))),
fn moveto_drawto_command_group(&mut self) -> Result<(), ParseError> {
self.moveto()?;
self.optional_drawto_commands().map(|_| ())
fn moveto_drawto_command_groups(&mut self) -> Result<(), ParseError> {
loop {
self.moveto_drawto_command_group()?;
break;
fn optional_drawto_commands(&mut self) -> Result<bool, ParseError> {
while self.drawto_command()? {
// everything happens in the drawto_command() calls.
Ok(false)
// FIXME: This should not just fail to match 'M' and 'm', but make sure the
// command is in the set of drawto command characters.
fn match_if_drawto_command_with_absolute(&mut self) -> Option<(u8, bool)> {
let cmd = self.peek_command();
let result = match cmd {
Some(b'M') => None,
Some(b'm') => None,
Some(c) => {
let c_up = c.to_ascii_uppercase();
if c == c_up {
Some((c_up, true))
Some((c_up, false))
if result.is_some() {
let _ = self.match_command();
fn drawto_command(&mut self) -> Result<bool, ParseError> {
match self.match_if_drawto_command_with_absolute() {
Some((b'Z', _)) => {
self.emit_close_path();
Ok(true)
Some((b'L', abs)) => {
self.lineto_argument_sequence(abs)?;
Some((b'H', abs)) => {
self.horizontal_lineto_argument_sequence(abs)?;
Some((b'V', abs)) => {
self.vertical_lineto_argument_sequence(abs)?;
Some((b'C', abs)) => {
self.curveto_argument_sequence(abs)?;
Some((b'S', abs)) => {
self.smooth_curveto_argument_sequence(abs)?;
Some((b'Q', abs)) => {
self.quadratic_curveto_argument_sequence(abs)?;
Some((b'T', abs)) => {
self.smooth_quadratic_curveto_argument_sequence(abs)?;
Some((b'A', abs)) => {
self.elliptical_arc_argument_sequence(abs)?;
_ => Ok(false),
fn emit_close_path(&mut self) {
let (x, y) = (self.subpath_start_x, self.subpath_start_y);
self.builder.close_path();
fn should_break_arg_sequence(&mut self) -> bool {
if self.match_comma().is_ok() {
// if there is a comma (indicating we should continue to loop), eat the comma
// so we're ready at the next start of the loop to process the next token.
false
// continue to process args in the sequence unless the next token is a comma
self.peek_number().is_none()
fn lineto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> {
self.emit_line_to(x, y);
if self.should_break_arg_sequence() {
fn horizontal_lineto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> {
let mut x = self.match_number()?;
let y = self.current_y;
fn vertical_lineto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> {
let mut y = self.match_number()?;
let x = self.current_x;
fn curveto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> {
let (mut x2, mut y2) = self.coordinate_pair()?;
let (mut x3, mut y3) = self.coordinate_pair()?;
let (mut x4, mut y4) = self.coordinate_pair()?;
x2 += self.current_x;
y2 += self.current_y;
x3 += self.current_x;
y3 += self.current_y;
x4 += self.current_x;
y4 += self.current_y;
self.emit_curve_to(x2, y2, x3, y3, x4, y4);
fn smooth_curveto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> {
let (x2, y2) = (
self.current_x + self.current_x - self.cubic_reflection_x,
self.current_y + self.current_y - self.cubic_reflection_y,
fn quadratic_curveto_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> {
let (mut a, mut b) = self.coordinate_pair()?;
let (mut c, mut d) = self.coordinate_pair()?;
a += self.current_x;
b += self.current_y;
c += self.current_x;
d += self.current_y;
self.emit_quadratic_curve_to(a, b, c, d);
fn smooth_quadratic_curveto_argument_sequence(
absolute: bool,
) -> Result<(), ParseError> {
let (a, b) = (
self.current_x + self.current_x - self.quadratic_reflection_x,
self.current_y + self.current_y - self.quadratic_reflection_y,
fn elliptical_arc_argument_sequence(&mut self, absolute: bool) -> Result<(), ParseError> {
let rx = self.match_number()?.abs();
let ry = self.match_comma_number()?.abs();
let (x_axis_rotation, f1, f2) = self.match_number_and_flags()?;
let large_arc = LargeArc(f1);
let sweep = if f2 { Sweep::Positive } else { Sweep::Negative };
self.emit_arc(rx, ry, x_axis_rotation, large_arc, sweep, x, y);
#[derive(Debug, PartialEq)]
pub enum ErrorKind {
UnexpectedToken(Token),
UnexpectedCommand(u8),
LexError(LexError),
pub struct ParseError {
pub position: usize,
pub kind: ErrorKind,
impl ParseError {
fn new(pos: usize, k: ErrorKind) -> ParseError {
ParseError {
kind: k,
use crate::path_parser::ErrorKind::*;
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let description = match self.kind {
UnexpectedToken(_t) => "unexpected token",
UnexpectedCommand(_c) => "unexpected command",
UnexpectedEof => "unexpected end of data",
LexError(_le) => "error processing token",
write!(f, "error at position {}: {}", self.position, description)
#[cfg(test)]
#[rustfmt::skip]
mod tests {
use super::*;
fn find_error_pos(s: &str) -> Option<usize> {
s.find('^')
fn make_parse_result(
error_pos_str: &str,
error_kind: Option<ErrorKind>,
if let Some(pos) = find_error_pos(error_pos_str) {
Err(ParseError {
kind: error_kind.unwrap(),
})
assert!(error_kind.is_none());
fn test_parser(
path_str: &str,
expected_commands: &[PathCommand],
expected_error_kind: Option<ErrorKind>,
let expected_result = make_parse_result(error_pos_str, expected_error_kind);
let mut builder = PathBuilder::default();
let result = builder.parse(path_str);
let path = builder.into_path();
let commands = path.iter().collect::<Vec<_>>();
assert_eq!(expected_commands, commands.as_slice());
assert_eq!(expected_result, result);
fn moveto(x: f64, y: f64) -> PathCommand {
PathCommand::MoveTo(x, y)
fn lineto(x: f64, y: f64) -> PathCommand {
PathCommand::LineTo(x, y)
fn curveto(x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) -> PathCommand {
PathCommand::CurveTo(CubicBezierCurve {
pt1: (x2, y2),
pt2: (x3, y3),
to: (x4, y4),
fn arc(x2: f64, y2: f64, xr: f64, large_arc: bool, sweep: bool,
x3: f64, y3: f64, x4: f64, y4: f64) -> PathCommand {
PathCommand::Arc(EllipticalArc {
r: (x2, y2),
x_axis_rotation: xr,
large_arc: LargeArc(large_arc),
sweep: match sweep {
true => Sweep::Positive,
false => Sweep::Negative,
from: (x3, y3),
fn closepath() -> PathCommand {
PathCommand::ClosePath
#[test]
fn handles_empty_data() {
test_parser(
"",
&Vec::<PathCommand>::new(),
None,
fn handles_numbers() {
"M 10 20",
&[moveto(10.0, 20.0)],
"M -10 -20",
&[moveto(-10.0, -20.0)],
"M .10 0.20",
&[moveto(0.10, 0.20)],
"M -.10 -0.20",
&[moveto(-0.10, -0.20)],
"M-.10-0.20",
"M10.5.50",
&[moveto(10.5, 0.50)],
"M.10.20",
"M .10E1 .20e-4",
&[moveto(1.0, 0.000020)],
"M-.10E1-.20",
&[moveto(-1.0, -0.20)],
"M10.10E2 -0.20e3",
&[moveto(1010.0, -200.0)],
"M-10.10E2-0.20e-3",
&[moveto(-1010.0, -0.00020)],
"M1e2.5", // a decimal after exponent start the next number
&[moveto(100.0, 0.5)],
"M1e-2.5", // but we are allowed a sign after exponent
&[moveto(0.01, 0.5)],
"M1e+2.5", // but we are allowed a sign after exponent
fn detects_bogus_numbers() {
"M+",
" ^",
&[],
Some(ErrorKind::LexError(LexError::UnexpectedEof)),
"M-",
"M+x",
Some(ErrorKind::LexError(LexError::UnexpectedByte(b'x'))),
"M10e",
Some(ErrorKind::LexError(LexError::ParseFloatError)),
"M10ex",
"M10e-",
"M10e+x",
fn handles_numbers_with_comma() {
"M 10, 20",
"M -10,-20",
"M.10 , 0.20",
"M -.10, -0.20 ",
"M .10E1,.20e-4",
"M-.10E-2,-.20",
&[moveto(-0.0010, -0.20)],
"M10.10E2,-0.20e3",
"M-10.10E2,-0.20e-3",
fn handles_single_moveto() {
"M 10 20 ",
"M10,20 ",
"M10 20 ",
" M10,20 ",
fn handles_relative_moveto() {
"m10 20",
fn handles_absolute_moveto_with_implicit_lineto() {
"M10 20 30 40",
&[moveto(10.0, 20.0), lineto(30.0, 40.0)],
"M10,20,30,40",
"M.1-2,3E2-4",
&[moveto(0.1, -2.0), lineto(300.0, -4.0)],
fn handles_relative_moveto_with_implicit_lineto() {
"m10 20 30 40",
&[moveto(10.0, 20.0), lineto(40.0, 60.0)],
fn handles_relative_moveto_with_relative_lineto_sequence() {
// 1 2 3 4 5
"m 46,447 l 0,0.5 -1,0 -1,0 0,1 0,12",
&vec![moveto(46.0, 447.0), lineto(46.0, 447.5), lineto(45.0, 447.5),
lineto(44.0, 447.5), lineto(44.0, 448.5), lineto(44.0, 460.5)],
fn handles_absolute_moveto_with_implicit_linetos() {
"M10,20 30,40,50 60",
&[moveto(10.0, 20.0), lineto(30.0, 40.0), lineto(50.0, 60.0)],
fn handles_relative_moveto_with_implicit_linetos() {
"m10 20 30 40 50 60",
&[moveto(10.0, 20.0), lineto(40.0, 60.0), lineto(90.0, 120.0)],
fn handles_absolute_moveto_moveto() {
"M10 20 M 30 40",
&[moveto(10.0, 20.0), moveto(30.0, 40.0)],
fn handles_relative_moveto_moveto() {
"m10 20 m 30 40",
&[moveto(10.0, 20.0), moveto(40.0, 60.0)],
fn handles_relative_moveto_lineto_moveto() {
"m10 20 30 40 m 50 60",
&[moveto(10.0, 20.0), lineto(40.0, 60.0), moveto(90.0, 120.0)],
fn handles_absolute_moveto_lineto() {
"M10 20 L30,40",
fn handles_relative_moveto_lineto() {
"m10 20 l30,40",
fn handles_relative_moveto_lineto_lineto_abs_lineto() {
"m10 20 30 40l30,40,50 60L200,300",
&vec![
moveto(10.0, 20.0),
lineto(40.0, 60.0),
lineto(70.0, 100.0),
lineto(120.0, 160.0),
lineto(200.0, 300.0),
],
fn handles_horizontal_lineto() {
"M10 20 H30",
&[moveto(10.0, 20.0), lineto(30.0, 20.0)],
"M10 20 H30 40",
&[moveto(10.0, 20.0), lineto(30.0, 20.0), lineto(40.0, 20.0)],
"M10 20 H30,40-50",
lineto(30.0, 20.0),
lineto(40.0, 20.0),
lineto(-50.0, 20.0),
"m10 20 h30,40-50",
lineto(80.0, 20.0),
fn handles_vertical_lineto() {
"M10 20 V30",
&[moveto(10.0, 20.0), lineto(10.0, 30.0)],
"M10 20 V30 40",
&[moveto(10.0, 20.0), lineto(10.0, 30.0), lineto(10.0, 40.0)],
"M10 20 V30,40-50",
lineto(10.0, 30.0),
lineto(10.0, 40.0),
lineto(10.0, -50.0),
"m10 20 v30,40-50",
lineto(10.0, 50.0),
lineto(10.0, 90.0),
fn handles_curveto() {
"M10 20 C 30,40 50 60-70,80",
&[moveto(10.0, 20.0),
curveto(30.0, 40.0, 50.0, 60.0, -70.0, 80.0)],
"M10 20 C 30,40 50 60-70,80,90 100,110 120,130,140",
curveto(30.0, 40.0, 50.0, 60.0, -70.0, 80.0),
curveto(90.0, 100.0, 110.0, 120.0, 130.0, 140.0)],
"m10 20 c 30,40 50 60-70,80,90 100,110 120,130,140",
curveto(40.0, 60.0, 60.0, 80.0, -60.0, 100.0),
curveto(30.0, 200.0, 50.0, 220.0, 70.0, 240.0)],
fn handles_smooth_curveto() {
"M10 20 S 30,40-50,60",
curveto(10.0, 20.0, 30.0, 40.0, -50.0, 60.0)],
"M10 20 S 30,40 50 60-70,80,90 100",
curveto(10.0, 20.0, 30.0, 40.0, 50.0, 60.0),
curveto(70.0, 80.0, -70.0, 80.0, 90.0, 100.0)],
"m10 20 s 30,40 50 60-70,80,90 100",
curveto(10.0, 20.0, 40.0, 60.0, 60.0, 80.0),
curveto(80.0, 100.0, -10.0, 160.0, 150.0, 180.0)],
fn handles_quadratic_curveto() {
"M10 20 Q30 40 50 60",
curveto(
70.0 / 3.0,
100.0 / 3.0,
110.0 / 3.0,
140.0 / 3.0,
50.0,
60.0,
)],
"M10 20 Q30 40 50 60,70,80-90 100",
),
190.0 / 3.0,
220.0 / 3.0,
50.0 / 3.0,
260.0 / 3.0,
-90.0,
100.0,
"m10 20 q 30,40 50 60-70,80 90 100",
90.0 / 3.0,
200.0 / 3.0,
80.0,
40.0 / 3.0,
400.0 / 3.0,
130.0 / 3.0,
500.0 / 3.0,
150.0,
180.0,
fn handles_smooth_quadratic_curveto() {
"M10 20 T30 40",
curveto(10.0, 20.0, 50.0 / 3.0, 80.0 / 3.0, 30.0, 40.0)],
"M10 20 Q30 40 50 60 T70 80",
curveto(190.0 / 3.0, 220.0 / 3.0, 70.0, 80.0, 70.0, 80.0)],
"m10 20 q 30,40 50 60t-70,80",
curveto(220.0 / 3.0, 280.0 / 3.0, 50.0, 120.0, -10.0, 160.0)],
fn handles_elliptical_arc() {
// no space required between arc flags
test_parser("M 1 2 A 1 2 3 00 6 7",
&[moveto(1.0, 2.0),
arc(1.0, 2.0, 3.0, false, false, 1.0, 2.0, 6.0, 7.0)],
None);
// or after...
test_parser("M 1 2 A 1 2 3 016 7",
arc(1.0, 2.0, 3.0, false, true, 1.0, 2.0, 6.0, 7.0)],
// commas and whitespace are optionally allowed
test_parser("M 1 2 A 1 2 3 10,6 7",
arc(1.0, 2.0, 3.0, true, false, 1.0, 2.0, 6.0, 7.0)],
test_parser("M 1 2 A 1 2 3 1,16, 7",
arc(1.0, 2.0, 3.0, true, true, 1.0, 2.0, 6.0, 7.0)],
test_parser("M 1 2 A 1 2 3 1,1 6 7",
test_parser("M 1 2 A 1 2 3 1 1 6 7",
test_parser("M 1 2 A 1 2 3 1 16 7",
fn handles_close_path() {
test_parser("M10 20 Z", "", &[moveto(10.0, 20.0), closepath()], None);
"m10 20 30 40 m 50 60 70 80 90 100z",
moveto(90.0, 120.0),
lineto(160.0, 200.0),
lineto(250.0, 300.0),
closepath(),
fn first_command_must_be_moveto() {
" L10 20",
" ^", // FIXME: why is this not at position 2?
Some(ErrorKind::UnexpectedCommand(b'L')),
fn moveto_args() {
"M",
Some(ErrorKind::UnexpectedEof),
"M,",
Some(ErrorKind::UnexpectedToken(Comma)),
"M10",
"M10,",
"M10x",
Some(ErrorKind::UnexpectedToken(Command(b'x'))),
"M10,x",
fn moveto_implicit_lineto_args() {
"M10-20,",
&[moveto(10.0, -20.0)],
"M10-20-30",
"M10-20-30 x",
fn closepath_no_args() {
"M10-20z10",
&[moveto(10.0, -20.0), closepath()],
Some(ErrorKind::UnexpectedToken(Number(10.0))),
"M10-20z,",
fn lineto_args() {
"M10-20L10",
"M 10,10 L 20,20,30",
&[moveto(10.0, 10.0), lineto(20.0, 20.0)],
"M 10,10 L 20,20,",
fn horizontal_lineto_args() {
"M10-20H",
"M10-20H,",
"M10-20H30,",
&[moveto(10.0, -20.0), lineto(30.0, -20.0)],
fn vertical_lineto_args() {
"M10-20v",
"M10-20v,",
"M10-20v30,",
&[moveto(10.0, -20.0), lineto(10.0, 10.0)],
fn curveto_args() {
"M10-20C1",
"M10-20C1,",
"M10-20C1 2",
"M10-20C1,2,",
"M10-20C1 2 3",
"M10-20C1,2,3",
"M10-20C1,2,3,",
"M10-20C1 2 3 4",
"M10-20C1,2,3,4",
"M10-20C1,2,3,4,",
"M10-20C1 2 3 4 5",
"M10-20C1,2,3,4,5",
"M10-20C1,2,3,4,5,",
"M10-20C1,2,3,4,5,6,",
&[moveto(10.0, -20.0), curveto(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)],
fn smooth_curveto_args() {
"M10-20S1",
"M10-20S1,",
"M10-20S1 2",
"M10-20S1,2,",
"M10-20S1 2 3",
"M10-20S1,2,3",
"M10-20S1,2,3,",
"M10-20S1,2,3,4,",
&[moveto(10.0, -20.0),
curveto(10.0, -20.0, 1.0, 2.0, 3.0, 4.0)],
fn quadratic_bezier_curveto_args() {
"M10-20Q1",
"M10-20Q1,",
"M10-20Q1 2",
"M10-20Q1,2,",
"M10-20Q1 2 3",
"M10-20Q1,2,3",
"M10-20Q1,2,3,",
"M10 20 Q30 40 50 60,",
fn smooth_quadratic_bezier_curveto_args() {
"M10-20T1",
"M10-20T1,",
"M10 20 T30 40,",
fn elliptical_arc_args() {
"M10-20A1",
"M10-20A1,",
"M10-20A1 2",
"M10-20A1 2,",
"M10-20A1 2 3",
"M10-20A1 2 3,",
"M10-20A1 2 3 4",
Some(ErrorKind::LexError(LexError::UnexpectedByte(b'4'))),
"M10-20A1 2 3 1",
"M10-20A1 2 3,1,",
"M10-20A1 2 3 1 5",
Some(ErrorKind::LexError(LexError::UnexpectedByte(b'5'))),
"M10-20A1 2 3 1 1",
"M10-20A1 2 3,1,1,",
"M10-20A1 2 3 1 1 6",
"M10-20A1 2 3,1,1,6,",
// no non 0|1 chars allowed for flags
test_parser("M 1 2 A 1 2 3 1.0 0.0 6 7",
&[moveto(1.0, 2.0)],
Some(ErrorKind::UnexpectedToken(Number(0.0))));
test_parser("M10-20A1 2 3,1,1,6,7,",
arc(1.0, 2.0, 3.0, true, true, 10.0, -20.0, 6.0, 7.0)],
Some(ErrorKind::UnexpectedEof));
fn bugs() {
// https://gitlab.gnome.org/GNOME/librsvg/issues/345
"M.. 1,0 0,100000",
" ^", // FIXME: we have to report position of error in lexer errors to make this right
Some(ErrorKind::LexError(LexError::UnexpectedByte(b'.'))),