2020-05-01 10:06:01 +10:00
|
|
|
//! A loader for a tiny fragment of SVG
|
|
|
|
|
|
|
|
use std::str::FromStr;
|
|
|
|
|
2020-05-29 04:48:36 +10:00
|
|
|
use roxmltree::{Document, Node};
|
2020-05-01 10:06:01 +10:00
|
|
|
|
2020-05-13 01:26:48 +10:00
|
|
|
use piet::kurbo::{Affine, BezPath};
|
2020-05-01 10:06:01 +10:00
|
|
|
|
|
|
|
use piet::{Color, RenderContext};
|
|
|
|
|
|
|
|
pub struct PicoSvg {
|
|
|
|
items: Vec<Item>,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub enum Item {
|
|
|
|
Fill(FillItem),
|
|
|
|
Stroke(StrokeItem),
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct StrokeItem {
|
|
|
|
width: f64,
|
|
|
|
color: Color,
|
|
|
|
path: BezPath,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct FillItem {
|
|
|
|
color: Color,
|
|
|
|
path: BezPath,
|
|
|
|
}
|
|
|
|
|
2020-05-29 04:48:36 +10:00
|
|
|
struct Parser<'a> {
|
2020-06-01 02:55:20 +10:00
|
|
|
scale: f64,
|
2020-05-29 04:48:36 +10:00
|
|
|
items: &'a mut Vec<Item>,
|
|
|
|
}
|
|
|
|
|
2020-05-01 10:06:01 +10:00
|
|
|
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 items = Vec::new();
|
2020-06-01 02:55:20 +10:00
|
|
|
let mut parser = Parser::new(&mut items, scale);
|
2020-05-29 04:48:36 +10:00
|
|
|
for node in root.children() {
|
|
|
|
parser.rec_parse(node)?;
|
2020-05-01 10:06:01 +10:00
|
|
|
}
|
|
|
|
Ok(PicoSvg { items })
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn render(&self, rc: &mut impl RenderContext) {
|
|
|
|
for item in &self.items {
|
|
|
|
match item {
|
|
|
|
Item::Fill(fill_item) => {
|
2020-05-21 00:38:52 +10:00
|
|
|
rc.fill(&fill_item.path, &fill_item.color);
|
|
|
|
//rc.stroke(&fill_item.path, &fill_item.color, 1.0);
|
2020-05-01 10:06:01 +10:00
|
|
|
}
|
|
|
|
Item::Stroke(stroke_item) => {
|
|
|
|
rc.stroke(&stroke_item.path, &stroke_item.color, stroke_item.width);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-29 04:48:36 +10:00
|
|
|
impl<'a> Parser<'a> {
|
2020-06-01 02:55:20 +10:00
|
|
|
fn new(items: &'a mut Vec<Item>, scale: f64) -> Parser<'a> {
|
|
|
|
Parser { scale, items }
|
2020-05-29 04:48:36 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
fn rec_parse(&mut self, node: Node) -> Result<(), Box<dyn std::error::Error>> {
|
2020-06-01 02:55:20 +10:00
|
|
|
let transform = if self.scale >= 0.0 {
|
|
|
|
Affine::scale(self.scale)
|
|
|
|
} else {
|
|
|
|
Affine::new([-self.scale, 0.0, 0.0, self.scale, 0.0, 1536.0])
|
|
|
|
};
|
2020-05-29 04:48:36 +10:00
|
|
|
if node.is_element() {
|
|
|
|
match node.tag_name().name() {
|
|
|
|
"g" => {
|
|
|
|
for child in node.children() {
|
|
|
|
self.rec_parse(child)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
"path" => {
|
|
|
|
let d = node.attribute("d").ok_or("missing 'd' attribute")?;
|
|
|
|
let bp = BezPath::from_svg(d)?;
|
2020-06-01 02:55:20 +10:00
|
|
|
let path = transform * bp;
|
2020-05-29 04:48:36 +10:00
|
|
|
// TODO: default fill color is black, but this is overridden in tiger to this logic.
|
|
|
|
if let Some(fill_color) = node.attribute("fill") {
|
|
|
|
if fill_color != "none" {
|
|
|
|
let color = parse_color(fill_color);
|
|
|
|
let color = modify_opacity(color, "fill-opacity", node);
|
2020-06-01 02:55:20 +10:00
|
|
|
self.items.push(Item::Fill(FillItem {
|
|
|
|
color,
|
|
|
|
path: path.clone(),
|
|
|
|
}));
|
2020-05-29 04:48:36 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if let Some(stroke_color) = node.attribute("stroke") {
|
|
|
|
if stroke_color != "none" {
|
2020-06-01 02:55:20 +10:00
|
|
|
let width = self.scale.abs()
|
|
|
|
* f64::from_str(
|
|
|
|
node.attribute("stroke-width").ok_or("missing width")?,
|
|
|
|
)?;
|
2020-05-29 04:48:36 +10:00
|
|
|
let color = parse_color(stroke_color);
|
|
|
|
let color = modify_opacity(color, "stroke-opacity", node);
|
2020-06-01 02:55:20 +10:00
|
|
|
self.items
|
|
|
|
.push(Item::Stroke(StrokeItem { width, color, path }));
|
2020-05-29 04:48:36 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-01 10:06:01 +10:00
|
|
|
fn parse_color(color: &str) -> Color {
|
|
|
|
if color.as_bytes()[0] == b'#' {
|
|
|
|
let mut hex = u32::from_str_radix(&color[1..], 16).unwrap();
|
|
|
|
if color.len() == 4 {
|
|
|
|
hex = (hex >> 8) * 0x110000 + ((hex >> 4) & 0xf) * 0x1100 + (hex & 0xf) * 0x11;
|
|
|
|
}
|
|
|
|
Color::from_rgba32_u32((hex << 8) + 0xff)
|
2020-05-29 04:48:36 +10:00
|
|
|
} else if color.starts_with("rgb(") {
|
|
|
|
let mut iter = color[4..color.len() - 1].split(',');
|
|
|
|
let r = u8::from_str(iter.next().unwrap()).unwrap();
|
|
|
|
let g = u8::from_str(iter.next().unwrap()).unwrap();
|
|
|
|
let b = u8::from_str(iter.next().unwrap()).unwrap();
|
|
|
|
Color::rgb8(r, g, b)
|
2020-05-01 10:06:01 +10:00
|
|
|
} else {
|
|
|
|
Color::from_rgba32_u32(0xff00ff80)
|
|
|
|
}
|
|
|
|
}
|
2020-05-29 04:48:36 +10:00
|
|
|
|
|
|
|
fn modify_opacity(color: Color, attr_name: &str, node: Node) -> Color {
|
|
|
|
if let Some(opacity) = node.attribute(attr_name) {
|
|
|
|
let alpha = if opacity.ends_with("%") {
|
|
|
|
let pctg = opacity[..opacity.len() - 1].parse().unwrap_or(100.0);
|
|
|
|
pctg * 0.01
|
|
|
|
} else {
|
|
|
|
opacity.parse().unwrap_or(1.0)
|
|
|
|
};
|
|
|
|
color.with_alpha(alpha)
|
|
|
|
} else {
|
|
|
|
color
|
|
|
|
}
|
|
|
|
}
|