ribir_painter/
svg.rs

1use std::{error::Error, io::Read};
2
3use ribir_algo::Resource;
4use ribir_geom::{Point, Rect, Size, Transform};
5use serde::{Deserialize, Serialize};
6use usvg::{Options, Stop, Tree, TreeParsing};
7
8use crate::{
9  color::{LinearGradient, RadialGradient},
10  Brush, Color, GradientStop, LineCap, LineJoin, PaintCommand, Path, StrokeOptions,
11};
12
13#[derive(Serialize, Deserialize, Clone)]
14pub struct Svg {
15  pub size: Size,
16  pub commands: Resource<Box<[PaintCommand]>>,
17}
18
19/// Fits size into a viewbox. copy from resvg
20fn fit_view_box(size: usvg::Size, vb: &usvg::ViewBox) -> usvg::Size {
21  let s = vb.rect.size();
22
23  if vb.aspect.align == usvg::Align::None {
24    s
25  } else if vb.aspect.slice {
26    size.expand_to(s)
27  } else {
28    size.scale_to(s)
29  }
30}
31
32// todo: we need to support currentColor to change svg color.
33impl Svg {
34  pub fn parse_from_bytes(svg_data: &[u8]) -> Result<Self, Box<dyn Error>> {
35    let opt = Options { ..<_>::default() };
36    let tree = Tree::from_data(svg_data, &opt).unwrap();
37    let view_rect = tree.view_box.rect;
38    let size = tree.size;
39    let fit_size = fit_view_box(size, &tree.view_box);
40
41    let bound_rect = Rect::from_size(Size::new(f32::MAX, f32::MAX));
42    let mut painter = crate::Painter::new(bound_rect);
43    painter.apply_transform(
44      &Transform::translation(-view_rect.x(), -view_rect.y())
45        .then_scale(size.width() / fit_size.width(), size.height() / fit_size.height()),
46    );
47    tree.root.traverse().for_each(|edge| match edge {
48      rctree::NodeEdge::Start(node) => {
49        use usvg::NodeKind;
50        painter.save();
51        match &*node.borrow() {
52          NodeKind::Path(p) => {
53            painter.apply_transform(&matrix_convert(p.transform));
54            let path = usvg_path_to_path(p);
55            if let Some(ref fill) = p.fill {
56              let (brush, transform) = brush_from_usvg_paint(&fill.paint, fill.opacity, &size);
57              let mut painter = painter.save_guard();
58
59              let inverse_ts = transform.inverse().unwrap();
60              let path = Resource::new(path.clone().transform(&inverse_ts));
61              painter
62                .set_brush(brush.clone())
63                .apply_transform(&transform)
64                .fill_path(path);
65              //&o_ts.then(&n_ts.inverse().unwrap())));
66            }
67
68            if let Some(ref stroke) = p.stroke {
69              let cap = match stroke.linecap {
70                usvg::LineCap::Butt => LineCap::Butt,
71                usvg::LineCap::Square => LineCap::Square,
72                usvg::LineCap::Round => LineCap::Round,
73              };
74              let join = match stroke.linejoin {
75                usvg::LineJoin::Miter => LineJoin::Miter,
76                usvg::LineJoin::Bevel => LineJoin::Bevel,
77                usvg::LineJoin::Round => LineJoin::Round,
78                usvg::LineJoin::MiterClip => LineJoin::MiterClip,
79              };
80              let options = StrokeOptions {
81                width: stroke.width.get(),
82                line_cap: cap,
83                line_join: join,
84                miter_limit: stroke.miterlimit.get(),
85              };
86
87              let (brush, transform) = brush_from_usvg_paint(&stroke.paint, stroke.opacity, &size);
88              let mut painter = painter.save_guard();
89
90              painter
91                .set_brush(brush.clone())
92                .apply_transform(&transform);
93
94              let path = path
95                .transform(&transform.inverse().unwrap())
96                .stroke(&options, Some(painter.get_transform()));
97
98              if let Some(p) = path {
99                painter.fill_path(Resource::new(p));
100              }
101            };
102          }
103          NodeKind::Image(_) => {
104            // todo;
105            log::warn!("[painter]: not support draw embed image in svg, ignored!");
106          }
107          NodeKind::Group(ref g) => {
108            painter.apply_transform(&matrix_convert(g.transform));
109            // todo;
110            if g.opacity.get() != 1. {
111              log::warn!("[painter]: not support `opacity` in svg, ignored!");
112            }
113            if g.clip_path.is_some() {
114              log::warn!("[painter]: not support `clip path` in svg, ignored!");
115            }
116            if g.mask.is_some() {
117              log::warn!("[painter]: not support `mask` in svg, ignored!");
118            }
119            if !g.filters.is_empty() {
120              log::warn!("[painter]: not support `filters` in svg, ignored!");
121            }
122          }
123          NodeKind::Text(_) => {
124            todo!("Not support text in SVG temporarily, we'll add it after refactoring `painter`.")
125          }
126        }
127      }
128      rctree::NodeEdge::End(_) => {
129        painter.restore();
130      }
131    });
132
133    let paint_commands = painter.finish().to_owned().into_boxed_slice();
134
135    Ok(Svg {
136      size: Size::new(size.width(), size.height()),
137      commands: Resource::new(paint_commands),
138    })
139  }
140
141  pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Box<dyn Error>> {
142    let mut file = std::fs::File::open(path)?;
143    let mut bytes = vec![];
144    file.read_to_end(&mut bytes)?;
145    Self::parse_from_bytes(&bytes)
146  }
147
148  pub fn serialize(&self) -> Result<String, Box<dyn Error>> {
149    // use json replace bincode, because https://github.com/Ogeon/palette/issues/130
150    Ok(serde_json::to_string(self)?)
151  }
152
153  pub fn deserialize(str: &str) -> Result<Self, Box<dyn Error>> { Ok(serde_json::from_str(str)?) }
154}
155
156fn usvg_path_to_path(path: &usvg::Path) -> Path {
157  let mut builder = lyon_algorithms::path::Path::svg_builder();
158  path.data.segments().for_each(|seg| match seg {
159    usvg::tiny_skia_path::PathSegment::MoveTo(pt) => {
160      builder.move_to(point(pt.x, pt.y));
161    }
162    usvg::tiny_skia_path::PathSegment::LineTo(pt) => {
163      builder.line_to(point(pt.x, pt.y));
164    }
165    usvg::tiny_skia_path::PathSegment::CubicTo(pt1, pt2, pt3) => {
166      builder.cubic_bezier_to(point(pt1.x, pt1.y), point(pt2.x, pt2.y), point(pt3.x, pt3.y));
167    }
168    usvg::tiny_skia_path::PathSegment::QuadTo(pt1, pt2) => {
169      builder.quadratic_bezier_to(point(pt1.x, pt1.y), point(pt2.x, pt2.y));
170    }
171    usvg::tiny_skia_path::PathSegment::Close => builder.close(),
172  });
173
174  builder.build().into()
175}
176
177fn point(x: f32, y: f32) -> lyon_algorithms::math::Point { Point::new(x, y).to_untyped() }
178
179fn matrix_convert(t: usvg::Transform) -> Transform {
180  let usvg::Transform { sx, kx, ky, sy, tx, ty } = t;
181  Transform::new(sx, ky, kx, sy, tx, ty)
182}
183
184fn brush_from_usvg_paint(
185  paint: &usvg::Paint, opacity: usvg::Opacity, size: &usvg::Size,
186) -> (Brush, Transform) {
187  match paint {
188    usvg::Paint::Color(usvg::Color { red, green, blue }) => (
189      Color::from_rgb(*red, *green, *blue)
190        .with_alpha(opacity.get())
191        .into(),
192      Transform::identity(),
193    ),
194    usvg::Paint::LinearGradient(linear) => {
195      let stops = convert_to_gradient_stops(&linear.stops);
196      let size_scale = match linear.units {
197        usvg::Units::UserSpaceOnUse => (1., 1.),
198        usvg::Units::ObjectBoundingBox => (size.width(), size.height()),
199      };
200      let gradient = LinearGradient {
201        start: Point::new(linear.x1 * size_scale.0, linear.y1 * size_scale.1),
202        end: Point::new(linear.x2 * size_scale.0, linear.y2 * size_scale.1),
203        stops,
204        spread_method: linear.spread_method.into(),
205      };
206
207      (Brush::LinearGradient(gradient), matrix_convert(linear.transform))
208    }
209    usvg::Paint::RadialGradient(radial_gradient) => {
210      let stops = convert_to_gradient_stops(&radial_gradient.stops);
211      let size_scale = match radial_gradient.units {
212        usvg::Units::UserSpaceOnUse => (1., 1.),
213        usvg::Units::ObjectBoundingBox => (size.width(), size.height()),
214      };
215      let gradient = RadialGradient {
216        start_center: Point::new(
217          radial_gradient.fx * size_scale.0,
218          radial_gradient.fy * size_scale.1,
219        ),
220        start_radius: 0., // usvg not support fr
221        end_center: Point::new(
222          radial_gradient.cx * size_scale.0,
223          radial_gradient.cy * size_scale.1,
224        ),
225        end_radius: radial_gradient.r.get() * size_scale.0,
226        stops,
227        spread_method: radial_gradient.spread_method.into(),
228      };
229
230      (Brush::RadialGradient(gradient), matrix_convert(radial_gradient.transform))
231    }
232    paint => {
233      log::warn!("[painter]: not support `{paint:?}` in svg, use black instead!");
234      (Color::BLACK.into(), Transform::identity())
235    }
236  }
237}
238
239fn convert_to_gradient_stops(stops: &[Stop]) -> Vec<GradientStop> {
240  assert!(!stops.is_empty());
241
242  let mut stops: Vec<_> = stops
243    .iter()
244    .map(|stop| {
245      let usvg::Color { red, green, blue } = stop.color;
246      GradientStop {
247        offset: stop.offset.get(),
248        color: Color::new(red, green, blue, stop.opacity.to_u8()),
249      }
250    })
251    .collect();
252
253  stops.sort_by(|s1, s2| s1.offset.partial_cmp(&s2.offset).unwrap());
254
255  if let Some(first) = stops.first() {
256    if first.offset != 0. {
257      stops.insert(0, GradientStop { offset: 0., color: first.color });
258    }
259  }
260  if let Some(last) = stops.last() {
261    if last.offset < 1. {
262      stops.push(GradientStop { offset: 1., color: last.color });
263    }
264  }
265  stops
266}