1use std::{fs, path::Path as StdPath, sync::Arc};
9
10use derive_builder::Builder;
11use lyon_geom::point;
12use lyon_path::Path as LyonPath;
13use lyon_tessellation::{
14 BuffersBuilder, FillOptions, FillRule as LyonFillRule, FillTessellator, FillVertex,
15 LineCap as LyonLineCap, LineJoin as LyonLineJoin, StrokeOptions, StrokeTessellator,
16 StrokeVertex, VertexBuffers,
17};
18use tessera_ui::{Color, ComputedData, Constraint, DimensionValue, Px, tessera};
19use thiserror::Error;
20use usvg::{
21 BlendMode, FillRule, Group, LineCap as SvgLineCap, LineJoin as SvgLineJoin, Node, Paint,
22 PaintOrder, Path, Stroke, Tree, tiny_skia_path::PathSegment,
23};
24
25use crate::pipelines::image_vector::{ImageVectorCommand, ImageVectorVertex};
26
27pub use crate::pipelines::image_vector::ImageVectorData;
28
29#[derive(Clone, Debug)]
31pub enum ImageVectorSource {
32 Path(String),
34 Bytes(Arc<[u8]>),
36}
37
38#[derive(Debug, Error)]
40pub enum ImageVectorLoadError {
41 #[error("failed to read SVG from {path}: {source}")]
43 Io {
44 path: String,
46 #[source]
48 source: std::io::Error,
49 },
50 #[error("failed to parse SVG: {0}")]
52 Parse(#[from] usvg::Error),
53 #[error("SVG viewport must have finite, positive size")]
55 InvalidViewport,
56 #[error("unsupported SVG feature: {0}")]
58 UnsupportedFeature(String),
59 #[error("failed to apply SVG transforms")]
61 TransformFailed,
62 #[error("tessellation error: {0}")]
64 Tessellation(#[from] lyon_tessellation::TessellationError),
65 #[error("SVG produced no renderable paths")]
67 EmptyGeometry,
68}
69
70pub fn load_image_vector_from_source(
72 source: &ImageVectorSource,
73) -> Result<ImageVectorData, ImageVectorLoadError> {
74 let (bytes, resources_dir) = read_source_bytes(source)?;
75
76 let mut options = usvg::Options::default();
77 options.resources_dir = resources_dir;
78 let tree = Tree::from_data(&bytes, &options)?;
79
80 build_vector_data(&tree)
81}
82
83#[derive(Debug, Builder, Clone)]
85#[builder(pattern = "owned")]
86pub struct ImageVectorArgs {
87 #[builder(setter(into))]
89 pub data: Arc<ImageVectorData>,
90 #[builder(default = "DimensionValue::WRAP", setter(into))]
92 pub width: DimensionValue,
93 #[builder(default = "DimensionValue::WRAP", setter(into))]
95 pub height: DimensionValue,
96 #[builder(default = "Color::WHITE")]
98 pub tint: Color,
99}
100
101impl From<ImageVectorData> for ImageVectorArgs {
102 fn from(data: ImageVectorData) -> Self {
103 ImageVectorArgsBuilder::default()
104 .data(Arc::new(data))
105 .build()
106 .expect("ImageVectorArgsBuilder failed with required fields set")
107 }
108}
109
110#[tessera]
111pub fn image_vector(args: impl Into<ImageVectorArgs>) {
112 let image_args: ImageVectorArgs = args.into();
113
114 measure(Box::new(move |input| {
115 let intrinsic_width = px_from_f32(image_args.data.viewport_width);
116 let intrinsic_height = px_from_f32(image_args.data.viewport_height);
117
118 let constraint = Constraint::new(image_args.width, image_args.height);
119 let effective_constraint = constraint.merge(input.parent_constraint);
120
121 let width = match effective_constraint.width {
122 DimensionValue::Fixed(value) => value,
123 DimensionValue::Wrap { min, max } => min
124 .unwrap_or(Px(0))
125 .max(intrinsic_width)
126 .min(max.unwrap_or(Px::MAX)),
127 DimensionValue::Fill { min, max } => {
128 let parent_max = input.parent_constraint.width.get_max().unwrap_or(Px::MAX);
129 max.unwrap_or(parent_max)
130 .max(min.unwrap_or(Px(0)))
131 .max(intrinsic_width)
132 }
133 };
134
135 let height = match effective_constraint.height {
136 DimensionValue::Fixed(value) => value,
137 DimensionValue::Wrap { min, max } => min
138 .unwrap_or(Px(0))
139 .max(intrinsic_height)
140 .min(max.unwrap_or(Px::MAX)),
141 DimensionValue::Fill { min, max } => {
142 let parent_max = input.parent_constraint.height.get_max().unwrap_or(Px::MAX);
143 max.unwrap_or(parent_max)
144 .max(min.unwrap_or(Px(0)))
145 .max(intrinsic_height)
146 }
147 };
148
149 let command = ImageVectorCommand {
150 data: image_args.data.clone(),
151 tint: image_args.tint,
152 };
153
154 input
155 .metadatas
156 .entry(input.current_node_id)
157 .or_default()
158 .push_draw_command(command);
159
160 Ok(ComputedData { width, height })
161 }));
162}
163
164fn px_from_f32(value: f32) -> Px {
165 let clamped = value.max(0.0).min(i32::MAX as f32);
166 Px(clamped.round() as i32)
167}
168
169fn read_source_bytes(
170 source: &ImageVectorSource,
171) -> Result<(Vec<u8>, Option<std::path::PathBuf>), ImageVectorLoadError> {
172 match source {
173 ImageVectorSource::Path(path) => {
174 let bytes = fs::read(path).map_err(|source| ImageVectorLoadError::Io {
175 path: path.clone(),
176 source,
177 })?;
178 let dir = StdPath::new(path).parent().map(|p| p.to_path_buf());
179 Ok((bytes, dir))
180 }
181 ImageVectorSource::Bytes(bytes) => Ok((bytes.as_ref().to_vec(), None)),
182 }
183}
184
185fn build_vector_data(tree: &Tree) -> Result<ImageVectorData, ImageVectorLoadError> {
186 let size = tree.size();
187 let viewport_width = size.width();
188 let viewport_height = size.height();
189
190 if !viewport_width.is_finite()
191 || !viewport_height.is_finite()
192 || viewport_width <= 0.0
193 || viewport_height <= 0.0
194 {
195 return Err(ImageVectorLoadError::InvalidViewport);
196 }
197
198 let mut collector = VectorGeometryCollector::new(viewport_width, viewport_height);
199 visit_group(tree.root(), 1.0, &mut collector)?;
200
201 collector.finish()
202}
203
204fn visit_group(
205 group: &Group,
206 inherited_opacity: f32,
207 collector: &mut VectorGeometryCollector,
208) -> Result<(), ImageVectorLoadError> {
209 if group.clip_path().is_some() || group.mask().is_some() || !group.filters().is_empty() {
210 return Err(ImageVectorLoadError::UnsupportedFeature(
211 "clip paths, masks, and filters are not supported".to_string(),
212 ));
213 }
214
215 if group.blend_mode() != BlendMode::Normal {
216 return Err(ImageVectorLoadError::UnsupportedFeature(
217 "non-normal blend modes".to_string(),
218 ));
219 }
220
221 let accumulated_opacity = inherited_opacity * group.opacity().get();
222
223 for node in group.children() {
224 match node {
225 Node::Group(child) => visit_group(child, accumulated_opacity, collector)?,
226 Node::Path(path) => collector.process_path(path, accumulated_opacity)?,
227 Node::Image(_) | Node::Text(_) => {
228 return Err(ImageVectorLoadError::UnsupportedFeature(
229 "non-path nodes in SVG are not supported".to_string(),
230 ));
231 }
232 }
233 }
234
235 Ok(())
236}
237
238struct VectorGeometryCollector {
239 viewport_width: f32,
240 viewport_height: f32,
241 buffers: VertexBuffers<ImageVectorVertex, u32>,
242}
243
244impl VectorGeometryCollector {
245 fn new(viewport_width: f32, viewport_height: f32) -> Self {
246 Self {
247 viewport_width,
248 viewport_height,
249 buffers: VertexBuffers::new(),
250 }
251 }
252
253 fn process_path(
254 &mut self,
255 path: &Path,
256 inherited_opacity: f32,
257 ) -> Result<(), ImageVectorLoadError> {
258 if !path.is_visible() {
259 return Ok(());
260 }
261
262 if path.rendering_mode() != usvg::ShapeRendering::default() {
263 return Err(ImageVectorLoadError::UnsupportedFeature(
264 "shape-rendering modes are not supported".to_string(),
265 ));
266 }
267
268 let lyon_path = convert_to_lyon_path(path)?;
269
270 match path.paint_order() {
271 PaintOrder::FillAndStroke => {
272 if let Some(fill) = path.fill() {
273 self.tessellate_fill(&lyon_path, fill, inherited_opacity)?;
274 }
275 if let Some(stroke) = path.stroke() {
276 self.tessellate_stroke(&lyon_path, stroke, inherited_opacity)?;
277 }
278 }
279 PaintOrder::StrokeAndFill => {
280 if let Some(stroke) = path.stroke() {
281 self.tessellate_stroke(&lyon_path, stroke, inherited_opacity)?;
282 }
283 if let Some(fill) = path.fill() {
284 self.tessellate_fill(&lyon_path, fill, inherited_opacity)?;
285 }
286 }
287 }
288
289 Ok(())
290 }
291
292 fn tessellate_fill(
293 &mut self,
294 path: &LyonPath,
295 fill: &usvg::Fill,
296 inherited_opacity: f32,
297 ) -> Result<(), ImageVectorLoadError> {
298 let color = color_from_paint(fill.paint(), fill.opacity().get(), inherited_opacity)?;
299 let fill_rule = match fill.rule() {
300 FillRule::EvenOdd => LyonFillRule::EvenOdd,
301 FillRule::NonZero => LyonFillRule::NonZero,
302 };
303
304 let options = FillOptions::default().with_fill_rule(fill_rule);
305 let viewport = [self.viewport_width, self.viewport_height];
306
307 FillTessellator::new().tessellate_path(
308 path,
309 &options,
310 &mut BuffersBuilder::new(&mut self.buffers, |vertex: FillVertex| {
311 ImageVectorVertex::new(vertex.position().to_array(), color, viewport)
312 }),
313 )?;
314
315 Ok(())
316 }
317
318 fn tessellate_stroke(
319 &mut self,
320 path: &LyonPath,
321 stroke: &Stroke,
322 inherited_opacity: f32,
323 ) -> Result<(), ImageVectorLoadError> {
324 if stroke.dasharray().is_some() {
325 return Err(ImageVectorLoadError::UnsupportedFeature(
326 "stroke dash arrays".to_string(),
327 ));
328 }
329
330 let color = color_from_paint(stroke.paint(), stroke.opacity().get(), inherited_opacity)?;
331
332 let mut options = StrokeOptions::default()
333 .with_line_width(stroke.width().get())
334 .with_line_cap(map_line_cap(stroke.linecap()))
335 .with_line_join(map_line_join(stroke.linejoin()));
336
337 options.miter_limit = stroke.miterlimit().get();
338
339 let viewport = [self.viewport_width, self.viewport_height];
340
341 StrokeTessellator::new().tessellate_path(
342 path,
343 &options,
344 &mut BuffersBuilder::new(&mut self.buffers, |vertex: StrokeVertex| {
345 ImageVectorVertex::new(vertex.position().to_array(), color, viewport)
346 }),
347 )?;
348
349 Ok(())
350 }
351
352 fn finish(self) -> Result<ImageVectorData, ImageVectorLoadError> {
353 if self.buffers.vertices.is_empty() || self.buffers.indices.is_empty() {
354 return Err(ImageVectorLoadError::EmptyGeometry);
355 }
356
357 Ok(ImageVectorData::new(
358 self.viewport_width,
359 self.viewport_height,
360 Arc::new(self.buffers.vertices),
361 Arc::new(self.buffers.indices),
362 ))
363 }
364}
365
366fn color_from_paint(
367 paint: &Paint,
368 paint_opacity: f32,
369 inherited_opacity: f32,
370) -> Result<Color, ImageVectorLoadError> {
371 let opacity = (paint_opacity * inherited_opacity).clamp(0.0, 1.0);
372 match paint {
373 Paint::Color(color) => Ok(Color::new(
374 f32::from(color.red) / 255.0,
375 f32::from(color.green) / 255.0,
376 f32::from(color.blue) / 255.0,
377 opacity,
378 )),
379 _ => Err(ImageVectorLoadError::UnsupportedFeature(
380 "only solid color fills and strokes are supported".to_string(),
381 )),
382 }
383}
384
385fn convert_to_lyon_path(path: &Path) -> Result<LyonPath, ImageVectorLoadError> {
386 let transformed = path
387 .data()
388 .clone()
389 .transform(path.abs_transform())
390 .ok_or(ImageVectorLoadError::TransformFailed)?;
391
392 let mut builder = LyonPath::builder().with_svg();
393 for segment in transformed.segments() {
394 match segment {
395 PathSegment::MoveTo(p0) => {
396 builder.move_to(point(p0.x, p0.y));
397 }
398 PathSegment::LineTo(p0) => {
399 builder.line_to(point(p0.x, p0.y));
400 }
401 PathSegment::QuadTo(p0, p1) => {
402 builder.quadratic_bezier_to(point(p0.x, p0.y), point(p1.x, p1.y));
403 }
404 PathSegment::CubicTo(p0, p1, p2) => {
405 builder.cubic_bezier_to(point(p0.x, p0.y), point(p1.x, p1.y), point(p2.x, p2.y));
406 }
407 PathSegment::Close => {
408 builder.close();
409 }
410 }
411 }
412
413 Ok(builder.build())
414}
415
416fn map_line_cap(cap: SvgLineCap) -> LyonLineCap {
417 match cap {
418 SvgLineCap::Butt => LyonLineCap::Butt,
419 SvgLineCap::Round => LyonLineCap::Round,
420 SvgLineCap::Square => LyonLineCap::Square,
421 }
422}
423
424fn map_line_join(join: SvgLineJoin) -> LyonLineJoin {
425 match join {
426 SvgLineJoin::Miter | SvgLineJoin::MiterClip => LyonLineJoin::Miter,
427 SvgLineJoin::Round => LyonLineJoin::Round,
428 SvgLineJoin::Bevel => LyonLineJoin::Bevel,
429 }
430}
431
432impl ImageVectorVertex {
433 fn new(position: [f32; 2], color: Color, viewport: [f32; 2]) -> Self {
434 ImageVectorVertex {
435 position: [position[0] / viewport[0], position[1] / viewport[1]],
436 color,
437 }
438 }
439}