Desktop: Native export (#3188)
* Testing native export with raster convert implementation * Jpg export * Fix transparent export * move texture conversion to runtime * fixup * move image encoding into editor export function * remove unused frontend message * remove unused type
This commit is contained in:
parent
c697b61a7e
commit
7e8c6cc432
|
|
@ -2310,6 +2310,7 @@ dependencies = [
|
||||||
"graph-craft",
|
"graph-craft",
|
||||||
"graphene-std",
|
"graphene-std",
|
||||||
"graphite-proc-macros",
|
"graphite-proc-macros",
|
||||||
|
"image",
|
||||||
"interpreted-executor",
|
"interpreted-executor",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"kurbo",
|
"kurbo",
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ web-sys = { workspace = true }
|
||||||
vello = { workspace = true }
|
vello = { workspace = true }
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
spin = { workspace = true }
|
spin = { workspace = true }
|
||||||
|
image = { workspace = true }
|
||||||
|
|
||||||
# Optional local dependencies
|
# Optional local dependencies
|
||||||
wgpu-executor = { workspace = true, optional = true }
|
wgpu-executor = { workspace = true, optional = true }
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,12 @@ impl NodeGraphExecutor {
|
||||||
let size = bounds[1] - bounds[0];
|
let size = bounds[1] - bounds[0];
|
||||||
let transform = DAffine2::from_translation(bounds[0]).inverse();
|
let transform = DAffine2::from_translation(bounds[0]).inverse();
|
||||||
|
|
||||||
|
let export_format = if export_config.file_type == FileType::Svg || cfg!(not(feature = "gpu")) {
|
||||||
|
graphene_std::application_io::ExportFormat::Svg
|
||||||
|
} else {
|
||||||
|
graphene_std::application_io::ExportFormat::Texture
|
||||||
|
};
|
||||||
|
|
||||||
let render_config = RenderConfig {
|
let render_config = RenderConfig {
|
||||||
viewport: Footprint {
|
viewport: Footprint {
|
||||||
transform: DAffine2::from_scale(DVec2::splat(export_config.scale_factor)) * transform,
|
transform: DAffine2::from_scale(DVec2::splat(export_config.scale_factor)) * transform,
|
||||||
|
|
@ -197,7 +203,7 @@ impl NodeGraphExecutor {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
time: Default::default(),
|
time: Default::default(),
|
||||||
export_format: graphene_std::application_io::ExportFormat::Svg,
|
export_format,
|
||||||
render_mode: document.render_mode,
|
render_mode: document.render_mode,
|
||||||
hide_artboards: export_config.transparent_background,
|
hide_artboards: export_config.transparent_background,
|
||||||
for_export: true,
|
for_export: true,
|
||||||
|
|
@ -219,28 +225,81 @@ impl NodeGraphExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
fn export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
||||||
let TaggedValue::RenderOutput(RenderOutput {
|
|
||||||
data: RenderOutputType::Svg { svg, .. },
|
|
||||||
..
|
|
||||||
}) = node_graph_output
|
|
||||||
else {
|
|
||||||
return Err("Incorrect render type for exporting (expected RenderOutput::Svg)".to_string());
|
|
||||||
};
|
|
||||||
|
|
||||||
let ExportConfig {
|
let ExportConfig {
|
||||||
file_type, name, size, scale_factor, ..
|
file_type,
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
scale_factor,
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
transparent_background,
|
||||||
|
..
|
||||||
} = export_config;
|
} = export_config;
|
||||||
|
|
||||||
let file_suffix = &format!(".{file_type:?}").to_lowercase();
|
let file_extension = match file_type {
|
||||||
let name = name + file_suffix;
|
FileType::Svg => "svg",
|
||||||
|
FileType::Png => "png",
|
||||||
|
FileType::Jpg => "jpg",
|
||||||
|
};
|
||||||
|
let name = format!("{name}.{file_extension}");
|
||||||
|
|
||||||
if file_type == FileType::Svg {
|
match node_graph_output {
|
||||||
responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() });
|
TaggedValue::RenderOutput(RenderOutput {
|
||||||
} else {
|
data: RenderOutputType::Svg { svg, .. },
|
||||||
let mime = file_type.to_mime().to_string();
|
..
|
||||||
let size = (size * scale_factor).into();
|
}) => {
|
||||||
responses.add(FrontendMessage::TriggerExportImage { svg, name, mime, size });
|
if file_type == FileType::Svg {
|
||||||
}
|
responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() });
|
||||||
|
} else {
|
||||||
|
let mime = file_type.to_mime().to_string();
|
||||||
|
let size = (size * scale_factor).into();
|
||||||
|
responses.add(FrontendMessage::TriggerExportImage { svg, name, mime, size });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
TaggedValue::RenderOutput(RenderOutput {
|
||||||
|
data: RenderOutputType::Buffer { data, width, height },
|
||||||
|
..
|
||||||
|
}) if file_type != FileType::Svg => {
|
||||||
|
use image::buffer::ConvertBuffer;
|
||||||
|
use image::{ImageFormat, RgbImage, RgbaImage};
|
||||||
|
|
||||||
|
let Some(image) = RgbaImage::from_raw(width, height, data) else {
|
||||||
|
return Err(format!("Failed to create image buffer for export"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut encoded = Vec::new();
|
||||||
|
let mut cursor = std::io::Cursor::new(&mut encoded);
|
||||||
|
|
||||||
|
match file_type {
|
||||||
|
FileType::Png => {
|
||||||
|
let result = if transparent_background {
|
||||||
|
image.write_to(&mut cursor, ImageFormat::Png)
|
||||||
|
} else {
|
||||||
|
let image: RgbImage = image.convert();
|
||||||
|
image.write_to(&mut cursor, ImageFormat::Png)
|
||||||
|
};
|
||||||
|
if let Err(err) = result {
|
||||||
|
return Err(format!("Failed to encode PNG: {err}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileType::Jpg => {
|
||||||
|
let image: RgbImage = image.convert();
|
||||||
|
let result = image.write_to(&mut cursor, ImageFormat::Jpeg);
|
||||||
|
if let Err(err) = result {
|
||||||
|
return Err(format!("Failed to encode JPG: {err}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileType::Svg => {
|
||||||
|
return Err(format!("SVG cannot be exported from an image buffer"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responses.add(FrontendMessage::TriggerSaveFile { name, content: encoded });
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(format!("Incorrect render type for exporting to an SVG ({file_type:?}, {node_graph_output})"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@ use graph_craft::graphene_compiler::Compiler;
|
||||||
use graph_craft::proto::GraphErrors;
|
use graph_craft::proto::GraphErrors;
|
||||||
use graph_craft::wasm_application_io::EditorPreferences;
|
use graph_craft::wasm_application_io::EditorPreferences;
|
||||||
use graph_craft::{ProtoNodeIdentifier, concrete};
|
use graph_craft::{ProtoNodeIdentifier, concrete};
|
||||||
use graphene_std::application_io::{ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
|
use graphene_std::application_io::{ApplicationIo, ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
|
||||||
use graphene_std::bounds::RenderBoundingBox;
|
use graphene_std::bounds::RenderBoundingBox;
|
||||||
use graphene_std::memo::IORecord;
|
use graphene_std::memo::IORecord;
|
||||||
|
use graphene_std::ops::Convert;
|
||||||
|
use graphene_std::raster_types::Raster;
|
||||||
use graphene_std::renderer::{Render, RenderParams, SvgRender};
|
use graphene_std::renderer::{Render, RenderParams, SvgRender};
|
||||||
use graphene_std::renderer::{RenderSvgSegmentList, SvgSegment};
|
use graphene_std::renderer::{RenderSvgSegmentList, SvgSegment};
|
||||||
use graphene_std::table::{Table, TableRow};
|
use graphene_std::table::{Table, TableRow};
|
||||||
|
|
@ -220,16 +222,44 @@ impl NodeRuntime {
|
||||||
// Resolve the result from the inspection by accessing the monitor node
|
// Resolve the result from the inspection by accessing the monitor node
|
||||||
let inspect_result = self.inspect_state.and_then(|state| state.access(&self.executor));
|
let inspect_result = self.inspect_state.and_then(|state| state.access(&self.executor));
|
||||||
|
|
||||||
let texture = if let Ok(TaggedValue::RenderOutput(RenderOutput {
|
let (result, texture) = match result {
|
||||||
data: RenderOutputType::Texture(texture),
|
Ok(TaggedValue::RenderOutput(RenderOutput {
|
||||||
..
|
data: RenderOutputType::Texture(image_texture),
|
||||||
})) = &result
|
metadata,
|
||||||
{
|
})) if render_config.for_export => {
|
||||||
// We can early return becaus we know that there is at most one execution request and it will always be handled last
|
let executor = self
|
||||||
Some(texture.clone())
|
.editor_api
|
||||||
} else {
|
.application_io
|
||||||
None
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.gpu_executor()
|
||||||
|
.expect("GPU executor should be available when we receive a texture");
|
||||||
|
|
||||||
|
let raster_cpu = Raster::new_gpu(image_texture.texture).convert(Footprint::BOUNDLESS, executor).await;
|
||||||
|
|
||||||
|
let (data, width, height) = raster_cpu.to_flat_u8();
|
||||||
|
|
||||||
|
(
|
||||||
|
Ok(TaggedValue::RenderOutput(RenderOutput {
|
||||||
|
data: RenderOutputType::Buffer { data, width, height },
|
||||||
|
metadata,
|
||||||
|
})),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Ok(TaggedValue::RenderOutput(RenderOutput {
|
||||||
|
data: RenderOutputType::Texture(texture),
|
||||||
|
metadata,
|
||||||
|
})) => (
|
||||||
|
Ok(TaggedValue::RenderOutput(RenderOutput {
|
||||||
|
data: RenderOutputType::Texture(texture.clone()),
|
||||||
|
metadata,
|
||||||
|
})),
|
||||||
|
Some(texture),
|
||||||
|
),
|
||||||
|
r => (r, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.sender.send_execution_response(ExecutionResponse {
|
self.sender.send_execution_response(ExecutionResponse {
|
||||||
execution_id,
|
execution_id,
|
||||||
result,
|
result,
|
||||||
|
|
@ -274,18 +304,12 @@ impl NodeRuntime {
|
||||||
async fn execute_network(&mut self, render_config: RenderConfig) -> Result<TaggedValue, String> {
|
async fn execute_network(&mut self, render_config: RenderConfig) -> Result<TaggedValue, String> {
|
||||||
use graph_craft::graphene_compiler::Executor;
|
use graph_craft::graphene_compiler::Executor;
|
||||||
|
|
||||||
let result = match self.executor.input_type() {
|
match self.executor.input_type() {
|
||||||
Some(t) if t == concrete!(RenderConfig) => (&self.executor).execute(render_config).await.map_err(|e| e.to_string()),
|
Some(t) if t == concrete!(RenderConfig) => (&self.executor).execute(render_config).await.map_err(|e| e.to_string()),
|
||||||
Some(t) if t == concrete!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()),
|
Some(t) if t == concrete!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()),
|
||||||
Some(t) => Err(format!("Invalid input type {t:?}")),
|
Some(t) => Err(format!("Invalid input type {t:?}")),
|
||||||
_ => Err(format!("No input type:\n{:?}", self.node_graph_errors)),
|
_ => Err(format!("No input type:\n{:?}", self.node_graph_errors)),
|
||||||
};
|
}
|
||||||
let result = match result {
|
|
||||||
Ok(value) => value,
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates state data
|
/// Updates state data
|
||||||
|
|
|
||||||
|
|
@ -222,10 +222,6 @@ pub trait GetEditorPreferences {
|
||||||
pub enum ExportFormat {
|
pub enum ExportFormat {
|
||||||
#[default]
|
#[default]
|
||||||
Svg,
|
Svg,
|
||||||
Png {
|
|
||||||
transparent: bool,
|
|
||||||
},
|
|
||||||
Jpeg,
|
|
||||||
Canvas,
|
Canvas,
|
||||||
Texture,
|
Texture,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -441,11 +441,16 @@ pub enum RenderOutputType {
|
||||||
CanvasFrame(SurfaceFrame),
|
CanvasFrame(SurfaceFrame),
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
Texture(ImageTexture),
|
Texture(ImageTexture),
|
||||||
|
#[serde(skip)]
|
||||||
|
Buffer {
|
||||||
|
data: Vec<u8>,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
},
|
||||||
Svg {
|
Svg {
|
||||||
svg: String,
|
svg: String,
|
||||||
image_data: Vec<(u64, Image<Color>)>,
|
image_data: Vec<(u64, Image<Color>)>,
|
||||||
},
|
},
|
||||||
Image(Vec<u8>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hash for RenderOutput {
|
impl Hash for RenderOutput {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
|
||||||
data: impl Node<Context<'static>, Output = T>,
|
data: impl Node<Context<'static>, Output = T>,
|
||||||
editor_api: impl Node<Context<'static>, Output = &'a WasmEditorApi>,
|
editor_api: impl Node<Context<'static>, Output = &'a WasmEditorApi>,
|
||||||
) -> RenderIntermediate {
|
) -> RenderIntermediate {
|
||||||
let mut render = SvgRender::new();
|
|
||||||
let render_params = ctx
|
let render_params = ctx
|
||||||
.vararg(0)
|
.vararg(0)
|
||||||
.expect("Did not find var args")
|
.expect("Did not find var args")
|
||||||
|
|
@ -59,9 +58,20 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
|
||||||
data.collect_metadata(&mut metadata, footprint, None);
|
data.collect_metadata(&mut metadata, footprint, None);
|
||||||
let contains_artboard = data.contains_artboard();
|
let contains_artboard = data.contains_artboard();
|
||||||
|
|
||||||
let editor_api = editor_api.eval(None).await;
|
let use_vello = {
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
{
|
||||||
|
let editor_api = editor_api.eval(None).await;
|
||||||
|
!render_params.for_export && editor_api.editor_preferences.use_vello() && matches!(render_params.render_output_type, graphene_svg_renderer::RenderOutputType::Vello)
|
||||||
|
}
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
{
|
||||||
|
let _ = editor_api;
|
||||||
|
matches!(render_params.render_output_type, graphene_svg_renderer::RenderOutputType::Vello)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if !render_params.for_export && editor_api.editor_preferences.use_vello() && matches!(render_params.render_output_type, graphene_svg_renderer::RenderOutputType::Vello) {
|
if use_vello {
|
||||||
let mut scene = vello::Scene::new();
|
let mut scene = vello::Scene::new();
|
||||||
|
|
||||||
let mut context = wgpu_executor::RenderContext::default();
|
let mut context = wgpu_executor::RenderContext::default();
|
||||||
|
|
@ -73,6 +83,8 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
|
||||||
contains_artboard,
|
contains_artboard,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
let mut render = SvgRender::new();
|
||||||
|
|
||||||
data.render_svg(&mut render, render_params);
|
data.render_svg(&mut render, render_params);
|
||||||
|
|
||||||
RenderIntermediate {
|
RenderIntermediate {
|
||||||
|
|
@ -93,11 +105,10 @@ async fn create_context<'a: 'n>(
|
||||||
|
|
||||||
let render_output_type = match render_config.export_format {
|
let render_output_type = match render_config.export_format {
|
||||||
ExportFormat::Svg => RenderOutputTypeRequest::Svg,
|
ExportFormat::Svg => RenderOutputTypeRequest::Svg,
|
||||||
ExportFormat::Png { .. } => todo!(),
|
|
||||||
ExportFormat::Jpeg => todo!(),
|
|
||||||
ExportFormat::Canvas => RenderOutputTypeRequest::Vello,
|
|
||||||
ExportFormat::Texture => RenderOutputTypeRequest::Vello,
|
ExportFormat::Texture => RenderOutputTypeRequest::Vello,
|
||||||
|
ExportFormat::Canvas => RenderOutputTypeRequest::Vello,
|
||||||
};
|
};
|
||||||
|
|
||||||
let render_params = RenderParams {
|
let render_params = RenderParams {
|
||||||
render_mode: render_config.render_mode,
|
render_mode: render_config.render_mode,
|
||||||
hide_artboards: render_config.hide_artboards,
|
hide_artboards: render_config.hide_artboards,
|
||||||
|
|
@ -106,6 +117,7 @@ async fn create_context<'a: 'n>(
|
||||||
footprint: Footprint::default(),
|
footprint: Footprint::default(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let ctx = OwnedContextImpl::default()
|
let ctx = OwnedContextImpl::default()
|
||||||
.with_footprint(footprint)
|
.with_footprint(footprint)
|
||||||
.with_real_time(render_config.time.time)
|
.with_real_time(render_config.time.time)
|
||||||
|
|
@ -198,6 +210,7 @@ async fn render<'a: 'n>(
|
||||||
if !contains_artboard && !render_params.hide_artboards {
|
if !contains_artboard && !render_params.hide_artboards {
|
||||||
background = Color::WHITE;
|
background = Color::WHITE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(surface_handle) = surface_handle {
|
if let Some(surface_handle) = surface_handle {
|
||||||
exec.render_vello_scene(&scene, &surface_handle, footprint.resolution, context, background)
|
exec.render_vello_scene(&scene, &surface_handle, footprint.resolution, context, background)
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ impl WgpuExecutor {
|
||||||
mip_level_count: 1,
|
mip_level_count: 1,
|
||||||
sample_count: 1,
|
sample_count: 1,
|
||||||
dimension: wgpu::TextureDimension::D2,
|
dimension: wgpu::TextureDimension::D2,
|
||||||
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
|
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
|
||||||
format: VELLO_SURFACE_FORMAT,
|
format: VELLO_SURFACE_FORMAT,
|
||||||
view_formats: &[],
|
view_formats: &[],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue