Lay Groundwork for Rust-based SVG rasterization (#1422)

* Add functions for constructing a usvg tree

* Actually encode the image in the usvg tree

* Implement path translation

* Render document using resvg
This commit is contained in:
Dennis Kobert
2023-09-30 11:43:24 +02:00
committed by Keavon Chambers
parent 34f2d61257
commit e1cdb2242d
10 changed files with 585 additions and 46 deletions

View File

@@ -9,25 +9,10 @@ license = "MIT OR Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
std = [
"dyn-any",
"dyn-any/std",
"alloc",
"glam/std",
"specta",
"num-traits/std",
"rustybuzz",
"image",
]
std = ["dyn-any", "dyn-any/std", "alloc", "glam/std", "specta", "num-traits/std", "rustybuzz", "image"]
default = ["async", "serde", "kurbo", "log", "std", "rand_chacha", "wasm"]
log = ["dep:log"]
serde = [
"dep:serde",
"glam/serde",
"bezier-rs/serde",
"bezier-rs/serde",
"base64",
]
serde = ["dep:serde", "glam/serde", "bezier-rs/serde", "bezier-rs/serde", "base64"]
gpu = ["spirv-std", "glam/bytemuck", "dyn-any", "glam/libm"]
async = ["async-trait", "alloc"]
nightly = []
@@ -77,6 +62,7 @@ num-traits = { version = "0.2.15", default-features = false, features = [
wasm-bindgen = { workspace = true, optional = true }
js-sys = { version = "0.3.55", optional = true }
usvg = "0.35.0"
[dependencies.web-sys]
version = "0.3.4"

View File

@@ -1,13 +1,14 @@
use crate::raster::{BlendMode, ImageFrame};
use crate::vector::VectorData;
use crate::vector::{subpath, VectorData};
use crate::{Color, Node};
use bezier_rs::BezierHandles;
use dyn_any::{DynAny, StaticType};
use node_macro::node_fn;
use core::future::Future;
use core::ops::{Deref, DerefMut};
use glam::IVec2;
use glam::{DAffine2, DVec2, IVec2, UVec2};
pub mod renderer;
@@ -198,6 +199,110 @@ where
impl GraphicGroup {
pub const EMPTY: Self = Self(Vec::new());
pub fn to_usvg_tree(&self, resolution: UVec2, viewbox: [DVec2; 2]) -> usvg::Tree {
let root_node = usvg::Node::new(usvg::NodeKind::Group(usvg::Group::default()));
let tree = usvg::Tree {
size: usvg::Size::from_wh(resolution.x as f32, resolution.y as f32).unwrap(),
view_box: usvg::ViewBox {
rect: usvg::NonZeroRect::from_ltrb(viewbox[0].x as f32, viewbox[0].y as f32, viewbox[1].x as f32, viewbox[1].y as f32).unwrap(),
aspect: usvg::AspectRatio::default(),
},
root: root_node.clone(),
};
for element in self.0.iter() {
root_node.append(element.to_usvg_node());
}
tree
}
}
impl GraphicElement {
fn to_usvg_node(&self) -> usvg::Node {
fn to_transform(transform: DAffine2) -> usvg::Transform {
let cols = transform.to_cols_array();
usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32)
}
match &self.graphic_element_data {
GraphicElementData::VectorShape(vector_data) => {
use usvg::tiny_skia_path::PathBuilder;
let mut builder = PathBuilder::new();
let transform = to_transform(vector_data.transform);
let style = &vector_data.style;
for subpath in vector_data.subpaths.iter() {
let start = vector_data.transform.transform_point2(subpath[0].anchor);
builder.move_to(start.x as f32, start.y as f32);
for bezier in subpath.iter() {
bezier.apply_transformation(|pos| vector_data.transform.transform_point2(pos));
let end = bezier.end;
match bezier.handles {
BezierHandles::Linear => builder.line_to(end.x as f32, end.y as f32),
BezierHandles::Quadratic { handle } => builder.quad_to(handle.x as f32, handle.y as f32, end.x as f32, end.y as f32),
BezierHandles::Cubic { handle_start, handle_end } => {
builder.cubic_to(handle_start.x as f32, handle_start.y as f32, handle_end.x as f32, handle_end.y as f32, end.x as f32, end.y as f32)
}
}
}
if subpath.closed {
builder.close()
}
}
let path = builder.finish().unwrap();
let mut path = usvg::Path::new(path.into());
path.transform = transform;
// TODO: use proper style
path.fill = None;
path.stroke = Some(usvg::Stroke::default());
usvg::Node::new(usvg::NodeKind::Path(path))
}
GraphicElementData::ImageFrame(image_frame) => {
if image_frame.image.width * image_frame.image.height == 0 {
return usvg::Node::new(usvg::NodeKind::Group(usvg::Group::default()));
}
let png = image_frame.image.to_png();
usvg::Node::new(usvg::NodeKind::Image(usvg::Image {
id: String::new(),
transform: to_transform(image_frame.transform),
visibility: usvg::Visibility::Visible,
view_box: usvg::ViewBox {
rect: usvg::NonZeroRect::from_xywh(0., 0., 1., 1.).unwrap(),
aspect: usvg::AspectRatio::default(),
},
rendering_mode: usvg::ImageRendering::OptimizeSpeed,
kind: usvg::ImageKind::PNG(png.into()),
}))
}
GraphicElementData::Text(text) => usvg::Node::new(usvg::NodeKind::Text(usvg::Text {
id: String::new(),
transform: usvg::Transform::identity(),
rendering_mode: usvg::TextRendering::OptimizeSpeed,
positions: Vec::new(),
rotate: Vec::new(),
writing_mode: usvg::WritingMode::LeftToRight,
chunks: vec![usvg::TextChunk {
text: text.clone(),
x: None,
y: None,
anchor: usvg::TextAnchor::Start,
spans: vec![],
text_flow: usvg::TextFlow::Linear,
}],
})),
GraphicElementData::GraphicGroup(group) => {
let group_element = usvg::Node::new(usvg::NodeKind::Group(usvg::Group::default()));
for element in group.0.iter() {
group_element.append(element.to_usvg_node());
}
group_element
}
// TODO
GraphicElementData::Artboard(board) => usvg::Node::new(usvg::NodeKind::Group(usvg::Group::default())),
}
}
}
impl core::hash::Hash for GraphicElement {

View File

@@ -297,12 +297,7 @@ impl GraphicElementRendered for ImageFrame<Color> {
if image.data.is_empty() {
return;
}
let (flat_data, _, _) = image.clone().into_flat_u8();
let mut output = Vec::new();
let encoder = image::codecs::png::PngEncoder::new(&mut output);
encoder
.write_image(&flat_data, image.width, image.height, image::ColorType::Rgba8)
.expect("failed to encode image as png");
let output = image.to_png();
let preamble = "data:image/png;base64,";
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
base64_string.push_str(preamble);

View File

@@ -134,6 +134,15 @@ impl Image<Color> {
let data = image_data.chunks_exact(4).map(|v| Color::from_rgba8_srgb(v[0], v[1], v[2], v[3])).collect();
Image { width, height, data }
}
pub fn to_png(&self) -> Vec<u8> {
use ::image::ImageEncoder;
let (data, width, height) = self.to_flat_u8();
let mut png = Vec::new();
let encoder = ::image::codecs::png::PngEncoder::new(&mut png);
encoder.write_image(&data, width, height, ::image::ColorType::Rgba8).expect("failed to encode image as png");
png
}
}
use super::*;
@@ -143,9 +152,9 @@ where
<P as Alpha>::AlphaChannel: Linear,
{
/// Flattens each channel cast to a u8
pub fn into_flat_u8(self) -> (Vec<u8>, u32, u32) {
pub fn to_flat_u8(&self) -> (Vec<u8>, u32, u32) {
let Image { width, height, data } = self;
assert_eq!(data.len(), width as usize * height as usize);
assert_eq!(data.len(), *width as usize * *height as usize);
// Cache the last sRGB value we computed, speeds up fills.
let mut last_r = 0.;
@@ -190,7 +199,7 @@ where
i += 4;
}
(result, width, height)
(result, *width, *height)
}
}

View File

@@ -22,6 +22,8 @@ quantization = ["autoquant"]
wasm = ["wasm-bindgen", "web-sys", "js-sys"]
imaginate = ["image/png", "base64", "js-sys", "web-sys", "wasm-bindgen-futures"]
image-compare = ["dep:image-compare"]
vello = ["dep:vello", "resvg", "gpu", "dep:vello_svg"]
resvg = ["dep:resvg"]
wayland = []
[dependencies]
@@ -72,6 +74,9 @@ winit = "0.28.6"
url = "2.4.0"
tokio = { version = "1.29.0", optional = true, features = ["fs", "io-std"] }
image-compare = { version = "0.3.0", optional = true }
vello = { git = "https://github.com/linebender/vello", version = "0.0.1", optional = true }
vello_svg = { git = "https://github.com/linebender/vello", version = "0.0.1", optional = true }
resvg = { version = "0.35.0", optional = true }
[dependencies.serde]
version = "1.0"

View File

@@ -315,6 +315,7 @@ async fn render_node<'a: 'input, F: Future<Output = GraphicGroup>>(
render.format_svg(min, max);
RenderOutput::Svg(render.svg.to_string())
}
#[cfg(any(feature = "resvg", feature = "vello"))]
ExportFormat::Canvas => {
data.render_svg(&mut render, &render_params);
// TODO: reenable once we switch to full node graph
@@ -326,7 +327,22 @@ async fn render_node<'a: 'input, F: Future<Output = GraphicGroup>>(
let canvas = &surface_handle.surface;
canvas.set_width(resolution.x);
canvas.set_height(resolution.y);
let usvg_tree = data.to_usvg_tree(resolution, [min, max]);
if let Some(exec) = editor.application_io.gpu_executor() {
todo!()
} else {
let rtree = resvg::Tree::from_usvg(&usvg_tree);
let pixmap_size = rtree.size.to_int_size();
let mut pixmap = resvg::tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap();
rtree.render(resvg::tiny_skia::Transform::default(), &mut pixmap.as_mut());
let array: Clamped<&[u8]> = Clamped(pixmap.data());
let context = canvas.get_context("2d").unwrap().unwrap().dyn_into::<CanvasRenderingContext2d>().unwrap();
let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(array, pixmap_size.width(), pixmap_size.height()).expect("Failed to construct ImageData");
context.put_image_data(&image_data, 0.0, 0.0).unwrap();
}
/*
let preamble = "data:image/svg+xml;base64,";
let mut base64_string = String::with_capacity(preamble.len() + array.len() * 4);
base64_string.push_str(preamble);
@@ -334,9 +350,9 @@ async fn render_node<'a: 'input, F: Future<Output = GraphicGroup>>(
let image_data = web_sys::HtmlImageElement::new().unwrap();
image_data.set_src(base64_string.as_str());
let context = canvas.get_context("2d").unwrap().unwrap().dyn_into::<CanvasRenderingContext2d>().unwrap();
wasm_bindgen_futures::JsFuture::from(image_data.decode()).await.unwrap();
context.draw_image_with_html_image_element(&image_data, 0.0, 0.0).unwrap();
*/
let frame = SurfaceHandleFrame {
surface_handle,
transform: DAffine2::IDENTITY,