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",
|
||||
"graphene-std",
|
||||
"graphite-proc-macros",
|
||||
"image",
|
||||
"interpreted-executor",
|
||||
"js-sys",
|
||||
"kurbo",
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,28 +225,81 @@ 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}");
|
||||
|
||||
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 });
|
||||
}
|
||||
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 {
|
||||
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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
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
|
||||
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),
|
||||
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
|
||||
|
|
|
|||
|
|
@ -222,10 +222,6 @@ pub trait GetEditorPreferences {
|
|||
pub enum ExportFormat {
|
||||
#[default]
|
||||
Svg,
|
||||
Png {
|
||||
transparent: bool,
|
||||
},
|
||||
Jpeg,
|
||||
Canvas,
|
||||
Texture,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 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 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
|
||||
|
|
|
|||
|
|
@ -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: &[],
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue