svg-path-bbox 0.0.1

SVG paths bounding box calculator in Rust
Documentation
use std::io::Error;

use crate::{tokenize::*, types::*};
use PathCommandKindDiscriminants::*;

pub fn parse(path: &str) -> Result<PathCommands, Error> {
    let mut result: PathCommands = Vec::new();

    let tokens = tokenize(path);

    for token in tokens {
        let (abs_rel, command) = char_to_command(token[0].chars().next().unwrap()).unwrap();

        let num: Vec<f64> = token[1..]
            .iter()
            .map(|s| s.parse::<f64>().unwrap())
            .collect();

        let command_kind = match command {
            MoveTo => {
                let points = parse_chunks(&num, 2, |chunk| Point {
                    x: chunk[0],
                    y: chunk[1],
                });
                PathCommandKind::MoveTo(points)
            }
            LineTo => {
                let points = parse_chunks(&num, 2, |chunk| Point {
                    x: chunk[0],
                    y: chunk[1],
                });
                PathCommandKind::LineTo(points)
            }
            CurveTo => {
                let cubics = parse_chunks(&num, 6, |chunk| Cubic {
                    ctrl1: Point {
                        x: chunk[0],
                        y: chunk[1],
                    },
                    ctrl2: Point {
                        x: chunk[2],
                        y: chunk[3],
                    },
                    to: Point {
                        x: chunk[4],
                        y: chunk[5],
                    },
                });
                PathCommandKind::CurveTo(cubics)
            }
            QuadraticCurveTo => {
                let quads = parse_chunks(&num, 4, |chunk| Bezier2 {
                    ctrl1: Point {
                        x: chunk[0],
                        y: chunk[1],
                    },
                    to: Point {
                        x: chunk[2],
                        y: chunk[3],
                    },
                });
                PathCommandKind::QuadraticCurveTo(quads)
            }
            SmoothQuadraticCurveTo => {
                let points = parse_chunks(&num, 2, |chunk| Point {
                    x: chunk[0],
                    y: chunk[1],
                });
                PathCommandKind::SmoothQuadraticCurveTo(points)
            }
            EllipticalArcTo => {
                let arcs = parse_chunks(&num, 7, |chunk| Arc {
                    radii: Point {
                        x: chunk[0],
                        y: chunk[1],
                    },
                    x_axis_rotation: chunk[2],
                    large_arc: chunk[3] != 0.0,
                    sweep: chunk[4] != 0.0,
                    to: Point {
                        x: chunk[5],
                        y: chunk[6],
                    },
                });
                PathCommandKind::EllipticalArcTo(arcs)
            }
            HorizontalLineTo => PathCommandKind::HorizontalLineTo(num),
            VerticalLineTo => PathCommandKind::VerticalLineTo(num),
            ClosePath => PathCommandKind::ClosePath,
            SmoothCurveTo => {
                let bezier2 = parse_chunks(&num, 4, |chunk| Bezier2 {
                    ctrl1: Point {
                        x: chunk[0],
                        y: chunk[1],
                    },
                    to: Point {
                        x: chunk[2],
                        y: chunk[3],
                    },
                });
                PathCommandKind::SmoothCurveTo(bezier2)
            }
        };

        result.push((abs_rel, command_kind));
    }

    Ok(result)
}

fn parse_chunks<T, F>(num: &[f64], chunk_size: usize, f: F) -> Vec<T>
where
    F: Fn(&[f64]) -> T,
{
    if num.len() % chunk_size != 0 {
        panic!(
            "Number of coordinates {} is not a multiple of {}: {:?}",
            num.len(),
            chunk_size,
            num
        );
    }

    num.chunks(chunk_size).map(|chunk| f(chunk)).collect()
}

fn char_to_command(c: char) -> Option<(AbsRel, PathCommandKindDiscriminants)> {
    let abs_rel = match c.is_uppercase() {
        true => AbsRel::Absolute,
        false => AbsRel::Relative,
    };

    let path_command = match c.to_ascii_uppercase() {
        'M' => Some(MoveTo),
        'L' => Some(LineTo),
        'H' => Some(HorizontalLineTo),
        'V' => Some(VerticalLineTo),
        'Z' => Some(ClosePath),

        'C' => Some(CurveTo),
        'S' => Some(SmoothCurveTo),
        'Q' => Some(QuadraticCurveTo),
        'T' => Some(SmoothQuadraticCurveTo),
        'A' => Some(EllipticalArcTo),

        _ => None,
    };

    match path_command {
        Some(cmd) => Some((abs_rel, cmd)),
        None => None,
    }
}

