From b70487d10f5f1280d157d26859a8dacb4587fc29 Mon Sep 17 00:00:00 2001 From: James Lawrence Date: Mon, 3 Jan 2022 00:20:00 -0500 Subject: [PATCH 1/2] use resvg to display svg. --- druid/Cargo.toml | 6 +- druid/examples/svg.rs | 4 +- druid/src/widget/svg.rs | 613 ++++++++-------------------------------- 3 files changed, 129 insertions(+), 494 deletions(-) diff --git a/druid/Cargo.toml b/druid/Cargo.toml index 4587d77d2f..af25809bb7 100644 --- a/druid/Cargo.toml +++ b/druid/Cargo.toml @@ -24,7 +24,7 @@ cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples=examples"] default = ["gtk"] gtk = ["druid-shell/gtk"] image = ["druid-shell/image"] -svg = ["usvg"] +svg = ["usvg", "resvg", "tiny-skia"] x11 = ["druid-shell/x11"] # **WARNING** not ready for the prime time. Many things don't work yet. wayland = ["druid-shell/wayland"] @@ -71,7 +71,9 @@ instant = { version = "0.1.12", features = ["wasm-bindgen"] } # Optional dependencies chrono = { version = "0.4.23", optional = true } im = { version = "15.1.0", optional = true } -usvg = { version = "0.14.1", optional = true } +resvg = { version = "0.19.0", optional = true } +usvg = { version = "0.19.0", optional = true } +tiny-skia = { version = "0.6.1", optional = true } [target.'cfg(target_arch="wasm32")'.dependencies] tracing-wasm = { version = "0.2.1" } diff --git a/druid/examples/svg.rs b/druid/examples/svg.rs index 83f7d5ddba..3c17476430 100644 --- a/druid/examples/svg.rs +++ b/druid/examples/svg.rs @@ -20,7 +20,7 @@ use tracing::error; use druid::{ - widget::{FillStrat, Flex, Svg, SvgData, WidgetExt}, + widget::{Flex, Svg, SvgData, WidgetExt}, AppLauncher, LocalizedString, Widget, WindowDesc, }; @@ -47,7 +47,7 @@ fn ui_builder() -> impl Widget { let mut col = Flex::column(); col.add_flex_child(Svg::new(tiger_svg.clone()).fix_width(60.0).center(), 1.0); - col.add_flex_child(Svg::new(tiger_svg.clone()).fill_mode(FillStrat::Fill), 1.0); + col.add_flex_child(Svg::new(tiger_svg.clone()), 1.0); col.add_flex_child(Svg::new(tiger_svg), 1.0); col.debug_paint_layout() } diff --git a/druid/src/widget/svg.rs b/druid/src/widget/svg.rs index 6713ef1c8f..d02a8cc623 100644 --- a/druid/src/widget/svg.rs +++ b/druid/src/widget/svg.rs @@ -14,557 +14,190 @@ //! An SVG widget. -use std::{collections::HashMap, error::Error, rc::Rc, str::FromStr, sync::Arc}; -use tracing::{instrument, trace}; +use druid; +use druid::RenderContext; +use resvg; -use crate::{ - kurbo::BezPath, - piet::{self, GradientStop, LineCap, LineJoin, LinearGradient, RadialGradient, StrokeStyle}, - widget::common::FillStrat, - widget::prelude::*, - Affine, Color, Data, Rect, UnitPoint, -}; +#[allow(dead_code)] +pub fn new(data: impl Into>) -> Svg { + Svg::new(data.into()) +} + +#[allow(dead_code)] +pub fn from_str(s: &str) -> Result::Err> { + use std::str::FromStr; + SvgData::from_str(s) +} /// A widget that renders a SVG pub struct Svg { - svg_data: SvgData, - fill: FillStrat, + tree: std::sync::Arc, + default_size: druid::Size, + cached: Option<(druid::Size, druid::piet::ImageBuf)>, } impl Svg { /// Create an SVG-drawing widget from SvgData. /// /// The SVG will scale to fit its box constraints. - pub fn new(svg_data: SvgData) -> Self { + pub fn new(tree: impl Into>) -> Self { + let tree = tree.into(); Svg { - svg_data, - fill: FillStrat::default(), + default_size: Svg::size(&tree), + cached: None::<(druid::Size, druid::piet::ImageBuf)>, + tree, } } - /// Builder-style method for specifying the fill strategy. - pub fn fill_mode(mut self, mode: FillStrat) -> Self { - self.fill = mode; - self + /// Get the size of the svg. This is the size that the svg requests to be drawn. If it is + /// different from the viewbox size, then scaling will be required. + fn size(tree: &std::sync::Arc) -> druid::Size { + let root = tree.root(); + let rect = match *root.borrow() { + usvg::NodeKind::Svg(svg) => { + let s = svg.size; + druid::Size::new(s.width(), s.height()) + } + _ => { + tracing::error!( + "this SVG has no size. It is expected that usvg always adds a size" + ); + druid::Size::ZERO + } + }; + rect } - /// Modify the widget's `FillStrat`. - pub fn set_fill_mode(&mut self, newfil: FillStrat) { - self.fill = newfil; - } + fn render(&self, size: druid::Size) -> Option { + let fit = usvg::FitTo::Size(size.width as u32, size.height as u32); + let mut pixmap = tiny_skia::Pixmap::new(size.width as u32, size.height as u32).unwrap(); + + if resvg::render(&self.tree, fit, pixmap.as_mut()).is_none() { + tracing::error!("unable to render svg"); + return None; + } - /// Set the SVG data. - pub fn set_svg_data(&mut self, svg_data: SvgData) { - self.svg_data = svg_data; + Some(druid::piet::ImageBuf::from_raw( + pixmap.data(), + druid::piet::ImageFormat::RgbaPremul, + size.width as usize, + size.height as usize, + )) } } -impl Widget for Svg { - #[instrument(name = "Svg", level = "trace", skip(self, _ctx, _event, _data, _env))] - fn event(&mut self, _ctx: &mut EventCtx, _event: &Event, _data: &mut T, _env: &Env) {} +impl druid::Widget for Svg { + fn event( + &mut self, + _ctx: &mut druid::EventCtx, + _event: &druid::Event, + _data: &mut T, + _env: &druid::Env, + ) { + } - #[instrument(name = "Svg", level = "trace", skip(self, _ctx, _event, _data, _env))] - fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle, _data: &T, _env: &Env) {} + fn lifecycle( + &mut self, + _ctx: &mut druid::LifeCycleCtx, + _event: &druid::LifeCycle, + _data: &T, + _env: &druid::Env, + ) { + } - #[instrument( - name = "Svg", - level = "trace", - skip(self, _ctx, _old_data, _data, _env) - )] - fn update(&mut self, _ctx: &mut UpdateCtx, _old_data: &T, _data: &T, _env: &Env) {} + fn update(&mut self, _ctx: &mut druid::UpdateCtx, _old_data: &T, _data: &T, _env: &druid::Env) { + } - #[instrument( - name = "Svg", - level = "trace", - skip(self, _layout_ctx, bc, _data, _env) - )] fn layout( &mut self, - _layout_ctx: &mut LayoutCtx, - bc: &BoxConstraints, + _layout_ctx: &mut druid::LayoutCtx, + bc: &druid::BoxConstraints, _data: &T, - _env: &Env, - ) -> Size { - bc.debug_check("SVG"); + _env: &druid::Env, + ) -> druid::Size { // preferred size comes from the svg - let size = self.svg_data.size(); - let constrained_size = bc.constrain_aspect_ratio(size.height / size.width, size.width); - trace!("Computed size: {}", constrained_size); - constrained_size + let size = self.default_size; + bc.constrain_aspect_ratio(size.height / size.width, size.width) } - #[instrument(name = "Svg", level = "trace", skip(self, ctx, _data, _env))] - fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, _env: &Env) { - let offset_matrix = self.fill.affine_to_fill(ctx.size(), self.svg_data.size()); + fn paint(&mut self, ctx: &mut druid::PaintCtx, _data: &T, _env: &druid::Env) { + let size = ctx.size(); - let clip_rect = Rect::ZERO.with_size(ctx.size()); + let cached = self.cached.as_ref().filter(|(csize, _)| *csize == size); + let cached = match cached { + Some(current) => Some(current.clone()), + None => self.render(size).map(|i| (size, i)), + }; + let cached = match cached { + Some(current) => current, + None => { + tracing::error!("unable to paint svg"); + return; + } + }; - // The SvgData's to_piet function does not clip to the svg's size - // CairoRenderContext is very like druids but with some extra goodies like clip + let clip_rect = druid::Rect::ZERO.with_size(cached.0); + let img = cached.1.to_image(ctx.render_ctx); ctx.clip(clip_rect); - self.svg_data.to_piet(offset_matrix, ctx); + ctx.draw_image( + &img, + clip_rect, + druid::piet::InterpolationMode::NearestNeighbor, + ); + self.cached = Some(cached); } } -/// Stored SVG data. -/// Implements `FromStr` and can be converted to piet draw instructions. -#[derive(Clone, Data)] +/// Stored parsed SVG tree. +#[derive(Clone, druid::Data)] pub struct SvgData { - tree: Arc, + tree: std::sync::Arc, } impl SvgData { + fn new(tree: std::sync::Arc) -> Self { + Self { tree } + } + /// Create an empty SVG pub fn empty() -> Self { - let re_opt = usvg::Options { - keep_named_groups: false, - ..usvg::Options::default() - }; + use std::str::FromStr; let empty_svg = r###" - - - - + + + + "###; - SvgData { - tree: Arc::new(usvg::Tree::from_str(empty_svg, &re_opt).unwrap()), - } - } - - /// Convert SvgData into Piet draw instructions - pub fn to_piet(&self, offset_matrix: Affine, ctx: &mut PaintCtx) { - let mut state = SvgRenderer::new(offset_matrix * self.inner_affine()); - // I actually made `SvgRenderer` able to handle a stack of ``, but I'm gonna see if - // resvg always puts them at the top. - let root = self.tree.root(); - for n in root.children() { - state.render_node(&n, ctx); - } - } - - /// Get the viewbox for the SVG. This is the area that should be drawn. - pub fn viewbox(&self) -> Rect { - let root = self.tree.root(); - let rect = match *root.borrow() { - usvg::NodeKind::Svg(svg) => { - let r = svg.view_box.rect; - Rect::new(r.left(), r.top(), r.right(), r.bottom()) - } - _ => { - tracing::error!( - "this SVG has no viewbox. It is expected that usvg always adds a viewbox" - ); - Rect::ZERO - } - }; - rect - } - - /// Get the size of the SVG. This is the size that the SVG requests to be drawn. If it is - /// different from the viewbox size, then scaling will be required. - pub fn size(&self) -> Size { - let root = self.tree.root(); - let rect = match *root.borrow() { - usvg::NodeKind::Svg(svg) => { - let s = svg.size; - Size::new(s.width(), s.height()) - } - _ => { - tracing::error!( - "this SVG has no size. It is expected that usvg always adds a size" - ); - Size::ZERO - } - }; - rect - } - - /// Calculates the transform that should be applied first to the SVG path data, to convert from - /// image coordinates to piet coordinates. - fn inner_affine(&self) -> Affine { - let viewbox = self.viewbox(); - let size = self.size(); - // we want to move the viewbox top left to (0,0) and then scale it from viewbox size to - // size. - // TODO respect preserveAspectRatio - let t = Affine::translate((viewbox.min_x(), viewbox.min_y())); - let scale = - Affine::scale_non_uniform(size.width / viewbox.width(), size.height / viewbox.height()); - scale * t + SvgData::from_str(empty_svg).unwrap() } } -impl Default for SvgData { - fn default() -> Self { - SvgData::empty() - } -} - -impl FromStr for SvgData { - type Err = Box; +impl std::str::FromStr for SvgData { + type Err = Box; fn from_str(svg_str: &str) -> Result { - let mut re_opt = usvg::Options { + let re_opt = usvg::Options { keep_named_groups: false, ..usvg::Options::default() }; - re_opt.fontdb.load_system_fonts(); - - match usvg::Tree::from_str(svg_str, &re_opt) { - Ok(tree) => Ok(SvgData { - tree: Arc::new(tree), - }), + match usvg::Tree::from_str(svg_str, &re_opt.to_ref()) { + Ok(tree) => Ok(SvgData::new(std::sync::Arc::new(tree))), Err(err) => Err(err.into()), } } } -struct SvgRenderer { - offset_matrix: Affine, - defs: Defs, -} - -impl SvgRenderer { - fn new(offset_matrix: Affine) -> Self { - Self { - offset_matrix, - defs: Defs::new(), - } - } - - /// Take a usvg node and render it to the given context. - fn render_node(&mut self, n: &usvg::Node, ctx: &mut PaintCtx) { - match *n.borrow() { - usvg::NodeKind::Path(ref p) => self.render_path(p, ctx), - usvg::NodeKind::Defs => { - // children are defs - for def in n.children() { - match &*def.borrow() { - usvg::NodeKind::LinearGradient(linear_gradient) => { - self.linear_gradient_def(linear_gradient); - } - usvg::NodeKind::RadialGradient(gradient) => { - self.radial_gradient_def(gradient); - } - other => tracing::error!("unsupported element: {:?}", other), - } - } - } - usvg::NodeKind::Group(_) => { - // TODO I'm not sure if we need to apply the transform, or if usvg has already - // done it for us? I'm guessing the latter for now, but that could easily be wrong. - for child in n.children() { - self.render_node(&child, ctx); - } - } - _ => { - // TODO: handle more of the SVG spec. - tracing::error!("{:?} is unimplemented", n.clone()); - } - } - } - - /// Take a usvg path and render it to the given context. - fn render_path(&self, p: &usvg::Path, ctx: &mut PaintCtx) { - if matches!( - p.visibility, - usvg::Visibility::Hidden | usvg::Visibility::Collapse - ) { - // skip rendering - return; - } - - let mut path = BezPath::new(); - for segment in p.data.iter() { - match *segment { - usvg::PathSegment::MoveTo { x, y } => { - path.move_to((x, y)); - } - usvg::PathSegment::LineTo { x, y } => { - path.line_to((x, y)); - } - usvg::PathSegment::CurveTo { - x1, - y1, - x2, - y2, - x, - y, - } => { - path.curve_to((x1, y1), (x2, y2), (x, y)); - } - usvg::PathSegment::ClosePath => { - path.close_path(); - } - } - } - - path.apply_affine(self.offset_matrix * transform_to_affine(p.transform)); - - match &p.fill { - Some(fill) => { - let brush = self.brush_from_usvg(&fill.paint, fill.opacity); - if let usvg::FillRule::EvenOdd = fill.rule { - ctx.fill_even_odd(path.clone(), &*brush); - } else { - ctx.fill(path.clone(), &*brush); - } - } - None => {} - } - - match &p.stroke { - Some(stroke) => { - let brush = self.brush_from_usvg(&stroke.paint, stroke.opacity); - let mut stroke_style = StrokeStyle::new() - .line_join(match stroke.linejoin { - usvg::LineJoin::Miter => LineJoin::Miter { - limit: stroke.miterlimit.value(), - }, - usvg::LineJoin::Round => LineJoin::Round, - usvg::LineJoin::Bevel => LineJoin::Bevel, - }) - .line_cap(match stroke.linecap { - usvg::LineCap::Butt => LineCap::Butt, - usvg::LineCap::Round => LineCap::Round, - usvg::LineCap::Square => LineCap::Square, - }); - if let Some(dash_array) = &stroke.dasharray { - stroke_style.set_dash_pattern(dash_array.as_slice()); - stroke_style.set_dash_offset(stroke.dashoffset as f64); - } - ctx.stroke_styled(path, &*brush, stroke.width.value(), &stroke_style); - } - None => {} - } - } - - fn linear_gradient_def(&mut self, lg: &usvg::LinearGradient) { - let start = UnitPoint::new(lg.x1, lg.y1); - let end = UnitPoint::new(lg.x2, lg.y2); - let stops: Vec<_> = lg - .stops - .iter() - .map(|stop| GradientStop { - pos: stop.offset.value() as f32, - color: color_from_svg(stop.color, stop.opacity), - }) - .collect(); - - let gradient = LinearGradient::new(start, end, stops); - self.defs - .add_def(lg.id.clone(), piet::PaintBrush::Linear(gradient)); - } - - fn radial_gradient_def(&mut self, g: &usvg::RadialGradient) { - let center = UnitPoint::new(g.fx, g.fy); - let origin = UnitPoint::new(g.cx, g.cy); - - let stops: Vec<_> = g - .stops - .iter() - .map(|stop| GradientStop { - pos: stop.offset.value() as f32, - color: color_from_svg(stop.color, stop.opacity), - }) - .collect(); - - let gradient = RadialGradient::new(g.r.value(), stops) - .with_center(center) - .with_origin(origin); - self.defs - .add_def(g.id.clone(), piet::PaintBrush::Radial(gradient)); - } - - fn brush_from_usvg(&self, paint: &usvg::Paint, opacity: usvg::Opacity) -> Rc { - match paint { - usvg::Paint::Color(c) => { - // TODO I'm going to assume here that not retaining colors is OK. - let color = color_from_svg(*c, opacity); - Rc::new(piet::PaintBrush::Color(color)) - } - usvg::Paint::Link(id) => match self.defs.find(id) { - None => { - // generally this occurs due to unimplemented SVG functionality. - // until the SVG implementation matures log as a trace. - // other logging above will detect and emit the error that - // triggered the issue. - trace!("svg link id requested, but not found: {:?}", id); - Rc::new(piet::PaintBrush::Color(Color::TRANSPARENT)) - } - Some(v) => v, - }, - } - } -} - -type Def = piet::PaintBrush; - -/// A map from id to [`Def`] -/// -/// [`Def`]: Def -struct Defs(HashMap>); - -impl Defs { - fn new() -> Self { - Defs(HashMap::new()) - } - - /// Add a def. - fn add_def(&mut self, id: String, def: Def) { - self.0.insert(id, Rc::new(def)); +impl From for std::sync::Arc { + fn from(d: SvgData) -> Self { + d.tree } - - /// Look for a def by id. - fn find(&self, id: &str) -> Option> { - self.0.get(id).cloned() - } -} - -/// Convert a usvg transform to a kurbo `Affine`. -fn transform_to_affine(t: usvg::Transform) -> Affine { - Affine::new([t.a, t.b, t.c, t.d, t.e, t.f]) } -fn color_from_svg(c: usvg::Color, opacity: usvg::Opacity) -> Color { - Color::rgb8(c.red, c.green, c.blue).with_alpha(opacity.value()) -} - -#[cfg(not(target_arch = "wasm32"))] -#[cfg(test)] -mod tests { - use super::*; - use test_log::test; - - #[test] - fn usvg_transform_vs_affine() { - assert_eq!( - transform_to_affine(usvg::Transform::new_translate(1., 2.)), - Affine::translate((1., 2.)) - ); - assert_eq!( - transform_to_affine(usvg::Transform::new_scale(1., 2.)), - Affine::scale_non_uniform(1., 2.) - ); - // amazingly we get actual equality here - assert_eq!( - transform_to_affine(usvg::Transform::new_rotate(180.)), - Affine::rotate(std::f64::consts::PI) - ); - } - - #[test] - fn translate() { - use crate::tests::harness::Harness; - - let svg_data = SvgData::from_str( - " - - - - - - - - - - - ", - ) - .unwrap(); - - let svg_widget = Svg::new(svg_data); - - Harness::create_with_render( - true, - svg_widget, - Size::new(400., 600.), - |harness| { - harness.send_initial_events(); - harness.just_layout(); - harness.paint(); - }, - |target| { - let raw_pixels = target.into_raw(); - assert_eq!(raw_pixels.len(), 400 * 600 * 4); - - // Being a tall widget with a square image the top and bottom rows will be - // the padding color and the middle rows will not have any padding. - - // Check that the middle row 400 pix wide is 200 black then 200 white. - let expecting: Vec = [ - vec![41, 41, 41, 255].repeat(200), - vec![0, 0, 0, 255].repeat(200), - ] - .concat(); - assert_eq!(raw_pixels[400 * 300 * 4..400 * 301 * 4], expecting[..]); - - // Check that all of the last 100 rows are all the background color. - let expecting: Vec = vec![41, 41, 41, 255].repeat(400 * 100); - assert_eq!( - raw_pixels[400 * 600 * 4 - 4 * 400 * 100..400 * 600 * 4], - expecting[..] - ); - }, - ) - } - - #[test] - fn scale() { - use crate::tests::harness::Harness; - - let svg_data = SvgData::from_str( - " - - - - - - - - - - - ", - ) - .unwrap(); - - let svg_widget = Svg::new(svg_data); - - Harness::create_with_render( - true, - svg_widget, - Size::new(400., 600.), - |harness| { - harness.send_initial_events(); - harness.just_layout(); - harness.paint(); - }, - |target| { - let raw_pixels = target.into_raw(); - assert_eq!(raw_pixels.len(), 400 * 600 * 4); - - // Being a tall widget with a square image the top and bottom rows will be - // the padding color and the middle rows will not have any padding. - - // Check that the middle row 400 pix wide is 200 black then 200 white. - let expecting: Vec = [ - vec![41, 41, 41, 255].repeat(200), - vec![0, 0, 0, 255].repeat(200), - ] - .concat(); - assert_eq!(raw_pixels[400 * 300 * 4..400 * 301 * 4], expecting[..]); - - // Check that all of the last 100 rows are all the background color. - let expecting: Vec = vec![41, 41, 41, 255].repeat(400 * 100); - assert_eq!( - raw_pixels[400 * 600 * 4 - 4 * 400 * 100..400 * 600 * 4], - expecting[..] - ); - }, - ) +impl Default for SvgData { + fn default() -> Self { + SvgData::empty() } } From 1b9ebe909ea2c4ce79494410625ac397d8570c32 Mon Sep 17 00:00:00 2001 From: James Lawrence Date: Mon, 16 Jan 2023 07:44:57 -0500 Subject: [PATCH 2/2] update dependencies. --- druid/Cargo.toml | 6 +++--- druid/src/widget/svg.rs | 32 ++++++++++---------------------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/druid/Cargo.toml b/druid/Cargo.toml index af25809bb7..b7b1c6d585 100644 --- a/druid/Cargo.toml +++ b/druid/Cargo.toml @@ -71,9 +71,9 @@ instant = { version = "0.1.12", features = ["wasm-bindgen"] } # Optional dependencies chrono = { version = "0.4.23", optional = true } im = { version = "15.1.0", optional = true } -resvg = { version = "0.19.0", optional = true } -usvg = { version = "0.19.0", optional = true } -tiny-skia = { version = "0.6.1", optional = true } +resvg = { version = "0.28.0", optional = true } +usvg = { version = "0.28.0", optional = true } +tiny-skia = { version = "0.8.2", optional = true } [target.'cfg(target_arch="wasm32")'.dependencies] tracing-wasm = { version = "0.2.1" } diff --git a/druid/src/widget/svg.rs b/druid/src/widget/svg.rs index d02a8cc623..03b7042dd4 100644 --- a/druid/src/widget/svg.rs +++ b/druid/src/widget/svg.rs @@ -43,36 +43,24 @@ impl Svg { pub fn new(tree: impl Into>) -> Self { let tree = tree.into(); Svg { - default_size: Svg::size(&tree), + default_size: druid::Size::new(tree.size.width(), tree.size.height()), cached: None::<(druid::Size, druid::piet::ImageBuf)>, tree, } } - /// Get the size of the svg. This is the size that the svg requests to be drawn. If it is - /// different from the viewbox size, then scaling will be required. - fn size(tree: &std::sync::Arc) -> druid::Size { - let root = tree.root(); - let rect = match *root.borrow() { - usvg::NodeKind::Svg(svg) => { - let s = svg.size; - druid::Size::new(s.width(), s.height()) - } - _ => { - tracing::error!( - "this SVG has no size. It is expected that usvg always adds a size" - ); - druid::Size::ZERO - } - }; - rect - } - fn render(&self, size: druid::Size) -> Option { let fit = usvg::FitTo::Size(size.width as u32, size.height as u32); let mut pixmap = tiny_skia::Pixmap::new(size.width as u32, size.height as u32).unwrap(); - if resvg::render(&self.tree, fit, pixmap.as_mut()).is_none() { + if resvg::render( + &self.tree, + fit, + tiny_skia::Transform::identity(), + pixmap.as_mut(), + ) + .is_none() + { tracing::error!("unable to render svg"); return None; } @@ -183,7 +171,7 @@ impl std::str::FromStr for SvgData { ..usvg::Options::default() }; - match usvg::Tree::from_str(svg_str, &re_opt.to_ref()) { + match usvg::Tree::from_str(svg_str, &re_opt) { Ok(tree) => Ok(SvgData::new(std::sync::Arc::new(tree))), Err(err) => Err(err.into()), }