From 17585c328165a5095e182557d1362f6aef4e6240 Mon Sep 17 00:00:00 2001 From: Aaron Muir Hamilton Date: Mon, 22 Apr 2024 13:43:31 -0400 Subject: [PATCH] Resurrect pico_svg and use it to replace vello_svg in the examples. (#518) * WIP: resurrect pico_svg and use it to load scenes. * Add viewbox and width/height support to pico_svg. * Accept more single-function transforms in pico_svg. * Add copyright header to pico_svg.rs * Remove vello_svg. * Clean up pico_svg lints. * Parse multiple transforms in attribute. * Add scaleX and scaleY to pico_svg. * Render fuchsia unit square on error, to work around #291. * Print Display rather than Debug for Err in svg.rs * Interpret combined viewBox and width/height correctly. * Fix lint. * Clean up std::result qualified names. * Mention new vello_svg repository in ## Integrations * Improve color parsing in pico_svg. * Address review comments. --- ARCHITECTURE.md | 1 - Cargo.lock | 209 +-------------------- Cargo.toml | 2 - README.md | 8 +- examples/scenes/Cargo.toml | 3 +- examples/scenes/src/lib.rs | 1 + examples/scenes/src/pico_svg.rs | 280 +++++++++++++++++++++++++++ examples/scenes/src/svg.rs | 65 +++++-- integrations/vello_svg/Cargo.toml | 16 -- integrations/vello_svg/src/lib.rs | 301 ------------------------------ 10 files changed, 339 insertions(+), 547 deletions(-) create mode 100644 examples/scenes/src/pico_svg.rs delete mode 100644 integrations/vello_svg/Cargo.toml delete mode 100644 integrations/vello_svg/src/lib.rs diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4d28da71c..74d45fe52 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -38,7 +38,6 @@ The repository is structured as such: - `tests/` - Helper code for writing tests; current has a single smoke test and not much else. - `doc/` - Various documents detailing the vision for Vello as it was developed. This directory should probably be refactored away; adding to it not recommended. - `examples/` - Example projects using Vello. Each example is its own crate, with its own dependencies. The simplest example is the `shapes` one. -- `integrations/vello_svg` - An SVG rendered based on Vello and usvg. Used in examples. May be moved to `crates/` in the future. - `shader/` - This is where the magic happens. WGSL shaders that define the compute operations (often variations of prefix sum) that Vello does to render a scene. - `shared/` - Shared types, functions and constants included in other shaders through non-standard `#import` preprocessor directives (see "Shader templating"). - `src/` - Code for the main `vello` crate. diff --git a/Cargo.lock b/Cargo.lock index 528d69097..a08d381d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1449,12 +1449,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" -[[package]] -name = "data-url" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" - [[package]] name = "derive_more" version = "0.99.17" @@ -1717,12 +1711,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - [[package]] name = "flume" version = "0.11.0" @@ -1741,29 +1729,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "fontconfig-parser" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d" -dependencies = [ - "roxmltree", -] - -[[package]] -name = "fontdb" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" -dependencies = [ - "fontconfig-parser", - "log", - "memmap2", - "slotmap", - "tinyvec", - "ttf-parser", -] - [[package]] name = "foreign-types" version = "0.5.0" @@ -2156,12 +2121,6 @@ dependencies = [ "tiff", ] -[[package]] -name = "imagesize" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" - [[package]] name = "indexmap" version = "1.9.3" @@ -2332,15 +2291,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "kurbo" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" -dependencies = [ - "arrayvec", -] - [[package]] name = "kurbo" version = "0.11.0" @@ -2800,7 +2750,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caaf7fec601d640555d9a4cab7343eba1e1c7a5a71c9993ff63b4c26bc5d50c5" dependencies = [ - "kurbo 0.11.0", + "kurbo", "smallvec", ] @@ -3037,12 +2987,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rctree" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" - [[package]] name = "read-fonts" version = "0.19.0" @@ -3217,22 +3161,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "rustybuzz" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0ae5692c5beaad6a9e22830deeed7874eae8a4e3ba4076fb48e12c56856222c" -dependencies = [ - "bitflags 2.5.0", - "bytemuck", - "smallvec", - "ttf-parser", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - [[package]] name = "ruzstd" version = "0.5.0" @@ -3271,9 +3199,9 @@ dependencies = [ "inquire", "instant", "rand", + "roxmltree", "ureq", "vello", - "vello_svg", ] [[package]] @@ -3388,21 +3316,6 @@ dependencies = [ "winit", ] -[[package]] -name = "simplecss" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" -dependencies = [ - "log", -] - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "skrifa" version = "0.19.0" @@ -3509,9 +3422,6 @@ name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] [[package]] name = "strsim" @@ -3531,16 +3441,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83ba502a3265efb76efb89b0a2f7782ad6f2675015d4ce37e4b547dda42b499" -[[package]] -name = "svgtypes" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70" -dependencies = [ - "kurbo 0.9.5", - "siphasher", -] - [[package]] name = "syn" version = "1.0.109" @@ -3816,18 +3716,6 @@ version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" -[[package]] -name = "unicode-bidi-mirroring" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" - -[[package]] -name = "unicode-ccc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" - [[package]] name = "unicode-ident" version = "1.0.12" @@ -3843,30 +3731,12 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-properties" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" - -[[package]] -name = "unicode-script" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd" - [[package]] name = "unicode-segmentation" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" -[[package]] -name = "unicode-vo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" - [[package]] name = "unicode-width" version = "0.1.11" @@ -3913,67 +3783,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "usvg" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756" -dependencies = [ - "base64", - "log", - "pico-args", - "usvg-parser", - "usvg-text-layout", - "usvg-tree", - "xmlwriter", -] - -[[package]] -name = "usvg-parser" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc" -dependencies = [ - "data-url", - "flate2", - "imagesize", - "kurbo 0.9.5", - "log", - "roxmltree", - "simplecss", - "siphasher", - "svgtypes", - "usvg-tree", -] - -[[package]] -name = "usvg-text-layout" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d383a3965de199d7f96d4e11a44dd859f46e86de7f3dca9a39bf82605da0a37c" -dependencies = [ - "fontdb", - "kurbo 0.9.5", - "log", - "rustybuzz", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "usvg-tree", -] - -[[package]] -name = "usvg-tree" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3" -dependencies = [ - "rctree", - "strict-num", - "svgtypes", - "tiny-skia-path", -] - [[package]] name = "utf8-width" version = "0.1.7" @@ -4035,14 +3844,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "vello_svg" -version = "0.0.0" -dependencies = [ - "usvg", - "vello", -] - [[package]] name = "vello_tests" version = "0.0.0" @@ -4967,12 +4768,6 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index a88304efa..b28c60dec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,8 +5,6 @@ members = [ "crates/shaders", "crates/tests", - "integrations/vello_svg", - "examples/headless", "examples/with_winit", "examples/with_bevy", diff --git a/README.md b/README.md index 98d0126ac..6a4a363a5 100644 --- a/README.md +++ b/README.md @@ -120,11 +120,9 @@ More formal benchmarks are on their way. ### SVG -This repository also includes [`vello_svg`](./integrations/vello_svg/), which supports converting a [`usvg`](https://crates.io/crates/usvg) `Tree` into a Vello scene. +[`vello_svg`](https://github.com/linebender/vello_svg/), supports converting a [`usvg`](https://crates.io/crates/usvg) `Tree` into a Vello scene. -This is currently incomplete; see its crate level documentation for more information. - -This is used in the [winit](#winit) example for the SVG rendering. +It is currently incomplete; see its documentation for more information. ### Lottie @@ -139,7 +137,7 @@ Examples must be selected using the `--package` (or `-p`) Cargo flag. ### Winit Our [winit] example ([examples/with_winit](https://github.com/linebender/vello/tree/main/examples/with_winit)) demonstrates rendering to a [winit] window. -By default, this renders the [GhostScript Tiger] as well as all SVG files you add in the [examples/assets/downloads/](https://github.com/linebender/vello/tree/main/examples/assets/downloads) directory using [`vello_svg`](#svg). +By default, this renders the [GhostScript Tiger] as well as all SVG files you add in the [examples/assets/downloads/](https://github.com/linebender/vello/tree/main/examples/assets/downloads) directory. A custom list of SVG file paths (and directories to render all SVG files from) can be provided as arguments instead. It also includes a collection of test scenes showing the capabilities of `vello`, which can be shown with `--test-scenes`. diff --git a/examples/scenes/Cargo.toml b/examples/scenes/Cargo.toml index d9be6ea4f..addcf49b2 100644 --- a/examples/scenes/Cargo.toml +++ b/examples/scenes/Cargo.toml @@ -11,12 +11,13 @@ workspace = true [dependencies] vello = { path = "../../" } -vello_svg = { path = "../../integrations/vello_svg" } anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] } image = "0.24.9" rand = "0.8.5" instant = { workspace = true } +# for pico_svg +roxmltree = "0.19" # Used for the `download` command [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/examples/scenes/src/lib.rs b/examples/scenes/src/lib.rs index cc210cfb9..db6fc4271 100644 --- a/examples/scenes/src/lib.rs +++ b/examples/scenes/src/lib.rs @@ -5,6 +5,7 @@ pub mod download; mod images; mod mmark; +mod pico_svg; mod simple_text; mod svg; mod test_scenes; diff --git a/examples/scenes/src/pico_svg.rs b/examples/scenes/src/pico_svg.rs new file mode 100644 index 000000000..cac14f3a6 --- /dev/null +++ b/examples/scenes/src/pico_svg.rs @@ -0,0 +1,280 @@ +// Copyright 2024 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! A loader for a tiny fragment of SVG + +use std::str::FromStr; + +use roxmltree::{Document, Node}; +use vello::{ + kurbo::{Affine, BezPath, Point, Size, Vec2}, + peniko::Color, +}; + +pub struct PicoSvg { + pub items: Vec, + pub size: Size, +} + +pub enum Item { + Fill(FillItem), + Stroke(StrokeItem), +} + +pub struct StrokeItem { + pub width: f64, + pub color: Color, + pub path: BezPath, +} + +pub struct FillItem { + pub color: Color, + pub path: BezPath, +} + +struct Parser<'a> { + scale: f64, + items: &'a mut Vec, +} + +impl PicoSvg { + pub fn load(xml_string: &str, scale: f64) -> Result> { + let doc = Document::parse(xml_string)?; + let root = doc.root_element(); + let mut items = Vec::new(); + let mut parser = Parser::new(&mut items, scale); + let width = root.attribute("width").and_then(|s| f64::from_str(s).ok()); + let height = root.attribute("height").and_then(|s| f64::from_str(s).ok()); + let (origin, viewbox_size) = root + .attribute("viewBox") + .and_then(|vb_attr| { + let vs: Vec = vb_attr + .split(' ') + .map(|s| f64::from_str(s).unwrap()) + .collect(); + if let &[x, y, width, height] = vs.as_slice() { + Some((Point { x, y }, Size { width, height })) + } else { + None + } + }) + .unzip(); + + let mut transform = if let Some(origin) = origin { + Affine::translate(origin.to_vec2() * -1.0) + } else { + Affine::IDENTITY + }; + + transform *= match (width, height, viewbox_size) { + (None, None, Some(_)) => Affine::IDENTITY, + (Some(w), Some(h), Some(s)) => { + Affine::scale_non_uniform(1.0 / s.width * w, 1.0 / s.height * h) + } + (Some(w), None, Some(s)) => Affine::scale(1.0 / s.width * w), + (None, Some(h), Some(s)) => Affine::scale(1.0 / s.height * h), + _ => Affine::IDENTITY, + }; + + let size = match (width, height, viewbox_size) { + (None, None, Some(s)) => s, + (mw, mh, None) => Size { + width: mw.unwrap_or(300_f64), + height: mh.unwrap_or(150_f64), + }, + (Some(w), None, Some(s)) => Size { + width: w, + height: 1.0 / w * s.width * s.height, + }, + (None, Some(h), Some(s)) => Size { + width: 1.0 / h * s.height * s.width, + height: h, + }, + (Some(width), Some(height), Some(_)) => Size { width, height }, + }; + + transform *= if scale >= 0.0 { + Affine::scale(scale) + } else { + Affine::new([-scale, 0.0, 0.0, scale, 0.0, 0.0]) + }; + let props = RecursiveProperties { + transform, + fill: Some(Color::BLACK), + }; + // The root element is the svg document element, which we don't care about + for node in root.children() { + parser.rec_parse(node, &props)?; + } + Ok(PicoSvg { items, size }) + } +} + +#[derive(Clone)] +struct RecursiveProperties { + transform: Affine, + fill: Option, +} + +impl<'a> Parser<'a> { + fn new(items: &'a mut Vec, scale: f64) -> Parser<'a> { + Parser { scale, items } + } + + fn rec_parse( + &mut self, + node: Node, + properties: &RecursiveProperties, + ) -> Result<(), Box> { + if node.is_element() { + let mut properties = properties.clone(); + if let Some(fill_color) = node.attribute("fill") { + if fill_color == "none" { + properties.fill = None; + } else { + let color = parse_color(fill_color); + let color = modify_opacity(color, "fill-opacity", node); + // TODO: Handle recursive opacity properly + let color = modify_opacity(color, "opacity", node); + properties.fill = Some(color); + } + } + if let Some(transform) = node.attribute("transform") { + properties.transform *= parse_transform(transform); + } + match node.tag_name().name() { + "g" => { + for child in node.children() { + self.rec_parse(child, &properties)?; + } + } + "path" => { + let d = node.attribute("d").ok_or("missing 'd' attribute")?; + let bp = BezPath::from_svg(d)?; + let path = properties.transform * bp; + if let Some(color) = properties.fill { + self.items.push(Item::Fill(FillItem { + color, + path: path.clone(), + })); + } + if let Some(stroke_color) = node.attribute("stroke") { + if stroke_color != "none" { + let width = node + .attribute("stroke-width") + .map(|a| f64::from_str(a).unwrap_or(1.0)) + .unwrap_or(1.0) + * self.scale.abs(); + let color = parse_color(stroke_color); + let color = modify_opacity(color, "stroke-opacity", node); + // TODO: Handle recursive opacity properly + let color = modify_opacity(color, "opacity", node); + self.items + .push(Item::Stroke(StrokeItem { width, color, path })); + } + } + } + other => eprintln!("Unhandled node type {other}"), + } + } + Ok(()) + } +} + +fn parse_transform(transform: &str) -> Affine { + let mut nt = Affine::IDENTITY; + for ts in transform.split(')').map(str::trim) { + nt *= if let Some(s) = ts.strip_prefix("matrix(") { + let vals = s + .split(|c| matches!(c, ',' | ' ')) + .map(str::parse) + .collect::, _>>() + .expect("Could parse all values of 'matrix' as floats"); + Affine::new( + vals.try_into() + .expect("Should be six arguments to `matrix`"), + ) + } else if let Some(s) = ts.strip_prefix("translate(") { + if let Ok(vals) = s + .split(|c| matches!(c, ',' | ' ')) + .map(str::trim) + .map(str::parse) + .collect::, _>>() + { + match vals.as_slice() { + &[x, y] => Affine::translate(Vec2 { x, y }), + _ => Affine::IDENTITY, + } + } else { + Affine::IDENTITY + } + } else if let Some(s) = ts.strip_prefix("scale(") { + if let Ok(vals) = s + .split(|c| matches!(c, ',' | ' ')) + .map(str::trim) + .map(str::parse) + .collect::, _>>() + { + match *vals.as_slice() { + [x, y] => Affine::scale_non_uniform(x, y), + [x] => Affine::scale(x), + _ => Affine::IDENTITY, + } + } else { + Affine::IDENTITY + } + } else if let Some(s) = ts.strip_prefix("scaleX(") { + s.trim() + .parse() + .ok() + .map(|x| Affine::scale_non_uniform(x, 1.0)) + .unwrap_or(Affine::IDENTITY) + } else if let Some(s) = ts.strip_prefix("scaleY(") { + s.trim() + .parse() + .ok() + .map(|y| Affine::scale_non_uniform(1.0, y)) + .unwrap_or(Affine::IDENTITY) + } else { + if !ts.is_empty() { + eprintln!("Did not understand transform attribute {ts:?})"); + } + Affine::IDENTITY + }; + } + nt +} + +fn parse_color(color: &str) -> Color { + let color = color.trim(); + if let Some(c) = Color::parse(color) { + c + } else if let Some(s) = color.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')) { + let mut iter = s + .split(|c| matches!(c, ',' | ' ')) + .map(str::trim) + .map(u8::from_str); + + let r = iter.next().unwrap().unwrap(); + let g = iter.next().unwrap().unwrap(); + let b = iter.next().unwrap().unwrap(); + Color::rgb8(r, g, b) + } else { + Color::rgba8(255, 0, 255, 0x80) + } +} + +fn modify_opacity(mut color: Color, attr_name: &str, node: Node) -> Color { + if let Some(opacity) = node.attribute(attr_name) { + let alpha: f64 = if let Some(o) = opacity.strip_suffix('%') { + let pctg = o.parse().unwrap_or(100.0); + pctg * 0.01 + } else { + opacity.parse().unwrap_or(1.0) + }; + color.a = (alpha.min(1.0).max(0.0) * 255.0).round() as u8; + color + } else { + color + } +} diff --git a/examples/scenes/src/svg.rs b/examples/scenes/src/svg.rs index f14d3e9df..7336895b3 100644 --- a/examples/scenes/src/svg.rs +++ b/examples/scenes/src/svg.rs @@ -4,12 +4,14 @@ use std::fs::read_dir; use std::path::{Path, PathBuf}; -use anyhow::{Ok, Result}; +use anyhow::Result; use instant::Instant; -use vello::kurbo::Vec2; -use vello::Scene; -use vello_svg::usvg; -use vello_svg::usvg::TreeParsing; + +use vello::{ + kurbo::{Affine, Rect, Stroke, Vec2}, + peniko::{Color, Fill}, + Scene, +}; use crate::{ExampleScene, SceneParams, SceneSet}; @@ -93,16 +95,51 @@ pub fn svg_function_of>( contents: impl FnOnce() -> R + Send + 'static, ) -> impl FnMut(&mut Scene, &mut SceneParams) { fn render_svg_contents(name: &str, contents: &str) -> (Scene, Vec2) { + use crate::pico_svg::*; let start = Instant::now(); - let svg = usvg::Tree::from_str(contents, &usvg::Options::default()) - .unwrap_or_else(|e| panic!("failed to parse svg file {name}: {e}")); - eprintln!("Parsed svg {name} in {:?}", start.elapsed()); - let start = Instant::now(); - let mut new_scene = Scene::new(); - vello_svg::render_tree(&mut new_scene, &svg); - let resolution = Vec2::new(svg.size.width() as f64, svg.size.height() as f64); - eprintln!("Encoded svg {name} in {:?}", start.elapsed()); - (new_scene, resolution) + match PicoSvg::load(contents, 1.0) { + Ok(PicoSvg { items, size }) => { + eprintln!("Parsed svg {name} in {:?}", start.elapsed()); + let start = Instant::now(); + let mut new_scene = Scene::new(); + for item in items { + match item { + Item::Fill(fill) => { + new_scene.fill( + Fill::NonZero, + Affine::IDENTITY, + fill.color, + None, + &fill.path, + ); + } + Item::Stroke(stroke) => { + new_scene.stroke( + &Stroke::new(stroke.width), + Affine::IDENTITY, + stroke.color, + None, + &stroke.path, + ); + } + } + } + eprintln!("Encoded svg {name} in {:?}", start.elapsed()); + (new_scene, size.to_vec2()) + } + Err(e) => { + eprintln!("Failed to load svg: {e}"); + let mut error_scene = Scene::new(); + error_scene.fill( + Fill::NonZero, + Affine::IDENTITY, + Color::FUCHSIA, + None, + &Rect::new(0.0, 0.0, 1.0, 1.0), + ); + (error_scene, Vec2::new(1.0, 1.0)) + } + } } let mut cached_scene = None; #[cfg(not(target_arch = "wasm32"))] diff --git a/integrations/vello_svg/Cargo.toml b/integrations/vello_svg/Cargo.toml deleted file mode 100644 index 9693712fb..000000000 --- a/integrations/vello_svg/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "vello_svg" -description = "Render a usvg document to a Vello scene" -categories = ["rendering", "graphics"] -keywords = ["2d", "vector-graphics", "vello"] -edition.workspace = true -license.workspace = true -repository.workspace = true -publish = false - -[lints] -workspace = true - -[dependencies] -vello = { path = "../../" } -usvg = "0.37.0" diff --git a/integrations/vello_svg/src/lib.rs b/integrations/vello_svg/src/lib.rs deleted file mode 100644 index 2a00055a6..000000000 --- a/integrations/vello_svg/src/lib.rs +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright 2023 the Vello Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! Append a [`usvg::Tree`] to a Vello [`Scene`] -//! -//! This currently lacks support for a [number of important](crate#unsupported-features) SVG features. -//! This is because this integration was developed for examples, which only need to support enough SVG -//! to demonstrate Vello. -//! -//! However, this is also intended to be the preferred integration between Vello and [usvg], so [consider -//! contributing](https://github.com/linebender/vello) if you need a feature which is missing. -//! -//! [`render_tree_with`] is the primary entry point function, which supports choosing the behaviour -//! when [unsupported features](crate#unsupported-features) are detected. In a future release where there are -//! no unsupported features, this may be phased out -//! -//! [`render_tree`] is a convenience wrapper around [`render_tree_with`] which renders an indicator around not -//! yet supported features -//! -//! This crate also re-exports [`usvg`], to make handling dependency versions easier -//! -//! # Unsupported features -//! -//! Missing features include: -//! - embedded images -//! - text -//! - group opacity -//! - mix-blend-modes -//! - clipping -//! - masking -//! - filter effects -//! - group background -//! - path visibility -//! - path paint order -//! - path shape-rendering -//! - patterns - -use std::convert::Infallible; -use usvg::NodeExt; -use vello::kurbo::{Affine, BezPath, Point, Rect, Stroke}; -use vello::peniko::{Brush, Color, Fill}; -use vello::Scene; - -/// Re-export vello. -pub use vello; - -/// Re-export usvg. -pub use usvg; - -/// Append a [`usvg::Tree`] into a Vello [`Scene`], with default error handling -/// This will draw a red box over (some) unsupported elements -/// -/// Calls [`render_tree_with`] with an error handler implementing the above. -/// -/// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features -pub fn render_tree(scene: &mut Scene, svg: &usvg::Tree) { - render_tree_with(scene, svg, default_error_handler).unwrap_or_else(|e| match e {}); -} - -/// Append a [`usvg::Tree`] into a Vello [`Scene`]. -/// -/// Calls [`render_tree_with`] with [`default_error_handler`]. -/// This will draw a red box over unsupported element types. -/// -/// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features -pub fn render_tree_with Result<(), E>, E>( - scene: &mut Scene, - svg: &usvg::Tree, - mut on_err: F, -) -> Result<(), E> { - for elt in svg.root.descendants() { - let transform = { - let usvg::Transform { - sx, - kx, - ky, - sy, - tx, - ty, - } = elt.abs_transform(); - Affine::new([sx, kx, ky, sy, tx, ty].map(f64::from)) - }; - match &*elt.borrow() { - usvg::NodeKind::Group(_) => {} - usvg::NodeKind::Path(path) => { - let mut local_path = BezPath::new(); - // The semantics of SVG paths don't line up with `BezPath`; we - // must manually track initial points - let mut just_closed = false; - let mut most_recent_initial = (0., 0.); - for elt in path.data.segments() { - match elt { - usvg::tiny_skia_path::PathSegment::MoveTo(p) => { - if std::mem::take(&mut just_closed) { - local_path.move_to(most_recent_initial); - } - most_recent_initial = (p.x.into(), p.y.into()); - local_path.move_to(most_recent_initial); - } - usvg::tiny_skia_path::PathSegment::LineTo(p) => { - if std::mem::take(&mut just_closed) { - local_path.move_to(most_recent_initial); - } - local_path.line_to(Point::new(p.x as f64, p.y as f64)); - } - usvg::tiny_skia_path::PathSegment::QuadTo(p1, p2) => { - if std::mem::take(&mut just_closed) { - local_path.move_to(most_recent_initial); - } - local_path.quad_to( - Point::new(p1.x as f64, p1.y as f64), - Point::new(p2.x as f64, p2.y as f64), - ); - } - usvg::tiny_skia_path::PathSegment::CubicTo(p1, p2, p3) => { - if std::mem::take(&mut just_closed) { - local_path.move_to(most_recent_initial); - } - local_path.curve_to( - Point::new(p1.x as f64, p1.y as f64), - Point::new(p2.x as f64, p2.y as f64), - Point::new(p3.x as f64, p3.y as f64), - ); - } - usvg::tiny_skia_path::PathSegment::Close => { - just_closed = true; - local_path.close_path(); - } - } - } - - // FIXME: let path.paint_order determine the fill/stroke order. - - if let Some(fill) = &path.fill { - if let Some((brush, brush_transform)) = - paint_to_brush(&fill.paint, fill.opacity) - { - scene.fill( - match fill.rule { - usvg::FillRule::NonZero => Fill::NonZero, - usvg::FillRule::EvenOdd => Fill::EvenOdd, - }, - transform, - &brush, - Some(brush_transform), - &local_path, - ); - } else { - on_err(scene, &elt)?; - } - } - if let Some(stroke) = &path.stroke { - if let Some((brush, brush_transform)) = - paint_to_brush(&stroke.paint, stroke.opacity) - { - let mut conv_stroke = Stroke::new(stroke.width.get() as f64) - .with_caps(match stroke.linecap { - usvg::LineCap::Butt => vello::kurbo::Cap::Butt, - usvg::LineCap::Round => vello::kurbo::Cap::Round, - usvg::LineCap::Square => vello::kurbo::Cap::Square, - }) - .with_join(match stroke.linejoin { - usvg::LineJoin::Miter | usvg::LineJoin::MiterClip => { - vello::kurbo::Join::Miter - } - usvg::LineJoin::Round => vello::kurbo::Join::Round, - usvg::LineJoin::Bevel => vello::kurbo::Join::Bevel, - }) - .with_miter_limit(stroke.miterlimit.get() as f64); - if let Some(dash_array) = stroke.dasharray.as_ref() { - conv_stroke = conv_stroke.with_dashes( - stroke.dashoffset as f64, - dash_array.iter().map(|x| *x as f64), - ); - } - scene.stroke( - &conv_stroke, - transform, - &brush, - Some(brush_transform), - &local_path, - ); - } else { - on_err(scene, &elt)?; - } - } - } - usvg::NodeKind::Image(_) => { - on_err(scene, &elt)?; - } - usvg::NodeKind::Text(_) => { - on_err(scene, &elt)?; - } - } - } - Ok(()) -} - -/// Error handler function for [`render_tree_with`] which draws a transparent red box -/// instead of unsupported SVG features -pub fn default_error_handler(scene: &mut Scene, node: &usvg::Node) -> Result<(), Infallible> { - if let Some(bb) = node.calculate_bbox() { - let rect = Rect { - x0: bb.left() as f64, - y0: bb.top() as f64, - x1: bb.right() as f64, - y1: bb.bottom() as f64, - }; - scene.fill( - Fill::NonZero, - Affine::IDENTITY, - Color::RED.with_alpha_factor(0.5), - None, - &rect, - ); - } - Ok(()) -} - -fn paint_to_brush(paint: &usvg::Paint, opacity: usvg::Opacity) -> Option<(Brush, Affine)> { - match paint { - usvg::Paint::Color(color) => Some(( - Brush::Solid(Color::rgba8( - color.red, - color.green, - color.blue, - opacity.to_u8(), - )), - Affine::IDENTITY, - )), - usvg::Paint::LinearGradient(gr) => { - let stops: Vec = gr - .stops - .iter() - .map(|stop| { - let mut cstop = vello::peniko::ColorStop::default(); - cstop.color.r = stop.color.red; - cstop.color.g = stop.color.green; - cstop.color.b = stop.color.blue; - cstop.color.a = (stop.opacity * opacity).to_u8(); - cstop.offset = stop.offset.get(); - cstop - }) - .collect(); - let start = Point::new(gr.x1 as f64, gr.y1 as f64); - let end = Point::new(gr.x2 as f64, gr.y2 as f64); - let arr = [ - gr.transform.sx, - gr.transform.ky, - gr.transform.kx, - gr.transform.sy, - gr.transform.tx, - gr.transform.ty, - ] - .map(f64::from); - let transform = Affine::new(arr); - let gradient = - vello::peniko::Gradient::new_linear(start, end).with_stops(stops.as_slice()); - Some((Brush::Gradient(gradient), transform)) - } - usvg::Paint::RadialGradient(gr) => { - let stops: Vec = gr - .stops - .iter() - .map(|stop| { - let mut cstop = vello::peniko::ColorStop::default(); - cstop.color.r = stop.color.red; - cstop.color.g = stop.color.green; - cstop.color.b = stop.color.blue; - cstop.color.a = (stop.opacity * opacity).to_u8(); - cstop.offset = stop.offset.get(); - cstop - }) - .collect(); - - let start_center = Point::new(gr.cx as f64, gr.cy as f64); - let end_center = Point::new(gr.fx as f64, gr.fy as f64); - let start_radius = 0_f32; - let end_radius = gr.r.get(); - let arr = [ - gr.transform.sx, - gr.transform.ky, - gr.transform.kx, - gr.transform.sy, - gr.transform.tx, - gr.transform.ty, - ] - .map(f64::from); - let transform = Affine::new(arr); - let gradient = vello::peniko::Gradient::new_two_point_radial( - start_center, - start_radius, - end_center, - end_radius, - ) - .with_stops(stops.as_slice()); - Some((Brush::Gradient(gradient), transform)) - } - usvg::Paint::Pattern(_) => None, - } -}