#[cfg(test)]
use AbsRel::*;

#[test]
fn test_char_to_command() {
    assert_eq!(char_to_command('M'), Some((Absolute, MoveTo)));
    assert_eq!(char_to_command('L'), Some((Absolute, LineTo)));
    assert_eq!(char_to_command('H'), Some((Absolute, HorizontalLineTo)));
    assert_eq!(char_to_command('V'), Some((Absolute, VerticalLineTo)));
    assert_eq!(char_to_command('Z'), Some((Absolute, ClosePath)));
    assert_eq!(char_to_command('C'), Some((Absolute, CurveTo)));
    assert_eq!(char_to_command('S'), Some((Absolute, SmoothCurveTo)));
    assert_eq!(char_to_command('Q'), Some((Absolute, QuadraticCurveTo)));
    assert_eq!(
        char_to_command('T'),
        Some((Absolute, SmoothQuadraticCurveTo))
    );
    assert_eq!(char_to_command('A'), Some((Absolute, EllipticalArcTo)));

    assert_eq!(char_to_command('m'), Some((Relative, MoveTo)));
    assert_eq!(char_to_command('l'), Some((Relative, LineTo)));
    assert_eq!(char_to_command('h'), Some((Relative, HorizontalLineTo)));
    assert_eq!(char_to_command('v'), Some((Relative, VerticalLineTo)));
    assert_eq!(char_to_command('z'), Some((Relative, ClosePath)));
    assert_eq!(char_to_command('c'), Some((Relative, CurveTo)));
    assert_eq!(char_to_command('s'), Some((Relative, SmoothCurveTo)));
    assert_eq!(char_to_command('q'), Some((Relative, QuadraticCurveTo)));
    assert_eq!(
        char_to_command('t'),
        Some((Relative, SmoothQuadraticCurveTo))
    );
    assert_eq!(char_to_command('a'), Some((Relative, EllipticalArcTo)));

    assert_eq!(char_to_command('N'), None);
}

#[test]
fn test_parse() {
    assert_eq!(
        parse("M 10 20 L 30 40 H 50 V 60 Z").unwrap(),
        vec![
            (
                Absolute,
                PathCommandKind::MoveTo(vec![Point { x: 10.0, y: 20.0 }])
            ),
            (
                Absolute,
                PathCommandKind::LineTo(vec![Point { x: 30.0, y: 40.0 }])
            ),
            (Absolute, PathCommandKind::HorizontalLineTo(vec![50.0])),
            (Absolute, PathCommandKind::VerticalLineTo(vec![60.0])),
            (Absolute, PathCommandKind::ClosePath),
        ]
    );

    assert_eq!(
        parse("C 10 10 20 20 30 30 S 40 40 50 50 Q 60 60 70 70 T 80 80 A 10 20 0 1 0 90 100")
            .unwrap(),
        vec![
            (
                Absolute,
                PathCommandKind::CurveTo(vec![Cubic {
                    ctrl1: Point { x: 10.0, y: 10.0 },
                    ctrl2: Point { x: 20.0, y: 20.0 },
                    to: Point { x: 30.0, y: 30.0 },
                }])
            ),
            (
                Absolute,
                PathCommandKind::SmoothCurveTo(vec![Bezier2 {
                    ctrl1: Point { x: 40.0, y: 40.0 },
                    to: Point { x: 50.0, y: 50.0 },
                }])
            ),
            (
                Absolute,
                PathCommandKind::QuadraticCurveTo(vec![Bezier2 {
                    ctrl1: Point { x: 60.0, y: 60.0 },
                    to: Point { x: 70.0, y: 70.0 },
                }])
            ),
            (
                Absolute,
                PathCommandKind::SmoothQuadraticCurveTo(vec![Point { x: 80.0, y: 80.0 }])
            ),
            (
                Absolute,
                PathCommandKind::EllipticalArcTo(vec![Arc {
                    radii: Point { x: 10.0, y: 20.0 },
                    x_axis_rotation: 0.0,
                    large_arc: true,
                    sweep: false,
                    to: Point { x: 90.0, y: 100.0 },
                }])
            ),
        ]
    );
}