1use std::sync::Arc;
8
9use lyon_geom::point;
10use lyon_path::Path as LyonPath;
11use lyon_tessellation::{
12 BuffersBuilder, FillOptions, FillRule as LyonFillRule, FillTessellator, FillVertex,
13 LineCap as LyonLineCap, LineJoin as LyonLineJoin, StrokeOptions, StrokeTessellator,
14 StrokeVertex, VertexBuffers,
15};
16use tessera_ui::Color;
17use thiserror::Error;
18use usvg::{
19 BlendMode, FillRule, Group, LineCap as SvgLineCap, LineJoin as SvgLineJoin, Node, Paint,
20 PaintOrder, Path, Stroke, Tree, tiny_skia_path::PathSegment,
21};
22
23use crate::pipelines::image_vector::command::ImageVectorVertex;
24
25pub use crate::pipelines::image_vector::command::{ImageVectorData, VectorTintMode as TintMode};
26
27#[derive(Debug, Error)]
28pub(crate) enum ImageVectorLoadError {
29 #[error("failed to parse SVG: {0}")]
30 Parse(#[from] usvg::Error),
31 #[error("SVG viewport must have finite, positive size")]
32 InvalidViewport,
33 #[error("unsupported SVG feature: {0}")]
34 UnsupportedFeature(String),
35 #[error("failed to apply SVG transforms")]
36 TransformFailed,
37 #[error("tessellation error: {0}")]
38 Tessellation(#[from] lyon_tessellation::TessellationError),
39 #[error("SVG produced no renderable paths")]
40 EmptyGeometry,
41}
42
43pub(crate) fn decode_vector(bytes: &[u8]) -> Result<ImageVectorData, ImageVectorLoadError> {
44 build_vector_data(&Tree::from_data(bytes, &usvg::Options::default())?)
45}
46
47fn build_vector_data(tree: &Tree) -> Result<ImageVectorData, ImageVectorLoadError> {
48 let size = tree.size();
49 let viewport_width = size.width();
50 let viewport_height = size.height();
51
52 if !viewport_width.is_finite()
53 || !viewport_height.is_finite()
54 || viewport_width <= 0.0
55 || viewport_height <= 0.0
56 {
57 return Err(ImageVectorLoadError::InvalidViewport);
58 }
59
60 let mut collector = VectorGeometryCollector::new(viewport_width, viewport_height);
61 visit_group(tree.root(), 1.0, &mut collector)?;
62
63 collector.finish()
64}
65
66fn visit_group(
67 group: &Group,
68 inherited_opacity: f32,
69 collector: &mut VectorGeometryCollector,
70) -> Result<(), ImageVectorLoadError> {
71 if group.clip_path().is_some() || group.mask().is_some() || !group.filters().is_empty() {
72 return Err(ImageVectorLoadError::UnsupportedFeature(
73 "clip paths, masks, and filters are not supported".to_string(),
74 ));
75 }
76
77 if group.blend_mode() != BlendMode::Normal {
78 return Err(ImageVectorLoadError::UnsupportedFeature(
79 "non-normal blend modes".to_string(),
80 ));
81 }
82
83 let accumulated_opacity = inherited_opacity * group.opacity().get();
84
85 for node in group.children() {
86 match node {
87 Node::Group(child) => visit_group(child, accumulated_opacity, collector)?,
88 Node::Path(path) => collector.process_path(path, accumulated_opacity)?,
89 Node::Image(_) | Node::Text(_) => {
90 return Err(ImageVectorLoadError::UnsupportedFeature(
91 "non-path nodes in SVG are not supported".to_string(),
92 ));
93 }
94 }
95 }
96
97 Ok(())
98}
99
100struct VectorGeometryCollector {
101 viewport_width: f32,
102 viewport_height: f32,
103 buffers: VertexBuffers<ImageVectorVertex, u32>,
104}
105
106impl VectorGeometryCollector {
107 fn new(viewport_width: f32, viewport_height: f32) -> Self {
108 Self {
109 viewport_width,
110 viewport_height,
111 buffers: VertexBuffers::new(),
112 }
113 }
114
115 fn process_path(
116 &mut self,
117 path: &Path,
118 inherited_opacity: f32,
119 ) -> Result<(), ImageVectorLoadError> {
120 if !path.is_visible() {
121 return Ok(());
122 }
123
124 if path.rendering_mode() != usvg::ShapeRendering::default() {
125 return Err(ImageVectorLoadError::UnsupportedFeature(
126 "shape-rendering modes are not supported".to_string(),
127 ));
128 }
129
130 let lyon_path = convert_to_lyon_path(path)?;
131
132 match path.paint_order() {
133 PaintOrder::FillAndStroke => {
134 if let Some(fill) = path.fill() {
135 self.tessellate_fill(&lyon_path, fill, inherited_opacity)?;
136 }
137 if let Some(stroke) = path.stroke() {
138 self.tessellate_stroke(&lyon_path, stroke, inherited_opacity)?;
139 }
140 }
141 PaintOrder::StrokeAndFill => {
142 if let Some(stroke) = path.stroke() {
143 self.tessellate_stroke(&lyon_path, stroke, inherited_opacity)?;
144 }
145 if let Some(fill) = path.fill() {
146 self.tessellate_fill(&lyon_path, fill, inherited_opacity)?;
147 }
148 }
149 }
150
151 Ok(())
152 }
153
154 fn tessellate_fill(
155 &mut self,
156 path: &LyonPath,
157 fill: &usvg::Fill,
158 inherited_opacity: f32,
159 ) -> Result<(), ImageVectorLoadError> {
160 let color = color_from_paint(fill.paint(), fill.opacity().get(), inherited_opacity)?;
161 let fill_rule = match fill.rule() {
162 FillRule::EvenOdd => LyonFillRule::EvenOdd,
163 FillRule::NonZero => LyonFillRule::NonZero,
164 };
165
166 let options = FillOptions::default().with_fill_rule(fill_rule);
167 let viewport = [self.viewport_width, self.viewport_height];
168
169 FillTessellator::new().tessellate_path(
170 path,
171 &options,
172 &mut BuffersBuilder::new(&mut self.buffers, |vertex: FillVertex| {
173 ImageVectorVertex::new(vertex.position().to_array(), color, viewport)
174 }),
175 )?;
176
177 Ok(())
178 }
179
180 fn tessellate_stroke(
181 &mut self,
182 path: &LyonPath,
183 stroke: &Stroke,
184 inherited_opacity: f32,
185 ) -> Result<(), ImageVectorLoadError> {
186 if stroke.dasharray().is_some() {
187 return Err(ImageVectorLoadError::UnsupportedFeature(
188 "stroke dash arrays".to_string(),
189 ));
190 }
191
192 let color = color_from_paint(stroke.paint(), stroke.opacity().get(), inherited_opacity)?;
193
194 let mut options = StrokeOptions::default()
195 .with_line_width(stroke.width().get())
196 .with_line_cap(map_line_cap(stroke.linecap()))
197 .with_line_join(map_line_join(stroke.linejoin()));
198
199 options.miter_limit = stroke.miterlimit().get();
200
201 let viewport = [self.viewport_width, self.viewport_height];
202
203 StrokeTessellator::new().tessellate_path(
204 path,
205 &options,
206 &mut BuffersBuilder::new(&mut self.buffers, |vertex: StrokeVertex| {
207 ImageVectorVertex::new(vertex.position().to_array(), color, viewport)
208 }),
209 )?;
210
211 Ok(())
212 }
213
214 fn finish(self) -> Result<ImageVectorData, ImageVectorLoadError> {
215 if self.buffers.vertices.is_empty() || self.buffers.indices.is_empty() {
216 return Err(ImageVectorLoadError::EmptyGeometry);
217 }
218
219 Ok(ImageVectorData::new(
220 self.viewport_width,
221 self.viewport_height,
222 Arc::new(self.buffers.vertices),
223 Arc::new(self.buffers.indices),
224 ))
225 }
226}
227
228fn color_from_paint(
229 paint: &Paint,
230 paint_opacity: f32,
231 inherited_opacity: f32,
232) -> Result<Color, ImageVectorLoadError> {
233 let opacity = (paint_opacity * inherited_opacity).clamp(0.0, 1.0);
234 match paint {
235 Paint::Color(color) => Ok(Color::new(
236 f32::from(color.red) / 255.0,
237 f32::from(color.green) / 255.0,
238 f32::from(color.blue) / 255.0,
239 opacity,
240 )),
241 _ => Err(ImageVectorLoadError::UnsupportedFeature(
242 "only solid color fills and strokes are supported".to_string(),
243 )),
244 }
245}
246
247fn convert_to_lyon_path(path: &Path) -> Result<LyonPath, ImageVectorLoadError> {
248 let transformed = path
249 .data()
250 .clone()
251 .transform(path.abs_transform())
252 .ok_or(ImageVectorLoadError::TransformFailed)?;
253
254 let mut builder = LyonPath::builder().with_svg();
255 for segment in transformed.segments() {
256 match segment {
257 PathSegment::MoveTo(p0) => {
258 builder.move_to(point(p0.x, p0.y));
259 }
260 PathSegment::LineTo(p0) => {
261 builder.line_to(point(p0.x, p0.y));
262 }
263 PathSegment::QuadTo(p0, p1) => {
264 builder.quadratic_bezier_to(point(p0.x, p0.y), point(p1.x, p1.y));
265 }
266 PathSegment::CubicTo(p0, p1, p2) => {
267 builder.cubic_bezier_to(point(p0.x, p0.y), point(p1.x, p1.y), point(p2.x, p2.y));
268 }
269 PathSegment::Close => {
270 builder.close();
271 }
272 }
273 }
274
275 Ok(builder.build())
276}
277
278fn map_line_cap(cap: SvgLineCap) -> LyonLineCap {
279 match cap {
280 SvgLineCap::Butt => LyonLineCap::Butt,
281 SvgLineCap::Round => LyonLineCap::Round,
282 SvgLineCap::Square => LyonLineCap::Square,
283 }
284}
285
286fn map_line_join(join: SvgLineJoin) -> LyonLineJoin {
287 match join {
288 SvgLineJoin::Miter | SvgLineJoin::MiterClip => LyonLineJoin::Miter,
289 SvgLineJoin::Round => LyonLineJoin::Round,
290 SvgLineJoin::Bevel => LyonLineJoin::Bevel,
291 }
292}
293
294impl ImageVectorVertex {
295 fn new(position: [f32; 2], color: Color, viewport: [f32; 2]) -> Self {
296 ImageVectorVertex {
297 position: [position[0] / viewport[0], position[1] / viewport[1]],
298 color,
299 }
300 }
301}