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