diff --git a/api/cpp/cbindgen.rs b/api/cpp/cbindgen.rs index 6de9bbb3d16..8e9170941aa 100644 --- a/api/cpp/cbindgen.rs +++ b/api/cpp/cbindgen.rs @@ -387,6 +387,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", @@ -501,6 +502,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", @@ -593,6 +595,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_set_nine_slice_edges", "slint_image_to_rgb8", 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/generator/cpp.rs b/internal/compiler/generator/cpp.rs index 9ec1d723e9d..0cc243e716b 100644 --- a/internal/compiler/generator/cpp.rs +++ b/internal/compiler/generator/cpp.rs @@ -3249,7 +3249,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 744e7e22961..9c0853d5ac8 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -2417,7 +2417,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); diff --git a/internal/compiler/passes/resolving.rs b/internal/compiler/passes/resolving.rs index d6274adc75d..1d8173166d4 100644 --- a/internal/compiler/passes/resolving.rs +++ b/internal/compiler/passes/resolving.rs @@ -361,7 +361,14 @@ 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)) + .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 => { diff --git a/internal/core/Cargo.toml b/internal/core/Cargo.toml index 87c7f02f08c..738afa31be5 100644 --- a/internal/core/Cargo.toml +++ b/internal/core/Cargo.toml @@ -105,6 +105,7 @@ web-sys = { workspace = true, features = [ "HtmlImageElement" ] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] fontdb = { workspace = true, optional = true, default-features = true } +dataurl = "0.1.2" [dev-dependencies] slint = { path = "../../api/rs/slint", default-features = false, features = ["std", "compat-1-2"] } diff --git a/internal/core/graphics/image.rs b/internal/core/graphics/image.rs index 0d5b27efcfa..d947ef373dc 100644 --- a/internal/core/graphics/image.rs +++ b/internal/core/graphics/image.rs @@ -671,6 +671,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 { @@ -1274,6 +1282,15 @@ 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")] #[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 8eb896a3a86..febac8b2b14 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; @@ -110,6 +112,59 @@ 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 = DataUrl::parse(&str).unwrap(); + let media_type = data_url.get_media_type(); + if !media_type.starts_with("image/") { + eprintln!("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| { + eprintln!("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| { + eprintln!("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 7fdc3ecd25c..254b3fa185e 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -272,17 +272,21 @@ 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:") { + 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 de4cf178fe8..9721670dca3 100644 --- a/tests/cases/elements/image.slint +++ b/tests/cases/elements/image.slint @@ -17,12 +17,23 @@ TestCase := Rectangle { source: @image-url("image.slint"); } + out property with-border: @image-url("dog.jpg", nine-slice(12 13 14 15)); + out property data-url-plain-text: @image-url("data:,Hello%2C%20World%21"); + + // 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; + property data-url: data-url-plain-text.width == 0 && data-url-plain-text.height == 0 && + 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; + img2.width/1px == img2.source.width - 20 && img3.source.width == 0 && img3.source.height == 0 && data-url; } /*