mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
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:
committed by
Keavon Chambers
parent
34f2d61257
commit
e1cdb2242d
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user