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:
Timon 2025-10-01 21:13:40 +02:00 committed by GitHub
parent c697b61a7e
commit 7e8c6cc432
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 148 additions and 49 deletions

1
Cargo.lock generated
View File

@ -2310,6 +2310,7 @@ dependencies = [
"graph-craft",
"graphene-std",
"graphite-proc-macros",
"image",
"interpreted-executor",
"js-sys",
"kurbo",

View File

@ -45,6 +45,7 @@ web-sys = { workspace = true }
vello = { workspace = true }
base64 = { workspace = true }
spin = { workspace = true }
image = { workspace = true }
# Optional local dependencies
wgpu-executor = { workspace = true, optional = true }

View File

@ -190,6 +190,12 @@ impl NodeGraphExecutor {
let size = bounds[1] - bounds[0];
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 {
viewport: Footprint {
transform: DAffine2::from_scale(DVec2::splat(export_config.scale_factor)) * transform,
@ -197,7 +203,7 @@ impl NodeGraphExecutor {
..Default::default()
},
time: Default::default(),
export_format: graphene_std::application_io::ExportFormat::Svg,
export_format,
render_mode: document.render_mode,
hide_artboards: export_config.transparent_background,
for_export: true,
@ -219,21 +225,28 @@ impl NodeGraphExecutor {
}
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 {
file_type, name, size, scale_factor, ..
file_type,
name,
size,
scale_factor,
#[cfg(feature = "gpu")]
transparent_background,
..
} = export_config;
let file_suffix = &format!(".{file_type:?}").to_lowercase();
let name = name + file_suffix;
let file_extension = match file_type {
FileType::Svg => "svg",
FileType::Png => "png",
FileType::Jpg => "jpg",
};
let name = format!("{name}.{file_extension}");
match node_graph_output {
TaggedValue::RenderOutput(RenderOutput {
data: RenderOutputType::Svg { svg, .. },
..
}) => {
if file_type == FileType::Svg {
responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() });
} else {
@ -241,6 +254,52 @@ impl NodeGraphExecutor {
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(())
}

View File

@ -7,9 +7,11 @@ use graph_craft::graphene_compiler::Compiler;
use graph_craft::proto::GraphErrors;
use graph_craft::wasm_application_io::EditorPreferences;
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::memo::IORecord;
use graphene_std::ops::Convert;
use graphene_std::raster_types::Raster;
use graphene_std::renderer::{Render, RenderParams, SvgRender};
use graphene_std::renderer::{RenderSvgSegmentList, SvgSegment};
use graphene_std::table::{Table, TableRow};
@ -220,16 +222,44 @@ impl NodeRuntime {
// 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 texture = if let Ok(TaggedValue::RenderOutput(RenderOutput {
let (result, texture) = match result {
Ok(TaggedValue::RenderOutput(RenderOutput {
data: RenderOutputType::Texture(image_texture),
metadata,
})) if render_config.for_export => {
let executor = self
.editor_api
.application_io
.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),
..
})) = &result
{
// We can early return becaus we know that there is at most one execution request and it will always be handled last
Some(texture.clone())
} else {
None
metadata,
})) => (
Ok(TaggedValue::RenderOutput(RenderOutput {
data: RenderOutputType::Texture(texture.clone()),
metadata,
})),
Some(texture),
),
r => (r, None),
};
self.sender.send_execution_response(ExecutionResponse {
execution_id,
result,
@ -274,18 +304,12 @@ impl NodeRuntime {
async fn execute_network(&mut self, render_config: RenderConfig) -> Result<TaggedValue, String> {
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!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()),
Some(t) => Err(format!("Invalid input type {t:?}")),
_ => 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

View File

@ -222,10 +222,6 @@ pub trait GetEditorPreferences {
pub enum ExportFormat {
#[default]
Svg,
Png {
transparent: bool,
},
Jpeg,
Canvas,
Texture,
}

View File

@ -441,11 +441,16 @@ pub enum RenderOutputType {
CanvasFrame(SurfaceFrame),
#[serde(skip)]
Texture(ImageTexture),
#[serde(skip)]
Buffer {
data: Vec<u8>,
width: u32,
height: u32,
},
Svg {
svg: String,
image_data: Vec<(u64, Image<Color>)>,
},
Image(Vec<u8>),
}
impl Hash for RenderOutput {

View File

@ -44,7 +44,6 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
data: impl Node<Context<'static>, Output = T>,
editor_api: impl Node<Context<'static>, Output = &'a WasmEditorApi>,
) -> RenderIntermediate {
let mut render = SvgRender::new();
let render_params = ctx
.vararg(0)
.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);
let contains_artboard = data.contains_artboard();
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 context = wgpu_executor::RenderContext::default();
@ -73,6 +83,8 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
contains_artboard,
}
} else {
let mut render = SvgRender::new();
data.render_svg(&mut render, render_params);
RenderIntermediate {
@ -93,11 +105,10 @@ async fn create_context<'a: 'n>(
let render_output_type = match render_config.export_format {
ExportFormat::Svg => RenderOutputTypeRequest::Svg,
ExportFormat::Png { .. } => todo!(),
ExportFormat::Jpeg => todo!(),
ExportFormat::Canvas => RenderOutputTypeRequest::Vello,
ExportFormat::Texture => RenderOutputTypeRequest::Vello,
ExportFormat::Canvas => RenderOutputTypeRequest::Vello,
};
let render_params = RenderParams {
render_mode: render_config.render_mode,
hide_artboards: render_config.hide_artboards,
@ -106,6 +117,7 @@ async fn create_context<'a: 'n>(
footprint: Footprint::default(),
..Default::default()
};
let ctx = OwnedContextImpl::default()
.with_footprint(footprint)
.with_real_time(render_config.time.time)
@ -198,6 +210,7 @@ async fn render<'a: 'n>(
if !contains_artboard && !render_params.hide_artboards {
background = Color::WHITE;
}
if let Some(surface_handle) = surface_handle {
exec.render_vello_scene(&scene, &surface_handle, footprint.resolution, context, background)
.await

View File

@ -124,7 +124,7 @@ impl WgpuExecutor {
mip_level_count: 1,
sample_count: 1,
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,
view_formats: &[],
});