blob: ee149c0401c30b0af128b705235b5bd31c11bbcc [file] [log] [blame]
// Copyright 2024 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
// Below is copied, lightly adapted, from Vello.
//! A minimal SVG parser for rendering examples
//!
//! This module provides a simple SVG parser to load and render SVG files
//! for demonstration purposes. It supports basic SVG features like paths,
//! fill, stroke, and grouping.
extern crate std;
use alloc::boxed::Box;
use alloc::vec;
use alloc::vec::Vec;
use core::str::FromStr;
use std::eprintln;
use roxmltree::{Document, Node};
use vello_api::kurbo::{Affine, BezPath, Point, Size, Vec2};
use vello_api::peniko::color::{AlphaColor, DynamicColor, Srgb, palette};
/// A simplified representation of an SVG document
#[derive(Debug)]
pub struct PicoSvg {
/// The items (shapes, groups) contained in the SVG
pub items: Vec<Item>,
/// The size of the SVG document
pub size: Size,
}
/// Represents a single item in an SVG document
#[derive(Debug)]
pub enum Item {
/// A filled shape
Fill(FillItem),
/// A stroked shape
Stroke(StrokeItem),
/// A group of items
Group(GroupItem),
}
/// A stroke item with styling information
#[derive(Debug)]
pub struct StrokeItem {
/// The width of the stroke
pub width: f64,
/// The color of the stroke
pub color: AlphaColor<Srgb>,
/// The path to be stroked
pub path: BezPath,
}
/// A fill item with styling information
#[derive(Debug)]
pub struct FillItem {
/// The color to fill with
pub color: AlphaColor<Srgb>,
/// The path to be filled
pub path: BezPath,
}
/// A group of items that can be transformed together
#[derive(Debug)]
pub struct GroupItem {
/// The affine transformation to apply to all children
pub affine: Affine,
/// The child items in this group
pub children: Vec<Item>,
}
struct Parser {
scale: f64,
}
impl PicoSvg {
/// Load an SVG document from a string
pub fn load(xml_string: &str, scale: f64) -> Result<Self, Box<dyn core::error::Error>> {
let doc = Document::parse(xml_string)?;
let root = doc.root_element();
let mut parser = Parser::new(scale);
let root_width = root.attribute("width").and_then(|s| f64::from_str(s).ok());
let root_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 (root_width, root_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 (root_width, root_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(palette::css::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(Self {
items: vec![root_group],
size,
})
}
}
#[derive(Clone)]
struct RecursiveProperties {
fill: Option<AlphaColor<Srgb>>,
}
impl Parser {
fn new(scale: f64) -> Self {
Self { scale }
}
fn rec_parse(
&mut self,
node: Node<'_, '_>,
properties: &RecursiveProperties,
items: &mut Vec<Item>,
) -> Result<(), Box<dyn core::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) -> AlphaColor<Srgb> {
let color = color.trim();
vello_api::peniko::color::parse_color(color.trim())
.map(DynamicColor::to_alpha_color)
.unwrap_or(palette::css::FUCHSIA.with_alpha(0.5))
}
fn modify_opacity(
color: AlphaColor<Srgb>,
attr_name: &str,
node: Node<'_, '_>,
) -> AlphaColor<Srgb> {
if let Some(opacity) = node.attribute(attr_name) {
let alpha: f32 = 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.with_alpha(alpha.clamp(0., 1.))
} else {
color
}
}
#[cfg(test)]
mod tests {
use super::parse_color;
use vello_api::peniko::color::{AlphaColor, Srgb, palette};
fn assert_close_color(c1: AlphaColor<Srgb>, c2: AlphaColor<Srgb>) {
const EPSILON: f32 = 1e-4;
assert_eq!(c1.cs, c2.cs);
for i in 0..4 {
assert!((c1.components[i] - c2.components[i]).abs() < EPSILON);
}
}
#[test]
fn parse_colors() {
let lime = palette::css::LIME;
let lime_a = lime.with_alpha(0.4);
let named = parse_color("lime");
assert_close_color(lime, named);
let hex = parse_color("#00ff00");
assert_close_color(lime, hex);
let rgb = parse_color("rgb(0, 255, 0)");
assert_close_color(lime, rgb);
let modern = parse_color("color(srgb 0 1 0)");
assert_close_color(lime, modern);
let modern_a = parse_color("color(srgb 0 1 0 / 0.4)");
assert_close_color(lime_a, modern_a);
}
}