use std::{fs, path::Path as StdPath, sync::Arc};
use lyon_geom::point;
use lyon_path::Path as LyonPath;
use lyon_tessellation::{
BuffersBuilder, FillOptions, FillRule as LyonFillRule, FillTessellator, FillVertex,
LineCap as LyonLineCap, LineJoin as LyonLineJoin, StrokeOptions, StrokeTessellator,
StrokeVertex, VertexBuffers,
};
use tessera_ui::Color;
use thiserror::Error;
use usvg::{
BlendMode, FillRule, Group, LineCap as SvgLineCap, LineJoin as SvgLineJoin, Node, Paint,
PaintOrder, Path, Stroke, Tree, tiny_skia_path::PathSegment,
};
use crate::pipelines::image_vector::command::ImageVectorVertex;
pub use crate::pipelines::image_vector::command::{ImageVectorData, VectorTintMode as TintMode};
#[derive(Clone, Debug)]
pub enum ImageVectorSource {
Path(String),
Bytes(Arc<[u8]>),
}
#[derive(Debug, Error)]
pub enum ImageVectorLoadError {
#[error("failed to read SVG from {path}: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("failed to parse SVG: {0}")]
Parse(#[from] usvg::Error),
#[error("SVG viewport must have finite, positive size")]
InvalidViewport,
#[error("unsupported SVG feature: {0}")]
UnsupportedFeature(String),
#[error("failed to apply SVG transforms")]
TransformFailed,
#[error("tessellation error: {0}")]
Tessellation(#[from] lyon_tessellation::TessellationError),
#[error("SVG produced no renderable paths")]
EmptyGeometry,
}
pub fn load_image_vector_from_source(
source: &ImageVectorSource,
) -> Result<ImageVectorData, ImageVectorLoadError> {
let (bytes, resources_dir) = read_source_bytes(source)?;
let options = usvg::Options {
resources_dir,
..Default::default()
};
let tree = Tree::from_data(&bytes, &options)?;
build_vector_data(&tree)
}
fn read_source_bytes(
source: &ImageVectorSource,
) -> Result<(Vec<u8>, Option<std::path::PathBuf>), ImageVectorLoadError> {
match source {
ImageVectorSource::Path(path) => {
let bytes = fs::read(path).map_err(|source| ImageVectorLoadError::Io {
path: path.clone(),
source,
})?;
let dir = StdPath::new(path).parent().map(|p| p.to_path_buf());
Ok((bytes, dir))
}
ImageVectorSource::Bytes(bytes) => Ok((bytes.as_ref().to_vec(), None)),
}
}
fn build_vector_data(tree: &Tree) -> Result<ImageVectorData, ImageVectorLoadError> {
let size = tree.size();
let viewport_width = size.width();
let viewport_height = size.height();
if !viewport_width.is_finite()
|| !viewport_height.is_finite()
|| viewport_width <= 0.0
|| viewport_height <= 0.0
{
return Err(ImageVectorLoadError::InvalidViewport);
}
let mut collector = VectorGeometryCollector::new(viewport_width, viewport_height);
visit_group(tree.root(), 1.0, &mut collector)?;
collector.finish()
}
fn visit_group(
group: &Group,
inherited_opacity: f32,
collector: &mut VectorGeometryCollector,
) -> Result<(), ImageVectorLoadError> {
if group.clip_path().is_some() || group.mask().is_some() || !group.filters().is_empty() {
return Err(ImageVectorLoadError::UnsupportedFeature(
"clip paths, masks, and filters are not supported".to_string(),
));
}
if group.blend_mode() != BlendMode::Normal {
return Err(ImageVectorLoadError::UnsupportedFeature(
"non-normal blend modes".to_string(),
));
}
let accumulated_opacity = inherited_opacity * group.opacity().get();
for node in group.children() {
match node {
Node::Group(child) => visit_group(child, accumulated_opacity, collector)?,
Node::Path(path) => collector.process_path(path, accumulated_opacity)?,
Node::Image(_) | Node::Text(_) => {
return Err(ImageVectorLoadError::UnsupportedFeature(
"non-path nodes in SVG are not supported".to_string(),
));
}
}
}
Ok(())
}
struct VectorGeometryCollector {
viewport_width: f32,
viewport_height: f32,
buffers: VertexBuffers<ImageVectorVertex, u32>,
}
impl VectorGeometryCollector {
fn new(viewport_width: f32, viewport_height: f32) -> Self {
Self {
viewport_width,
viewport_height,
buffers: VertexBuffers::new(),
}
}
fn process_path(
&mut self,
path: &Path,
inherited_opacity: f32,
) -> Result<(), ImageVectorLoadError> {
if !path.is_visible() {
return Ok(());
}
if path.rendering_mode() != usvg::ShapeRendering::default() {
return Err(ImageVectorLoadError::UnsupportedFeature(
"shape-rendering modes are not supported".to_string(),
));
}
let lyon_path = convert_to_lyon_path(path)?;
match path.paint_order() {
PaintOrder::FillAndStroke => {
if let Some(fill) = path.fill() {
self.tessellate_fill(&lyon_path, fill, inherited_opacity)?;
}
if let Some(stroke) = path.stroke() {
self.tessellate_stroke(&lyon_path, stroke, inherited_opacity)?;
}
}
PaintOrder::StrokeAndFill => {
if let Some(stroke) = path.stroke() {
self.tessellate_stroke(&lyon_path, stroke, inherited_opacity)?;
}
if let Some(fill) = path.fill() {
self.tessellate_fill(&lyon_path, fill, inherited_opacity)?;
}
}
}
Ok(())
}
fn tessellate_fill(
&mut self,
path: &LyonPath,
fill: &usvg::Fill,
inherited_opacity: f32,
) -> Result<(), ImageVectorLoadError> {
let color = color_from_paint(fill.paint(), fill.opacity().get(), inherited_opacity)?;
let fill_rule = match fill.rule() {
FillRule::EvenOdd => LyonFillRule::EvenOdd,
FillRule::NonZero => LyonFillRule::NonZero,
};
let options = FillOptions::default().with_fill_rule(fill_rule);
let viewport = [self.viewport_width, self.viewport_height];
FillTessellator::new().tessellate_path(
path,
&options,
&mut BuffersBuilder::new(&mut self.buffers, |vertex: FillVertex| {
ImageVectorVertex::new(vertex.position().to_array(), color, viewport)
}),
)?;
Ok(())
}
fn tessellate_stroke(
&mut self,
path: &LyonPath,
stroke: &Stroke,
inherited_opacity: f32,
) -> Result<(), ImageVectorLoadError> {
if stroke.dasharray().is_some() {
return Err(ImageVectorLoadError::UnsupportedFeature(
"stroke dash arrays".to_string(),
));
}
let color = color_from_paint(stroke.paint(), stroke.opacity().get(), inherited_opacity)?;
let mut options = StrokeOptions::default()
.with_line_width(stroke.width().get())
.with_line_cap(map_line_cap(stroke.linecap()))
.with_line_join(map_line_join(stroke.linejoin()));
options.miter_limit = stroke.miterlimit().get();
let viewport = [self.viewport_width, self.viewport_height];
StrokeTessellator::new().tessellate_path(
path,
&options,
&mut BuffersBuilder::new(&mut self.buffers, |vertex: StrokeVertex| {
ImageVectorVertex::new(vertex.position().to_array(), color, viewport)
}),
)?;
Ok(())
}
fn finish(self) -> Result<ImageVectorData, ImageVectorLoadError> {
if self.buffers.vertices.is_empty() || self.buffers.indices.is_empty() {
return Err(ImageVectorLoadError::EmptyGeometry);
}
Ok(ImageVectorData::new(
self.viewport_width,
self.viewport_height,
Arc::new(self.buffers.vertices),
Arc::new(self.buffers.indices),
))
}
}
fn color_from_paint(
paint: &Paint,
paint_opacity: f32,
inherited_opacity: f32,
) -> Result<Color, ImageVectorLoadError> {
let opacity = (paint_opacity * inherited_opacity).clamp(0.0, 1.0);
match paint {
Paint::Color(color) => Ok(Color::new(
f32::from(color.red) / 255.0,
f32::from(color.green) / 255.0,
f32::from(color.blue) / 255.0,
opacity,
)),
_ => Err(ImageVectorLoadError::UnsupportedFeature(
"only solid color fills and strokes are supported".to_string(),
)),
}
}
fn convert_to_lyon_path(path: &Path) -> Result<LyonPath, ImageVectorLoadError> {
let transformed = path
.data()
.clone()
.transform(path.abs_transform())
.ok_or(ImageVectorLoadError::TransformFailed)?;
let mut builder = LyonPath::builder().with_svg();
for segment in transformed.segments() {
match segment {
PathSegment::MoveTo(p0) => {
builder.move_to(point(p0.x, p0.y));
}
PathSegment::LineTo(p0) => {
builder.line_to(point(p0.x, p0.y));
}
PathSegment::QuadTo(p0, p1) => {
builder.quadratic_bezier_to(point(p0.x, p0.y), point(p1.x, p1.y));
}
PathSegment::CubicTo(p0, p1, p2) => {
builder.cubic_bezier_to(point(p0.x, p0.y), point(p1.x, p1.y), point(p2.x, p2.y));
}
PathSegment::Close => {
builder.close();
}
}
}
Ok(builder.build())
}
fn map_line_cap(cap: SvgLineCap) -> LyonLineCap {
match cap {
SvgLineCap::Butt => LyonLineCap::Butt,
SvgLineCap::Round => LyonLineCap::Round,
SvgLineCap::Square => LyonLineCap::Square,
}
}
fn map_line_join(join: SvgLineJoin) -> LyonLineJoin {
match join {
SvgLineJoin::Miter | SvgLineJoin::MiterClip => LyonLineJoin::Miter,
SvgLineJoin::Round => LyonLineJoin::Round,
SvgLineJoin::Bevel => LyonLineJoin::Bevel,
}
}
impl ImageVectorVertex {
fn new(position: [f32; 2], color: Color, viewport: [f32; 2]) -> Self {
ImageVectorVertex {
position: [position[0] / viewport[0], position[1] / viewport[1]],
color,
}
}
}