diff --git a/api/cpp/cbindgen.rs b/api/cpp/cbindgen.rs index f8c6463eaab..9ffd2f69181 100644 --- a/api/cpp/cbindgen.rs +++ b/api/cpp/cbindgen.rs @@ -485,6 +485,7 @@ fn gen_corelib( "slint_image_size", "slint_image_path", "slint_image_load_from_path", + "slint_image_load_from_data_url", "slint_image_load_from_embedded_data", "slint_image_from_embedded_textures", "slint_image_compare_equal", diff --git a/api/cpp/include/slint_image.h b/api/cpp/include/slint_image.h index 56b61d580a9..3f03d6638a2 100644 --- a/api/cpp/include/slint_image.h +++ b/api/cpp/include/slint_image.h @@ -130,6 +130,14 @@ struct Image cbindgen_private::types::slint_image_load_from_path(&file_path, &img.data); return img; } + + /// Load an image from data url + [[nodiscard]] static Image load_from_data_url(const SharedString &data_url) + { + Image img; + cbindgen_private::types::slint_image_load_from_data_url(&data_url, &img.data); + return img; + } #endif /// Constructs a new Image from an existing OpenGL texture. The texture remains borrowed by diff --git a/internal/compiler/Cargo.toml b/internal/compiler/Cargo.toml index bcaf258efd7..f6b81f48bf1 100644 --- a/internal/compiler/Cargo.toml +++ b/internal/compiler/Cargo.toml @@ -56,6 +56,7 @@ itertools = { workspace = true } url = "2.2.1" linked_hash_set = "0.1.4" typed-index-collections = "3.2" +dataurl = "0.1.2" # for processing and embedding the rendered image (texture) image = { workspace = true, optional = true, features = ["default"] } diff --git a/internal/compiler/embedded_resources.rs b/internal/compiler/embedded_resources.rs index f7ded8d0555..7afe0666ba7 100644 --- a/internal/compiler/embedded_resources.rs +++ b/internal/compiler/embedded_resources.rs @@ -95,6 +95,8 @@ pub enum EmbeddedResourcesKind { ListOnly, /// Just put the file content as a resource RawData, + /// Decoded data from a data URL (data, extension) + DecodedData(Vec, String), /// The data has been processed in a texture #[cfg(feature = "software-renderer")] TextureData(Texture), diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs index 62b9954f862..c0cf3684d96 100644 --- a/internal/compiler/generator/cpp.rs +++ b/internal/compiler/generator/cpp.rs @@ -933,7 +933,7 @@ fn embed_resource( init.push(','); } write!(&mut init, "0x{byte:x}").unwrap(); - if index % 16 == 0 { + if index > 0 && index % 16 == 0 { init.push('\n'); } } @@ -1136,6 +1136,29 @@ fn embed_resource( ..Default::default() })) } + crate::embedded_resources::EmbeddedResourcesKind::DecodedData(data, _) => { + let mut init = "{ ".to_string(); + + for (index, byte) in data.iter().enumerate() { + if index > 0 { + init.push(','); + } + write!(&mut init, "0x{byte:x}").unwrap(); + if index > 0 && index % 16 == 0 { + init.push('\n'); + } + } + + init.push('}'); + + declarations.push(Declaration::Var(Var { + ty: "const uint8_t".into(), + name: format_smolstr!("slint_embedded_resource_{}", resource.id), + array_size: Some(data.len()), + init: Some(init), + ..Default::default() + })); + } } } @@ -3342,7 +3365,13 @@ fn compile_expression(expr: &llr::Expression, ctx: &EvaluationContext) -> String Expression::ImageReference { resource_ref, nine_slice } => { let image = match resource_ref { crate::expression_tree::ImageReference::None => r#"slint::Image()"#.to_string(), - crate::expression_tree::ImageReference::AbsolutePath(path) => format!(r#"slint::Image::load_from_path(slint::SharedString(u8"{}"))"#, escape_string(path.as_str())), + crate::expression_tree::ImageReference::AbsolutePath(path) => { + if path.starts_with("data:") { + format!(r#"slint::Image::load_from_data_url(u8"{}")"#, escape_string(path.as_str())) + } else { + format!(r#"slint::Image::load_from_path(u8"{}")"#, escape_string(path.as_str())) + } + } crate::expression_tree::ImageReference::EmbeddedData { resource_id, extension } => { let symbol = format!("slint_embedded_resource_{resource_id}"); format!(r#"slint::private_api::load_image_from_embedded_data({symbol}, "{}")"#, escape_string(extension)) diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index d11b3348256..6279cdfc87a 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -2460,7 +2460,11 @@ fn compile_expression(expr: &Expression, ctx: &EvaluationContext) -> TokenStream } crate::expression_tree::ImageReference::AbsolutePath(path) => { let path = path.as_str(); - quote!(sp::Image::load_from_path(::std::path::Path::new(#path)).unwrap_or_default()) + if path.starts_with("data:") { + quote!(sp::Image::load_from_data_url(#path).unwrap_or_default()) + } else { + quote!(sp::Image::load_from_path(::std::path::Path::new(#path)).unwrap_or_default()) + } } crate::expression_tree::ImageReference::EmbeddedData { resource_id, extension } => { let symbol = format_ident!("SLINT_EMBEDDED_RESOURCE_{}", resource_id); @@ -3411,6 +3415,10 @@ fn generate_resources(doc: &Document) -> Vec { let data = embedded_file_tokens(path); quote!(static #symbol: &'static [u8] = #data;) } + crate::embedded_resources::EmbeddedResourcesKind::DecodedData(data, _) => { + let data = proc_macro2::Literal::byte_string(data); + quote!(static #symbol: &'static [u8] = #data;) + } #[cfg(feature = "software-renderer")] crate::embedded_resources::EmbeddedResourcesKind::TextureData(crate::embedded_resources::Texture { data, format, rect, diff --git a/internal/compiler/passes/embed_images.rs b/internal/compiler/passes/embed_images.rs index 1618e8a02b2..3648951c1ec 100644 --- a/internal/compiler/passes/embed_images.rs +++ b/internal/compiler/passes/embed_images.rs @@ -143,6 +143,44 @@ fn embed_image( // Really do nothing with the image! e.insert(EmbeddedResources { id: maybe_id, kind: EmbeddedResourcesKind::ListOnly }); return ImageReference::None; + } else if path.starts_with("data:") { + // Handle data URLs by validating and decoding them at compile time + if let Ok(data_url) = dataurl::DataUrl::parse(path) { + let media_type = data_url.get_media_type(); + if !media_type.starts_with("image/") { + // Non-image data URLs should cause compilation errors + diag.push_error(format!("Data URL with unsupported media type '{}'. Only image/* data URLs are supported", media_type), source_location); + return ImageReference::None; + } + + let extension = media_type.split('/').nth(1).unwrap_or("").to_string(); + let decoded_data = if data_url.get_is_base64_encoded() { + data_url.get_data().to_vec() + } else { + data_url.get_text().as_bytes().to_vec() + }; + + // Check for oversized data URLs (> 1 MiB) + const MAX_DATA_URL_SIZE: usize = 1024 * 1024; // 1 MiB + if decoded_data.len() > MAX_DATA_URL_SIZE { + diag.push_error( + format!( + "Data URL is too large ({} bytes > {} bytes). Consider using a file reference instead.", + decoded_data.len(), + MAX_DATA_URL_SIZE + ), + source_location, + ); + return ImageReference::None; + } + + // For data URLs, store the decoded data with its extension + let kind = EmbeddedResourcesKind::DecodedData(decoded_data, extension); + e.insert(EmbeddedResources { id: maybe_id, kind }) + } else { + diag.push_error(format!("Invalid data URL format: {}", path), source_location); + return ImageReference::None; + } } else if let Some(_file) = crate::fileaccess::load_file(std::path::Path::new(path)) { #[allow(unused_mut)] let mut kind = EmbeddedResourcesKind::RawData; @@ -173,11 +211,14 @@ fn embed_image( } }; - match e.kind { + match &e.kind { #[cfg(feature = "software-renderer")] EmbeddedResourcesKind::TextureData { .. } => { ImageReference::EmbeddedTexture { resource_id: e.id } } + EmbeddedResourcesKind::DecodedData(_, extension) => { + ImageReference::EmbeddedData { resource_id: e.id, extension: extension.clone() } + } _ => ImageReference::EmbeddedData { resource_id: e.id, extension: std::path::Path::new(path) diff --git a/internal/compiler/passes/resolving.rs b/internal/compiler/passes/resolving.rs index 9ffba3ac12e..5ea0521c2d2 100644 --- a/internal/compiler/passes/resolving.rs +++ b/internal/compiler/passes/resolving.rs @@ -406,10 +406,16 @@ impl Expression { } fn from_at_image_url_node(node: syntax_nodes::AtImageUrl, ctx: &mut LookupCtx) -> Self { - let s = match node - .child_text(SyntaxKind::StringLiteral) - .and_then(|x| crate::literals::unescape_string(&x)) - { + let s = match node.child_text(SyntaxKind::StringLiteral).and_then(|x| { + if x.starts_with("\"data:") { + // Remove quotes here because unescape_string() doesn't support \n yet. + let x = x.strip_prefix('"')?; + let x = x.strip_suffix('"')?; + Some(SmolStr::new(x)) + } else { + crate::literals::unescape_string(&x) + } + }) { Some(s) => s, None => { ctx.diag.push_error("Cannot parse string literal".into(), &node); diff --git a/internal/core/Cargo.toml b/internal/core/Cargo.toml index c8f4992e1fa..d83509dcd41 100644 --- a/internal/core/Cargo.toml +++ b/internal/core/Cargo.toml @@ -40,6 +40,7 @@ std = [ "chrono/wasmbind", "chrono/clock", "dep:sys-locale", + "dataurl", ] # Unsafe feature meaning that there is only one core running and all thread_local are static. # You can only enable this feature if you are sure that any API of this crate is only called @@ -131,6 +132,10 @@ web-sys = { workspace = true, features = ["HtmlImageElement", "Navigator"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] fontdb = { workspace = true, optional = true, default-features = true } +[dependencies.dataurl] +version = "0.1.2" +optional = true + [dev-dependencies] slint = { path = "../../api/rs/slint", default-features = false, features = ["std", "compat-1-2"] } i-slint-backend-testing = { path = "../backends/testing" } diff --git a/internal/core/graphics/image.rs b/internal/core/graphics/image.rs index 13cd0345224..b2eb78f201f 100644 --- a/internal/core/graphics/image.rs +++ b/internal/core/graphics/image.rs @@ -709,6 +709,14 @@ impl Image { }) } + #[cfg(feature = "image-decoders")] + /// Load an Image from a data url + pub fn load_from_data_url(data_url: &str) -> Result { + self::cache::IMAGE_CACHE.with(|global_cache| { + global_cache.borrow_mut().load_image_from_data_url(&data_url).ok_or(LoadImageError(())) + }) + } + /// Creates a new Image from the specified shared pixel buffer, where each pixel has three color /// channels (red, green and blue) encoded as u8. pub fn from_rgb8(buffer: SharedPixelBuffer) -> Self { @@ -1325,6 +1333,18 @@ pub(crate) mod ffi { ) } + #[cfg(feature = "image-decoders")] + #[no_mangle] + pub unsafe extern "C" fn slint_image_load_from_data_url( + data_url: &SharedString, + image: *mut Image, + ) { + core::ptr::write( + image, + Image::load_from_data_url(data_url.as_str()).unwrap_or(Image::default()), + ) + } + #[cfg(feature = "std")] #[unsafe(no_mangle)] pub unsafe extern "C" fn slint_image_load_from_embedded_data( diff --git a/internal/core/graphics/image/cache.rs b/internal/core/graphics/image/cache.rs index 09a5735ee9a..098820b17c6 100644 --- a/internal/core/graphics/image/cache.rs +++ b/internal/core/graphics/image/cache.rs @@ -7,6 +7,8 @@ This module contains image and caching related types for the run-time library. use super::{CachedPath, Image, ImageCacheKey, ImageInner, SharedImageBuffer, SharedPixelBuffer}; use crate::{slice::Slice, SharedString}; +#[cfg(not(target_arch = "wasm32"))] +use dataurl::DataUrl; struct ImageWeightInBytes; @@ -112,6 +114,65 @@ impl ImageCache { }); } + pub(crate) fn load_image_from_data_url(&mut self, str: &str) -> Option { + let cache_key = ImageCacheKey::Path(CachedPath::new(str)); + #[cfg(target_arch = "wasm32")] + return self.lookup_image_in_cache_or_create(cache_key, |_| { + return Some(ImageInner::HTMLImage(vtable::VRc::new( + super::htmlimage::HTMLImage::new(&str), + ))); + }); + #[cfg(not(target_arch = "wasm32"))] + return self.lookup_image_in_cache_or_create(cache_key, |cache_key| { + let data_url = match DataUrl::parse(&str) { + Ok(url) => url, + Err(e) => { + crate::debug_log!("Failed to parse data URL: {:?}", e); + return None; + } + }; + let media_type = data_url.get_media_type(); + if !media_type.starts_with("image/") { + crate::debug_log!("Unsupported media type: {}", media_type); + return None; + } + let media_type = media_type.split('/').nth(1).unwrap_or(""); + + let text = data_url.get_text(); + let data = if data_url.get_is_base64_encoded() { + data_url.get_data() + } else { + text.as_bytes() + }; + + if cfg!(feature = "svg") && (media_type == ("svg+xml") || media_type == "svgz+xml") { + return Some(ImageInner::Svg(vtable::VRc::new( + super::svg::load_from_data(data, cache_key).map_or_else( + |err| { + crate::debug_log!("Error loading SVG from {}: {}", &str, err); + None + }, + Some, + )?, + ))); + } + + let format = image::ImageFormat::from_extension(media_type); + image::load_from_memory_with_format(data, format.unwrap()).map_or_else( + |decode_err| { + crate::debug_log!("Error loading image from {}: {}", &str, decode_err); + None + }, + |image| { + Some(ImageInner::EmbeddedImage { + cache_key, + buffer: dynamic_image_to_shared_image_buffer(image), + }) + }, + ) + }); + } + pub(crate) fn load_image_from_embedded_data( &mut self, data: Slice<'static, u8>, diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs index e2472118b71..23598a3bcff 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -283,17 +283,22 @@ pub fn eval_expression(expression: &Expression, local_context: &mut EvalLocalCon Ok(Default::default()) } i_slint_compiler::expression_tree::ImageReference::AbsolutePath(path) => { - let path = std::path::Path::new(path); - if path.starts_with("builtin:/") { - i_slint_compiler::fileaccess::load_file(path).and_then(|virtual_file| virtual_file.builtin_contents).map(|virtual_file| { - let extension = path.extension().unwrap().to_str().unwrap(); - corelib::graphics::load_image_from_embedded_data( - corelib::slice::Slice::from_slice(virtual_file), - corelib::slice::Slice::from_slice(extension.as_bytes()) - ) - }).ok_or_else(Default::default) + if path.starts_with("data:") { + // For interpreter, continue handling data URLs at runtime + corelib::graphics::Image::load_from_data_url(path) } else { - corelib::graphics::Image::load_from_path(path) + let path = std::path::Path::new(path); + if path.starts_with("builtin:/") { + i_slint_compiler::fileaccess::load_file(path).and_then(|virtual_file| virtual_file.builtin_contents).map(|virtual_file| { + let extension = path.extension().unwrap().to_str().unwrap(); + corelib::graphics::load_image_from_embedded_data( + corelib::slice::Slice::from_slice(virtual_file), + corelib::slice::Slice::from_slice(extension.as_bytes()) + ) + }).ok_or_else(Default::default) + } else { + corelib::graphics::Image::load_from_path(path) + } } } i_slint_compiler::expression_tree::ImageReference::EmbeddedData { .. } => { diff --git a/tests/cases/elements/image.slint b/tests/cases/elements/image.slint index 01398a289c8..6a6c9953b46 100644 --- a/tests/cases/elements/image.slint +++ b/tests/cases/elements/image.slint @@ -19,11 +19,19 @@ TestCase := Rectangle { out property with-border: @image-url("dog.jpg", nine-slice(12 13 14 15)); + // slint-logo-small-light.png + out property data-url-image-png-base64: @image-url("data:image/png;base64,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"); + + out property data-url-image-svg: @image-url("data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0A%3Csvg%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%2295%22%20viewBox%3D%220%200%20100%2095%22%3E%0A%3Cpolygon%20fill%3D%22%23231815%22%20points%3D%2250%2C0%2065.451%2C31.271%20100%2C36.287%2075%2C60.629%2080.902%2C95%2050%2C78.771%2019.098%2C95%2025%2C60.629%200%2C36.287%20%0A%0934.549%2C31.271%20%22%2F%3E%0A%3C%2Fsvg%3E"); + property img_width: img.width; property img_height: img.height; in-out property test_no_overflow: (21 - img.source.width) / 2; // (21 - 320)/2 = -149.5 + property data-url: data-url-image-png-base64.width == 128 && data-url-image-png-base64.height == 128 && + data-url-image-svg.width > 0 && data-url-image-svg.height > 0; property test: img2.source-clip-height * 1px == img2.height && img2.source-clip-width * 1px == img2.width && - img2.width/1px == img2.source.width - 20 && img3.source.width == 0 && img3.source.height == 0 && test_no_overflow == -149.5; + img2.width/1px == img2.source.width - 20 && img3.source.width == 0 && img3.source.height == 0 && test_no_overflow == -149.5 && + data-url; } /*