pizarra 0.6.3

The backend for a simple vector hand-drawing application
Documentation
use std::fmt;
use std::collections::HashMap;
use std::str::FromStr;

use xml::reader::{EventReader, XmlEvent, Error as XmlError};
use regex::Regex;
use lazy_static::lazy_static;

use crate::storage::Storage;
use crate::shape::line::Line;
use crate::color::Color;
use crate::consts::DEFAULT_THICKNESS;
use crate::point::Point;

lazy_static! {
    static ref CSS_COLOR_REGEX: Regex = Regex::new(r"(?x)#
        (?P<r>[\dA-Fa-f]{2})
        (?P<g>[\dA-Fa-f]{2})
        (?P<b>[\dA-Fa-f]{2})
    ").unwrap();
    static ref COLOR_REGEX: Regex = Regex::new(r"(?x)rgb\(
        (?P<r>\d+(\.\d+)?)
        %\s*,\s*
        (?P<g>\d+(\.\d+))?
        %\s*,\s*
        (?P<b>\d+(\.\d+))?
    %\s*\)").unwrap();
    static ref POINT_REGEX: Regex = Regex::new(r"(?x)
        (?P<x>-?\d+(\.\d+)?)
        \s+
        (?P<y>-?\d+(\.\d+)?)
    ").unwrap();
}

#[derive(Debug)]
pub enum Error {
    Xml(XmlError),
    PathWithNoDAttr,
    ColorDoesntMatchRegex,
    ParseFloatError(std::num::ParseFloatError),
    PointDoesntMatchRegex,
}

impl From<XmlError> for Error {
    fn from(error: XmlError) -> Error {
        Error::Xml(error)
    }
}

impl From<std::num::ParseFloatError> for Error {
    fn from(error: std::num::ParseFloatError) -> Self {
        Error::ParseFloatError(error)
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Load error caused by {:?}", self)
    }
}

impl std::error::Error for Error {
}

impl FromStr for Color {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if let Some(captures) = COLOR_REGEX.captures(s) {
            Ok(Color::from_rgb(
                captures["r"].parse::<f64>()?/ 100.0,
                captures["g"].parse::<f64>()?/100.0,
                captures["b"].parse::<f64>()?/100.0,
            ))
        } else {
            if let Some(captures) = CSS_COLOR_REGEX.captures(s) {
                Ok(Color::from_rgb(
                    u32::from_str_radix(&captures["r"], 16).unwrap() as f64 / 255.0,
                    u32::from_str_radix(&captures["g"], 16).unwrap() as f64 / 255.0,
                    u32::from_str_radix(&captures["b"], 16).unwrap() as f64 / 255.0,
                ))
            } else {
                Err(Error::ColorDoesntMatchRegex)
            }
        }
    }
}

impl FromStr for Point {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if let Some(captures) = POINT_REGEX.captures(s) {
            Ok(Point::new(
                captures["x"].parse::<f64>()?,
                captures["y"].parse::<f64>()?,
            ))
        } else {
            Err(Error::PointDoesntMatchRegex)
        }
    }
}

impl Line {
    pub fn from_xml_attributes(style: &str, path: &str) -> Result<Line, Error> {
        let attrs: HashMap<_, _> = style.split(';').filter_map(|s| {
            let pieces: Vec<_> = s.split(':').collect();

            if pieces.len() != 2 {
                return None
            }

            Some((pieces[0].trim(), pieces[1].trim()))
        }).collect();

        let color = if let Some(color_str) = attrs.get("stroke") {
                color_str.parse()?
            } else {
                Color::white()
            };

        let thickness = if let Some(thickness_str) = attrs.get("stroke-width") {
                thickness_str.parse()?
            } else {
                DEFAULT_THICKNESS
            };

        let points = path
            .split('L')
            .map(|point_str| point_str.parse())
            .collect::<Result<Vec<Point>, _>>()?;

        Ok(Line::with_params(color, points, thickness))
    }
}

impl Storage {
    pub fn from_svg(svg: &str) -> Result<Storage, Error> {
        let parser = EventReader::from_str(svg);
        let mut storage = Storage::new();

        for e in parser {
            match e {
                Ok(XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "path" => {
                    if let Some(styleattr) = attributes.iter().find(|a| a.name.local_name == "style") {
                        if let Some(dattr) = attributes.iter().find(|a| a.name.local_name == "d") {
                            storage.add(Box::new(Line::from_xml_attributes(&styleattr.value, &dattr.value)?));
                        }
                    } else {
                    }
                }
                Err(e) => {
                    return Err(e.into());
                }
                _ => {}
            }
        }

        storage.flush();

        Ok(storage)
    }
}

#[cfg(test)]
mod tests {
    use std::rc::Rc;

    use crate::storage::Storage;
    use crate::shape::{Line, DrawCommand, ShapeTrait};
    use crate::color::Color;
    use crate::point::Point;

    #[test]
    fn test_from_svg() {
        let svg_data = include_str!("../res/simple_file_load.svg");
        let storage = Storage::from_svg(svg_data).unwrap();

        assert_eq!(storage.shape_count(), 1);
    }

    #[test]
    fn test_from_custom_svg() {
        let svg_data = include_str!("../res/serialize_test.svg");
        let storage = Storage::from_svg(svg_data).unwrap();

        assert_eq!(storage.shape_count(), 1);
    }

    #[test]
    fn test_point_bug() {
        let svg_data = include_str!("../res/bug_opening.svg");
        let storage = Storage::from_svg(svg_data).unwrap();

        assert_eq!(storage.shape_count(), 4);
    }

    #[test]
    fn test_line_from_xml_attributes() {
        let line = Line::from_xml_attributes("fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(53.90625%,88.28125%,20.3125%);stroke-opacity:1;stroke-miterlimit:10;", "M 147.570312 40.121094 L 146.9375 40.121094 L 145.296875 40.460938 L 142.519531 41.710938 L 139.613281 43.304688 L 138.097656 44.894531 L 137.339844 46.714844 L 138.097656 47.621094 L 139.992188 47.851562 L 142.898438 47.621094 L 146.179688 47.167969 L 150.097656 47.964844 L 151.738281 49.667969 L 152.496094 51.714844 L 152.875 53.191406 ").unwrap();

        if let DrawCommand::Line {
            color,
            line,
            thickness,
        } = line.draw_commands() {
            assert_eq!(color, Color::from_rgb(0.5390625, 0.8828125, 0.203125));
            assert_eq!(line, Rc::new(vec![Point::new(147.570312, 40.121094), Point::new(146.9375, 40.121094), Point::new(145.296875, 40.460938), Point::new(142.519531, 41.710938), Point::new(139.613281, 43.304688), Point::new(138.097656, 44.894531), Point::new(137.339844, 46.714844), Point::new(138.097656, 47.621094), Point::new(139.992188, 47.851562), Point::new(142.898438, 47.621094), Point::new(146.179688, 47.167969), Point::new(150.097656, 47.964844), Point::new(151.738281, 49.667969), Point::new(152.496094, 51.714844), Point::new(152.875, 53.191406)]));
            assert_eq!(thickness, 3.0);
        } else {
            panic!();
        }
    }

    #[test]
    fn test_parse_color() {
        let color: Color = "rgb(53.90625%,88.28125%,20.3125%)".parse().unwrap();

        assert_eq!(color, Color::from_rgb(0.5390625, 0.8828125, 0.203125));
    }

    #[test]
    fn test_parse_css_color() {
        let color: Color = "#FF2030".parse().unwrap();

        assert_eq!(color, Color::from_rgb(1.0, 0.12549019607843137, 0.18823529411764706));

        let color: Color = "#FF2003".parse().unwrap();

        assert_eq!(color, Color::from_rgb(1.0, 0.12549019607843137, 0.011764705882352941));
    }

    #[test]
    fn test_parse_point() {
        let p = " 152.496094 51.714844 ";

        assert_eq!(p.parse::<Point>().unwrap(), Point::new(152.496094, 51.714844));
    }
}