| // Copyright 2024 the Vello Authors |
| // SPDX-License-Identifier: Apache-2.0 OR MIT |
| |
| //! A loader for a tiny fragment of SVG |
| |
| use std::str::FromStr; |
| |
| use roxmltree::{Document, Node}; |
| use vello::{ |
| kurbo::{Affine, BezPath, Point, Size, Vec2}, |
| peniko::Color, |
| }; |
| |
| pub struct PicoSvg { |
| pub items: Vec<Item>, |
| pub size: Size, |
| } |
| |
| pub enum Item { |
| Fill(FillItem), |
| Stroke(StrokeItem), |
| Group(GroupItem), |
| } |
| |
| pub struct StrokeItem { |
| pub width: f64, |
| pub color: Color, |
| pub path: BezPath, |
| } |
| |
| pub struct FillItem { |
| pub color: Color, |
| pub path: BezPath, |
| } |
| |
| pub struct GroupItem { |
| pub affine: Affine, |
| pub children: Vec<Item>, |
| } |
| |
| struct Parser { |
| scale: f64, |
| } |
| |
| impl PicoSvg { |
| pub fn load(xml_string: &str, scale: f64) -> Result<PicoSvg, Box<dyn std::error::Error>> { |
| let doc = Document::parse(xml_string)?; |
| let root = doc.root_element(); |
| let mut parser = Parser::new(scale); |
| let width = root.attribute("width").and_then(|s| f64::from_str(s).ok()); |
| let height = root.attribute("height").and_then(|s| f64::from_str(s).ok()); |
| let (origin, viewbox_size) = root |
| .attribute("viewBox") |
| .and_then(|vb_attr| { |
| let vs: Vec<f64> = vb_attr |
| .split(' ') |
| .map(|s| f64::from_str(s).unwrap()) |
| .collect(); |
| if let &[x, y, width, height] = vs.as_slice() { |
| Some((Point { x, y }, Size { width, height })) |
| } else { |
| None |
| } |
| }) |
| .unzip(); |
| |
| let mut transform = if let Some(origin) = origin { |
| Affine::translate(origin.to_vec2() * -1.0) |
| } else { |
| Affine::IDENTITY |
| }; |
| |
| transform *= match (width, height, viewbox_size) { |
| (None, None, Some(_)) => Affine::IDENTITY, |
| (Some(w), Some(h), Some(s)) => { |
| Affine::scale_non_uniform(1.0 / s.width * w, 1.0 / s.height * h) |
| } |
| (Some(w), None, Some(s)) => Affine::scale(1.0 / s.width * w), |
| (None, Some(h), Some(s)) => Affine::scale(1.0 / s.height * h), |
| _ => Affine::IDENTITY, |
| }; |
| |
| let size = match (width, height, viewbox_size) { |
| (None, None, Some(s)) => s, |
| (mw, mh, None) => Size { |
| width: mw.unwrap_or(300_f64), |
| height: mh.unwrap_or(150_f64), |
| }, |
| (Some(w), None, Some(s)) => Size { |
| width: w, |
| height: 1.0 / w * s.width * s.height, |
| }, |
| (None, Some(h), Some(s)) => Size { |
| width: 1.0 / h * s.height * s.width, |
| height: h, |
| }, |
| (Some(width), Some(height), Some(_)) => Size { width, height }, |
| }; |
| |
| transform *= if scale >= 0.0 { |
| Affine::scale(scale) |
| } else { |
| Affine::new([-scale, 0.0, 0.0, scale, 0.0, 0.0]) |
| }; |
| let props = RecursiveProperties { |
| fill: Some(Color::BLACK), |
| }; |
| // The root element is the svg document element, which we don't care about |
| let mut items = Vec::new(); |
| for node in root.children() { |
| parser.rec_parse(node, &props, &mut items)?; |
| } |
| let root_group = Item::Group(GroupItem { |
| affine: transform, |
| children: items, |
| }); |
| Ok(PicoSvg { |
| items: vec![root_group], |
| size, |
| }) |
| } |
| } |
| |
| #[derive(Clone)] |
| struct RecursiveProperties { |
| fill: Option<Color>, |
| } |
| |
| impl Parser { |
| fn new(scale: f64) -> Parser { |
| Parser { scale } |
| } |
| |
| fn rec_parse( |
| &mut self, |
| node: Node, |
| properties: &RecursiveProperties, |
| items: &mut Vec<Item>, |
| ) -> Result<(), Box<dyn std::error::Error>> { |
| if node.is_element() { |
| let mut properties = properties.clone(); |
| if let Some(fill_color) = node.attribute("fill") { |
| if fill_color == "none" { |
| properties.fill = None; |
| } else { |
| let color = parse_color(fill_color); |
| let color = modify_opacity(color, "fill-opacity", node); |
| // TODO: Handle recursive opacity properly |
| let color = modify_opacity(color, "opacity", node); |
| properties.fill = Some(color); |
| } |
| } |
| match node.tag_name().name() { |
| "g" => { |
| let mut children = Vec::new(); |
| let mut affine = Affine::default(); |
| if let Some(transform) = node.attribute("transform") { |
| affine = parse_transform(transform); |
| } |
| for child in node.children() { |
| self.rec_parse(child, &properties, &mut children)?; |
| } |
| items.push(Item::Group(GroupItem { affine, children })); |
| } |
| "path" => { |
| let d = node.attribute("d").ok_or("missing 'd' attribute")?; |
| let bp = BezPath::from_svg(d)?; |
| let path = bp; |
| if let Some(color) = properties.fill { |
| items.push(Item::Fill(FillItem { |
| color, |
| path: path.clone(), |
| })); |
| } |
| if let Some(stroke_color) = node.attribute("stroke") { |
| if stroke_color != "none" { |
| let width = node |
| .attribute("stroke-width") |
| .map(|a| f64::from_str(a).unwrap_or(1.0)) |
| .unwrap_or(1.0) |
| * self.scale.abs(); |
| let color = parse_color(stroke_color); |
| let color = modify_opacity(color, "stroke-opacity", node); |
| // TODO: Handle recursive opacity properly |
| let color = modify_opacity(color, "opacity", node); |
| items.push(Item::Stroke(StrokeItem { width, color, path })); |
| } |
| } |
| } |
| other => eprintln!("Unhandled node type {other}"), |
| } |
| } |
| Ok(()) |
| } |
| } |
| |
| fn parse_transform(transform: &str) -> Affine { |
| let mut nt = Affine::IDENTITY; |
| for ts in transform.split(')').map(str::trim) { |
| nt *= if let Some(s) = ts.strip_prefix("matrix(") { |
| let vals = s |
| .split([',', ' ']) |
| .map(str::parse) |
| .collect::<Result<Vec<f64>, _>>() |
| .expect("Could parse all values of 'matrix' as floats"); |
| Affine::new( |
| vals.try_into() |
| .expect("Should be six arguments to `matrix`"), |
| ) |
| } else if let Some(s) = ts.strip_prefix("translate(") { |
| if let Ok(vals) = s |
| .split([',', ' ']) |
| .map(str::trim) |
| .map(str::parse) |
| .collect::<Result<Vec<f64>, _>>() |
| { |
| match vals.as_slice() { |
| &[x, y] => Affine::translate(Vec2 { x, y }), |
| _ => Affine::IDENTITY, |
| } |
| } else { |
| Affine::IDENTITY |
| } |
| } else if let Some(s) = ts.strip_prefix("scale(") { |
| if let Ok(vals) = s |
| .split([',', ' ']) |
| .map(str::trim) |
| .map(str::parse) |
| .collect::<Result<Vec<f64>, _>>() |
| { |
| match *vals.as_slice() { |
| [x, y] => Affine::scale_non_uniform(x, y), |
| [x] => Affine::scale(x), |
| _ => Affine::IDENTITY, |
| } |
| } else { |
| Affine::IDENTITY |
| } |
| } else if let Some(s) = ts.strip_prefix("scaleX(") { |
| s.trim() |
| .parse() |
| .ok() |
| .map(|x| Affine::scale_non_uniform(x, 1.0)) |
| .unwrap_or(Affine::IDENTITY) |
| } else if let Some(s) = ts.strip_prefix("scaleY(") { |
| s.trim() |
| .parse() |
| .ok() |
| .map(|y| Affine::scale_non_uniform(1.0, y)) |
| .unwrap_or(Affine::IDENTITY) |
| } else { |
| if !ts.is_empty() { |
| eprintln!("Did not understand transform attribute {ts:?})"); |
| } |
| Affine::IDENTITY |
| }; |
| } |
| nt |
| } |
| |
| fn parse_color(color: &str) -> Color { |
| let color = color.trim(); |
| if let Some(c) = Color::parse(color) { |
| c |
| } else if let Some(s) = color.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')) { |
| let mut iter = s.split([',', ' ']).map(str::trim).map(u8::from_str); |
| |
| let r = iter.next().unwrap().unwrap(); |
| let g = iter.next().unwrap().unwrap(); |
| let b = iter.next().unwrap().unwrap(); |
| Color::rgb8(r, g, b) |
| } else { |
| Color::rgba8(255, 0, 255, 0x80) |
| } |
| } |
| |
| fn modify_opacity(mut color: Color, attr_name: &str, node: Node) -> Color { |
| if let Some(opacity) = node.attribute(attr_name) { |
| let alpha: f64 = if let Some(o) = opacity.strip_suffix('%') { |
| let pctg = o.parse().unwrap_or(100.0); |
| pctg * 0.01 |
| } else { |
| opacity.parse().unwrap_or(1.0) |
| }; |
| color.a = (alpha.clamp(0.0, 1.0) * 255.0).round() as u8; |
| color |
| } else { |
| color |
| } |
| } |