Skip to content

Commit 7e8c6cc

Browse files
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
1 parent c697b61 commit 7e8c6cc

File tree

8 files changed

+147
-48
lines changed

8 files changed

+147
-48
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editor/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ web-sys = { workspace = true }
4545
vello = { workspace = true }
4646
base64 = { workspace = true }
4747
spin = { workspace = true }
48+
image = { workspace = true }
4849

4950
# Optional local dependencies
5051
wgpu-executor = { workspace = true, optional = true }

editor/src/node_graph_executor.rs

Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,20 @@ impl NodeGraphExecutor {
190190
let size = bounds[1] - bounds[0];
191191
let transform = DAffine2::from_translation(bounds[0]).inverse();
192192

193+
let export_format = if export_config.file_type == FileType::Svg || cfg!(not(feature = "gpu")) {
194+
graphene_std::application_io::ExportFormat::Svg
195+
} else {
196+
graphene_std::application_io::ExportFormat::Texture
197+
};
198+
193199
let render_config = RenderConfig {
194200
viewport: Footprint {
195201
transform: DAffine2::from_scale(DVec2::splat(export_config.scale_factor)) * transform,
196202
resolution: (size * export_config.scale_factor).as_uvec2(),
197203
..Default::default()
198204
},
199205
time: Default::default(),
200-
export_format: graphene_std::application_io::ExportFormat::Svg,
206+
export_format,
201207
render_mode: document.render_mode,
202208
hide_artboards: export_config.transparent_background,
203209
for_export: true,
@@ -219,28 +225,81 @@ impl NodeGraphExecutor {
219225
}
220226

221227
fn export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, responses: &mut VecDeque<Message>) -> Result<(), String> {
222-
let TaggedValue::RenderOutput(RenderOutput {
223-
data: RenderOutputType::Svg { svg, .. },
228+
let ExportConfig {
229+
file_type,
230+
name,
231+
size,
232+
scale_factor,
233+
#[cfg(feature = "gpu")]
234+
transparent_background,
224235
..
225-
}) = node_graph_output
226-
else {
227-
return Err("Incorrect render type for exporting (expected RenderOutput::Svg)".to_string());
236+
} = export_config;
237+
238+
let file_extension = match file_type {
239+
FileType::Svg => "svg",
240+
FileType::Png => "png",
241+
FileType::Jpg => "jpg",
228242
};
243+
let name = format!("{name}.{file_extension}");
244+
245+
match node_graph_output {
246+
TaggedValue::RenderOutput(RenderOutput {
247+
data: RenderOutputType::Svg { svg, .. },
248+
..
249+
}) => {
250+
if file_type == FileType::Svg {
251+
responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() });
252+
} else {
253+
let mime = file_type.to_mime().to_string();
254+
let size = (size * scale_factor).into();
255+
responses.add(FrontendMessage::TriggerExportImage { svg, name, mime, size });
256+
}
257+
}
258+
#[cfg(feature = "gpu")]
259+
TaggedValue::RenderOutput(RenderOutput {
260+
data: RenderOutputType::Buffer { data, width, height },
261+
..
262+
}) if file_type != FileType::Svg => {
263+
use image::buffer::ConvertBuffer;
264+
use image::{ImageFormat, RgbImage, RgbaImage};
265+
266+
let Some(image) = RgbaImage::from_raw(width, height, data) else {
267+
return Err(format!("Failed to create image buffer for export"));
268+
};
229269

230-
let ExportConfig {
231-
file_type, name, size, scale_factor, ..
232-
} = export_config;
270+
let mut encoded = Vec::new();
271+
let mut cursor = std::io::Cursor::new(&mut encoded);
233272

234-
let file_suffix = &format!(".{file_type:?}").to_lowercase();
235-
let name = name + file_suffix;
273+
match file_type {
274+
FileType::Png => {
275+
let result = if transparent_background {
276+
image.write_to(&mut cursor, ImageFormat::Png)
277+
} else {
278+
let image: RgbImage = image.convert();
279+
image.write_to(&mut cursor, ImageFormat::Png)
280+
};
281+
if let Err(err) = result {
282+
return Err(format!("Failed to encode PNG: {err}"));
283+
}
284+
}
285+
FileType::Jpg => {
286+
let image: RgbImage = image.convert();
287+
let result = image.write_to(&mut cursor, ImageFormat::Jpeg);
288+
if let Err(err) = result {
289+
return Err(format!("Failed to encode JPG: {err}"));
290+
}
291+
}
292+
FileType::Svg => {
293+
return Err(format!("SVG cannot be exported from an image buffer"));
294+
}
295+
}
236296

237-
if file_type == FileType::Svg {
238-
responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() });
239-
} else {
240-
let mime = file_type.to_mime().to_string();
241-
let size = (size * scale_factor).into();
242-
responses.add(FrontendMessage::TriggerExportImage { svg, name, mime, size });
243-
}
297+
responses.add(FrontendMessage::TriggerSaveFile { name, content: encoded });
298+
}
299+
_ => {
300+
return Err(format!("Incorrect render type for exporting to an SVG ({file_type:?}, {node_graph_output})"));
301+
}
302+
};
244303

