Skip to main content

tessera_components/
image_vector.rs

1//! Scalable vector images.
2//!
3//! ## Usage
4//!
5//! Display SVG artwork at any size.
6
7use 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}