From 02134b2b5d713d1e4ab2986934f8007043883428 Mon Sep 17 00:00:00 2001 From: Kulratan Date: Tue, 9 Jun 2026 19:17:48 +0000 Subject: [PATCH] SVG embedded image import support --- Cargo.lock | 17 +++++++++ Cargo.toml | 1 + .../graph_operation_message_handler.rs | 38 ++++++++++++++++--- .../document/graph_operation/utility_types.rs | 22 +++++++---- .../resource/resource_message_handler.rs | 4 +- editor/src/node_graph_executor.rs | 2 +- 6 files changed, 68 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f3878680c1..0a07005a0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2698,12 +2698,23 @@ dependencies = [ "byteorder-lite", "color_quant", "gif", + "image-webp", "num-traits", "png 0.17.16", "zune-core", "zune-jpeg", ] +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + [[package]] name = "imagesize" version = "0.14.0" @@ -4256,6 +4267,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.37.5" diff --git a/Cargo.toml b/Cargo.toml index 0297028792..0c65285e0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -180,6 +180,7 @@ image = { version = "0.25", default-features = false, features = [ "jpeg", "bmp", "gif", + "webp", ] } pretty_assertions = "1.4" fern = { version = "0.7", features = ["colored"] } diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 0ad37b4dfb..eaf5f42e13 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -467,7 +467,8 @@ impl MessageHandler> for // After import, `layer_node` is set to the root group. Apply the placement transform to it // (skipped automatically when identity, so file-open with content at origin creates no Transform node). - modify_inputs.transform_set(placement_transform, TransformIn::Local, false); + modify_inputs.transform_set(placement_transform, TransformIn::Local, true); + responses.add(NodeGraphMessage::RunDocumentGraph); } } } @@ -617,8 +618,8 @@ fn import_usvg_node( usvg::Node::Path(path) => { import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops); } - usvg::Node::Image(_image) => { - warn!("Skip image"); + usvg::Node::Image(image) => { + import_usvg_image(modify_inputs, node, image, layer); } usvg::Node::Text(text) => { let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_std::consts::DEFAULT_FONT_STYLE.to_string()); @@ -668,8 +669,8 @@ fn import_usvg_node_inner( import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops); 0 } - usvg::Node::Image(_image) => { - warn!("Skip image"); + usvg::Node::Image(image) => { + import_usvg_image(modify_inputs, node, image, layer); 0 } usvg::Node::Text(text) => { @@ -865,3 +866,30 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b } }); } + +fn import_usvg_image(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, image: &usvg::Image, layer: LayerNodeIdentifier) { + let image_data = match image.kind() { + usvg::ImageKind::JPEG(data) => data.as_slice(), + usvg::ImageKind::PNG(data) => data.as_slice(), + usvg::ImageKind::GIF(data) => data.as_slice(), + usvg::ImageKind::WEBP(data) => data.as_slice(), + // TODO: Nested SVG-in-SVG (usvg::ImageKind::SVG) is currently unsupported. + // Also non-default preserveAspectRatio slicing/clipping are not reproduced. + _ => { + log::warn!("Unsupported SVG image format"); + return; + } + }; + + let width = image.size().width(); + let height = image.size().height(); + let transform_node_id = modify_inputs.insert_encoded_image_data(image_data.into(), layer); + + let node_transform = usvg_transform(node.abs_transform()); + let pixel_size = DVec2::new(width as f64, height as f64); + let final_transform = node_transform * DAffine2::from_scale(pixel_size); + + if final_transform != DAffine2::IDENTITY { + transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, final_transform); + } +} diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 84774cf0f2..4be27d1b08 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -300,17 +300,21 @@ impl<'a> ModifyInputsContext<'a> { self.network_interface.move_node_to_chain_start(&color_value_id, layer, &[], self.import); } - pub fn insert_image_data(&mut self, image: Image, layer: LayerNodeIdentifier) { - let transform = resolve_proto_node_type(graphene_std::transform_nodes::transform::IDENTIFIER) - .expect("Transform node does not exist") - .default_node_template(); + pub fn insert_image_data(&mut self, image: Image, layer: LayerNodeIdentifier) -> NodeId { + self.insert_encoded_image_data(image.to_png().into(), layer) + } + pub fn insert_encoded_image_data(&mut self, data: std::sync::Arc<[u8]>, layer: LayerNodeIdentifier) -> NodeId { let resource_id = ResourceId::new(); - self.responses.add(ResourceMessage::StoreEmbedded { - resource_id, - data: image.to_png().into(), - }); + // Store before any RunDocumentGraph so the image node can resolve the resource on first render + self.responses.add_front(ResourceMessage::StoreEmbedded { resource_id, data }); + self.insert_image_resource(resource_id, layer) + } + fn insert_image_resource(&mut self, resource_id: ResourceId, layer: LayerNodeIdentifier) -> NodeId { + let transform = resolve_proto_node_type(graphene_std::transform_nodes::transform::IDENTIFIER) + .expect("Transform node does not exist") + .default_node_template(); let image_node = resolve_proto_node_type(graphene_std::raster_nodes::std_nodes::image::IDENTIFIER) .expect("Image node does not exist") .node_template_input_override([Some(NodeInput::value(TaggedValue::Resource(resource_id), false))]); @@ -322,6 +326,8 @@ impl<'a> ModifyInputsContext<'a> { let transform_id = NodeId::new(); self.network_interface.insert_node(transform_id, transform, &[]); self.network_interface.move_node_to_chain_start(&transform_id, layer, &[], self.import); + + transform_id } fn get_output_layer(&self) -> Option { diff --git a/editor/src/messages/portfolio/document/resource/resource_message_handler.rs b/editor/src/messages/portfolio/document/resource/resource_message_handler.rs index 7a45060149..bdf2fe2436 100644 --- a/editor/src/messages/portfolio/document/resource/resource_message_handler.rs +++ b/editor/src/messages/portfolio/document/resource/resource_message_handler.rs @@ -32,7 +32,8 @@ impl MessageHandler> for ResourceMes let hash = ResourceHash::from(data.as_ref()); self.registry.push_source_back(&resource_id, DataSource::Embedded); self.registry.resolve(&resource_id, hash); - responses.add(ResourceStorageMessage::Store { data }); + // Store before any RunDocumentGraph so the image node can resolve the resource on first render + responses.add_front(ResourceStorageMessage::Store { data }); } ResourceMessage::AddFont { resource_id, font } => { let style = fonts.font_catalog.find_font_style_in_catalog(&font); @@ -69,7 +70,6 @@ impl MessageHandler> for ResourceMes log::warn!("Resource {resource_id} already resolved"); return; } - self.pending_resolves.insert(resource_id); let font_catalog = fonts.font_catalog.clone(); diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index ae089b9041..4d0e860c4f 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -416,7 +416,7 @@ impl NodeGraphExecutor { graphene_std::raster::Image { width: image.width, height: image.height, - data: image.data.iter().map(|&c| SRGBA8::from(c)).collect(), + data: image.data.iter().map(|&c| SRGBA8::from(c.to_unassociated_alpha())).collect(), base64_string: image.base64_string, }, )