pizarra 2.0.4

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

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

use crate::storage::Storage;
use crate::shape::{ShapeStored, stored::{path::Path, ellipse::Ellipse}};
use crate::color::Color;
use crate::point::{Vec2D, Unit, WorldUnit};
use crate::geom::Angle;
use crate::config::Config;
use crate::style::{Style, Stroke};

mod path;

use path::{parse_path, PathBuilder as SvgPathBuilder};

lazy_static! {
    static ref POINT_REGEX: Regex = Regex::new(r"(?x)
        (?P<x>-?\d+(\.\d+)?)
        \s+
        (?P<y>-?\d+(\.\d+)?)
    ").unwrap();
    static ref ANGLE_REGEX: Regex = Regex::new(r"(?x)
        rotate\(
            (?P<angle>-?\d+(\.\d*)?)
            (\s+(-?\d+(\.\d*)?)\s+(-?\d+(\.\d*)?))?
        \)
    ").unwrap();
}

#[derive(Debug)]
pub enum Error {
    Xml(XmlError),
    PathWithNoDAttr,
    CouldntUnderstandColor(String),
    ParseFloatError(std::num::ParseFloatError),
    PointDoesntMatchRegex,
    AngleDoesntMatchRegex(String),
    MissingAttribute(String),
    PathParseError(path::ParseError),
}

impl From<path::ParseError> for Error {
    fn from(error: path::ParseError) -> Error {
        Error::PathParseError(error)
    }
}

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 {
}

type Result<T> = std::result::Result<T, Error>;

impl FromStr for Vec2D<WorldUnit> {
    type Err = Error;

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

impl FromStr for Angle {
    type Err = Error;

    fn from_str(s: &str) -> Result<Angle> {
        if let Some(angle) = ANGLE_REGEX.captures(s) {
            Ok(Angle::from_degrees(angle["angle"].parse()?))
        } else {
            Err(Error::AngleDoesntMatchRegex(s.into()))
        }
    }
}

fn css_attrs_as_hashmap(attrs: &str) -> HashMap<&str, &str> {
    attrs.split(';').filter_map(|s| {
        let pieces: Vec<_> = s.split(':').collect();

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

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

fn xml_attrs_as_hashmap(attrs: Vec<OwnedAttribute>) -> HashMap<String, String> {
    attrs.into_iter().map(|a| {
        (a.name.local_name, a.value)
    }).collect()
}

fn parse_color(color_str: &str) -> Result<Option<Color>> {
    if color_str.to_lowercase() == "none" {
        Ok(None)
    } else {
        Ok(Some(color_str.parse()?))
    }
}

impl Path {
    pub fn from_xml_attributes(style: &str, path: &str, config: Config) -> Result<Path> {
        let attrs = css_attrs_as_hashmap(style);

        let color: Option<Color> = attrs.get("stroke").map(|s| parse_color(s)).transpose()?.flatten();
        let fill: Option<Color> = attrs.get("fill").map(|s| parse_color(s)).transpose()?.flatten();
        let thickness: WorldUnit = attrs.get("stroke-width").map(|s| s.parse()).transpose()?.unwrap_or_else(|| config.thickness.val().into());
        let alpha = attrs.get("stroke-opacity").map(|s| s.parse()).transpose()?.unwrap_or(1.0);
        let fill_alpha = attrs.get("fill-opacity").map(|s| s.parse()).transpose()?.unwrap_or(1.0);

        let mut builder = SvgPathBuilder::new();

        parse_path(path, &mut builder)?;

        Ok(Path::from_parts(
            builder.into_path(),
            Style {
                stroke: color.map(|c| Stroke {
                    color: c.with_float_alpha(alpha),
                    size: thickness,
                }),
                fill: fill.map(|c| c.with_float_alpha(fill_alpha)),
            },
        ))
    }
}

trait Deserialize {
    fn deserialize(attributes: HashMap<String, String>, config: Config) -> Result<Box<dyn ShapeStored>>;
}

impl Deserialize for Path {
    fn deserialize(attributes: HashMap<String, String>, config: Config) -> Result<Box<dyn ShapeStored>> {
        let styleattr = attributes.get("style").ok_or_else(|| Error::MissingAttribute("style".into()))?;
        let dattr = attributes.get("d").ok_or_else(|| Error::MissingAttribute("d".into()))?;

        Ok(Box::new(Path::from_xml_attributes(styleattr, dattr, config)?))
    }
}

impl Deserialize for Ellipse {
    fn deserialize(attributes: HashMap<String, String>, config: Config) -> Result<Box<dyn ShapeStored>> {
        let cx = attributes.get("cx").ok_or_else(|| Error::MissingAttribute("cx".into()))?;
        let cy = attributes.get("cy").ok_or_else(|| Error::MissingAttribute("cy".into()))?;
        let rx = attributes.get("rx").ok_or_else(|| Error::MissingAttribute("rx".into()))?;
        let ry = attributes.get("ry").ok_or_else(|| Error::MissingAttribute("ry".into()))?;
        let angle= attributes.get("transform").map(|s| s.as_str()).unwrap_or("rotate(0)");
        let styleattr = attributes.get("style").ok_or_else(|| Error::MissingAttribute("style".into()))?;

        let attrs = css_attrs_as_hashmap(styleattr);

        let color: Option<Color> = attrs.get("stroke").map(|s| parse_color(s)).transpose()?.flatten();
        let fill: Option<Color> = attrs.get("fill").map(|s| parse_color(s)).transpose()?.flatten();
        let thickness: WorldUnit = attrs.get("stroke-width").map(|s| s.parse()).transpose()?.unwrap_or_else(|| config.thickness.val().into());
        let alpha = attrs.get("stroke-opacity").map(|s| s.parse()).transpose()?.unwrap_or(1.0);
        let fill_alpha = attrs.get("fill-opacity").map(|s| s.parse()).transpose()?.unwrap_or(1.0);

        let angle: Angle = angle.parse()?;

        let center = Vec2D::new_world(cx.parse()?, cy.parse()?);
        let rx = rx.parse()?;
        let ry = ry.parse()?;

        Ok(Box::new(Ellipse::from_parts(
            center,
            rx,
            ry,
            angle,
            Style {
                stroke: color.map(|c| Stroke {
                    color: c.with_float_alpha(alpha),
                    size: thickness,
                }),
                fill: fill.map(|c| c.with_float_alpha(fill_alpha)),
            },
        )))
    }
}

impl Storage {
    pub fn from_svg(svg: &str, config: Config) -> Result<Storage> {
        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" => {
                    storage.add(Path::deserialize(xml_attrs_as_hashmap(attributes), config)?);
                }
                Ok(XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "ellipse" => {
                    storage.add(Ellipse::deserialize(xml_attrs_as_hashmap(attributes), config)?);
                }
                Err(e) => {
                    return Err(e.into());
                }
                _ => {}
            }
        }

        Ok(storage)
    }
}

#[cfg(test)]
mod tests {
    use crate::path_command::PathCommand;
    use crate::draw_commands::DrawCommand;

    use super::*;

    #[test]
    fn test_from_svg() {
        let svg_data = include_str!("../res/simple_file_load.svg");
        let storage = Storage::from_svg(svg_data, Default::default()).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, Default::default()).unwrap();

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

    #[test]
    fn test_can_deserialize_circle() {
        let svg_data = include_str!("../res/circle.svg");
        let storage = Storage::from_svg(svg_data, Default::default()).unwrap();

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

    #[test]
    fn test_can_deserialize_ellipse() {
        let svg_data = include_str!("../res/ellipse.svg");
        let storage = Storage::from_svg(svg_data, Default::default()).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, Default::default()).unwrap();

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

    #[test]
    fn test_line_from_xml_attributes() {
        let line = Path::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 ", Default::default()).unwrap();

        if let DrawCommand::Path {
            commands, style, ..
        } = line.draw_commands() {
            assert_eq!(style.stroke.unwrap().color, Color::from_float_rgb(0.5390625, 0.8828125, 0.203125));
            assert_eq!(commands, vec![
                PathCommand::MoveTo(Vec2D::new_world(147.570312, 40.121094)),
                PathCommand::LineTo(Vec2D::new_world(146.9375, 40.121094)),
                PathCommand::LineTo(Vec2D::new_world(145.296875, 40.460938)),
                PathCommand::LineTo(Vec2D::new_world(142.519531, 41.710938)),
                PathCommand::LineTo(Vec2D::new_world(139.613281, 43.304688)),
                PathCommand::LineTo(Vec2D::new_world(138.097656, 44.894531)),
                PathCommand::LineTo(Vec2D::new_world(137.339844, 46.714844)),
                PathCommand::LineTo(Vec2D::new_world(138.097656, 47.621094)),
                PathCommand::LineTo(Vec2D::new_world(139.992188, 47.851562)),
                PathCommand::LineTo(Vec2D::new_world(142.898438, 47.621094)),
                PathCommand::LineTo(Vec2D::new_world(146.179688, 47.167969)),
                PathCommand::LineTo(Vec2D::new_world(150.097656, 47.964844)),
                PathCommand::LineTo(Vec2D::new_world(151.738281, 49.667969)),
                PathCommand::LineTo(Vec2D::new_world(152.496094, 51.714844)),
                PathCommand::LineTo(Vec2D::new_world(152.875, 53.191406)),
            ]);
            assert_eq!(style.stroke.unwrap().size, 3.0.into());
        } else {
            panic!();
        }
    }

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

        assert_eq!(p.parse::<Vec2D<WorldUnit>>().unwrap(), Vec2D::new_world(152.496094, 51.714844));
    }

    #[test]
    fn test_parse_alpha() {
        let line = Path::from_xml_attributes("fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke:#FF0000;stroke-opacity:0.8;stroke-miterlimit:10;", "M 10 10 L 20 20", Default::default()).unwrap();

        assert_eq!(line.style().stroke.unwrap().color, Color::red().with_float_alpha(0.8));
    }

    #[test]
    fn can_deserialize_rotated_ellipse() {
        let attrs: HashMap<String, String> = vec![
            ("cx".into(), "20.4".into()),
            ("cy".into(), "-30.5".into()),
            ("rx".into(), "5.6".into()),
            ("ry".into(), "3.5".into()),
            ("transform".into(), "rotate(34.5)".into()),
            ("style".into(), "stroke:#cabada;stroke-width:3.5;stroke-opacity:0.8".into()),
        ].into_iter().collect();

        let deserialized = Ellipse::deserialize(attrs, Default::default()).unwrap();

        match deserialized.draw_commands() {
            DrawCommand::Ellipse { ellipse: e, style } => {
                assert_eq!(e.center, Vec2D::new_world(20.4, -30.5));
                assert_eq!(e.semimajor, 5.6.into());
                assert_eq!(e.semiminor, 3.5.into());
                assert_eq!(e.angle.degrees(), 34.5);
                assert_eq!(style.stroke.unwrap().color, Color::from_int_rgb(0xca, 0xba, 0xda).with_float_alpha(0.8));
                assert_eq!(style.stroke.unwrap().size, 3.5.into());
            },
            _ => panic!()
        }
    }

    #[test]
    fn can_deserialize_ellipse_serialization_test() {
        let svg_data = include_str!("../res/serialize_ellipse.svg");

        Storage::from_svg(svg_data, Default::default()).unwrap();
    }

    #[test]
    fn fill_and_stroke_can_be_none_in_path() {
        let svg_data = include_str!("../res/color_fill_none_path.svg");
        let storage = Storage::from_svg(svg_data, Default::default()).unwrap();

        let commands = storage.draw_commands(storage.get_bounds().unwrap());
        let command = &commands[0];

        assert_eq!(command.color(), None);
        assert_eq!(command.fill(), None);
    }

    #[test]
    fn fill_and_stroke_can_be_none_in_ellipse() {
        let svg_data = include_str!("../res/color_fill_none_ellipse.svg");
        let storage = Storage::from_svg(svg_data, Default::default()).unwrap();

        let commands = storage.draw_commands(storage.get_bounds().unwrap());
        let command = &commands[0];

        assert_eq!(command.color(), None);
        assert_eq!(command.fill(), None);
    }
}