245304
Ok(())
246305
}

editor/src/node_graph_executor/runtime.rs

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ use graph_craft::graphene_compiler::Compiler;
77
use graph_craft::proto::GraphErrors;
88
use graph_craft::wasm_application_io::EditorPreferences;
99
use graph_craft::{ProtoNodeIdentifier, concrete};
10-
use graphene_std::application_io::{ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
10+
use graphene_std::application_io::{ApplicationIo, ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
1111
use graphene_std::bounds::RenderBoundingBox;
1212
use graphene_std::memo::IORecord;
13+
use graphene_std::ops::Convert;
14+
use graphene_std::raster_types::Raster;
1315
use graphene_std::renderer::{Render, RenderParams, SvgRender};
1416
use graphene_std::renderer::{RenderSvgSegmentList, SvgSegment};
1517
use graphene_std::table::{Table, TableRow};
@@ -220,16 +222,44 @@ impl NodeRuntime {
220222
// Resolve the result from the inspection by accessing the monitor node
221223
let inspect_result = self.inspect_state.and_then(|state| state.access(&self.executor));
222224

223-
let texture = if let Ok(TaggedValue::RenderOutput(RenderOutput {
224-
data: RenderOutputType::Texture(texture),
225-
..
226-
})) = &result
227-
{
228-
// We can early return becaus we know that there is at most one execution request and it will always be handled last
229-
Some(texture.clone())
230-
} else {
231-
None
225+
let (result, texture) = match result {
226+
Ok(TaggedValue::RenderOutput(RenderOutput {
227+
data: RenderOutputType::Texture(image_texture),
228+
metadata,
229+
})) if render_config.for_export => {
230+
let executor = self
231+
.editor_api
232+
.application_io
233+
.as_ref()
234+
.unwrap()
235+
.gpu_executor()
236+
.expect("GPU executor should be available when we receive a texture");
237+
238+
let raster_cpu = Raster::new_gpu(image_texture.texture).convert(Footprint::BOUNDLESS, executor).await;
239+
240+
let (data, width, height) = raster_cpu.to_flat_u8();
241+
242+
(
243+
Ok(TaggedValue::RenderOutput(RenderOutput {
244+
data: RenderOutputType::Buffer { data, width, height },
245+
metadata,
246+
})),
247+
None,
248+
)
249+
}
250+
Ok(TaggedValue::RenderOutput(RenderOutput {
251+
data: RenderOutputType::Texture(texture),
252+
metadata,
253+
})) => (
254+
Ok(TaggedValue::RenderOutput(RenderOutput {
255+
data: RenderOutputType::Texture(texture.clone()),
256+
metadata,
257+
})),
258+
Some(texture),
259+
),
260+
r => (r, None),
232261
};
262+
233263
self.sender.send_execution_response(ExecutionResponse {
234264
execution_id,
235265
result,
@@ -274,18 +304,12 @@ impl NodeRuntime {
274304
async fn execute_network(&mut self, render_config: RenderConfig) -> Result<TaggedValue, String> {
275305
use graph_craft::graphene_compiler::Executor;
276306

277-
let result = match self.executor.input_type() {
307+
match self.executor.input_type() {
278308
Some(t) if t == concrete!(RenderConfig) => (&self.executor).execute(render_config).await.map_err(|e| e.to_string()),
279309
Some(t) if t == concrete!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()),
280310
Some(t) => Err(format!("Invalid input type {t:?}")),
281311
_ => Err(format!("No input type:\n{:?}", self.node_graph_errors)),
282-
};
283-
let result = match result {
284-
Ok(value) => value,
285-
Err(e) => return Err(e),
286-
};
287-
288-
Ok(result)
312+
}
289313
}
290314

291315
/// Updates state data

node-graph/gapplication-io/src/lib.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,6 @@ pub trait GetEditorPreferences {
222222
pub enum ExportFormat {
223223
#[default]
224224
Svg,
225-
Png {
226-
transparent: bool,
227-
},
228-
Jpeg,
229225
Canvas,
230226
Texture,
231227
}

node-graph/graph-craft/src/document/value.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,11 +441,16 @@ pub enum RenderOutputType {
441441
CanvasFrame(SurfaceFrame),
442442
#[serde(skip)]
443443
Texture(ImageTexture),
444+
#[serde(skip)]
445+
Buffer {
446+
data: Vec<u8>,
447+
width: u32,
448+
height: u32,
449+
},
444450
Svg {
445451
svg: String,
446452
image_data: Vec<(u64, Image<Color>)>,
447453
},
448-
Image(Vec<u8>),
449454
}
450455

451456
impl Hash for RenderOutput {

node-graph/gstd/src/render_node.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
4444
data: impl Node<Context<'static>, Output = T>,
4545
editor_api: impl Node<Context<'static>, Output = &'a WasmEditorApi>,
4646
) -> RenderIntermediate {
47-
let mut render = SvgRender::new();
4847
let render_params = ctx
4948
.vararg(0)
5049
.expect("Did not find var args")
@@ -59,9 +58,20 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
5958
data.collect_metadata(&mut metadata, footprint, None);
6059
let contains_artboard = data.contains_artboard();
6160

62-
let editor_api = editor_api.eval(None).await;
61+
let use_vello = {
62+
#[cfg(target_family = "wasm")]
63+
{
64+
let editor_api = editor_api.eval(None).await;
65+
!render_params.for_export && editor_api.editor_preferences.use_vello() && matches!(render_params.render_output_type, graphene_svg_renderer::RenderOutputType::Vello)
66+
}
67+
#[cfg(not(target_family = "wasm"))]
68+
{
69+
let _ = editor_api;
70+
matches!(render_params.render_output_type, graphene_svg_renderer::RenderOutputType::Vello)
71+
}
72+
};
6373

64-
if !render_params.for_export && editor_api.editor_preferences.use_vello() && matches!(render_params.render_output_type, graphene_svg_renderer::RenderOutputType::Vello) {
74+
if use_vello {
6575
let mut scene = vello::Scene::new();
6676

6777
let mut context = wgpu_executor::RenderContext::default();
@@ -73,6 +83,8 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
7383
contains_artboard,
7484
}
7585
} else {
86+
let mut render = SvgRender::new();
87+
7688
data.render_svg(&mut render, render_params);
7789

7890
RenderIntermediate {
@@ -93,11 +105,10 @@ async fn create_context<'a: 'n>(
93105

94106
let render_output_type = match render_config.export_format {
95107
ExportFormat::Svg => RenderOutputTypeRequest::Svg,
96-
ExportFormat::Png { .. } => todo!(),
97-
ExportFormat::Jpeg => todo!(),
98-
ExportFormat::Canvas => RenderOutputTypeRequest::Vello,
99108
ExportFormat::Texture => RenderOutputTypeRequest::Vello,
109+
ExportFormat::Canvas => RenderOutputTypeRequest::Vello,
100110
};
111+
101112
let render_params = RenderParams {
102113
render_mode: render_config.render_mode,
103114
hide_artboards: render_config.hide_artboards,
@@ -106,6 +117,7 @@ async fn create_context<'a: 'n>(
106117
footprint: Footprint::default(),
107118
..Default::default()
108119
};
120+
109121
let ctx = OwnedContextImpl::default()
110122
.with_footprint(footprint)
111123
.with_real_time(render_config.time.time)
@@ -198,6 +210,7 @@ async fn render<'a: 'n>(
198210
if !contains_artboard && !render_params.hide_artboards {
199211
background = Color::WHITE;
200212
}
213+
201214
if let Some(surface_handle) = surface_handle {
202215
exec.render_vello_scene(&scene, &surface_handle, footprint.resolution, context, background)
203216
.await

node-graph/wgpu-executor/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ impl WgpuExecutor {
124124
mip_level_count: 1,
125125
sample_count: 1,
126126
dimension: wgpu::TextureDimension::D2,
127-
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
127+
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
128128
format: VELLO_SURFACE_FORMAT,
129129
view_formats: &[],
130130
});

0 commit comments

Comments
 (0)