Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/cpp/cbindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions api/cpp/include/slint_image.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions internal/compiler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
2 changes: 2 additions & 0 deletions internal/compiler/embedded_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>, String),
/// The data has been processed in a texture
#[cfg(feature = "software-renderer")]
TextureData(Texture),
Expand Down
33 changes: 31 additions & 2 deletions internal/compiler/generator/cpp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
Expand Down Expand Up @@ -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()
}));
}
}
}

Expand Down Expand Up @@ -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))
Expand Down
10 changes: 9 additions & 1 deletion internal/compiler/generator/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -3411,6 +3415,10 @@ fn generate_resources(doc: &Document) -> Vec<TokenStream> {
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,
Expand Down
43 changes: 42 additions & 1 deletion internal/compiler/passes/embed_images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 10 additions & 4 deletions internal/compiler/passes/resolving.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions internal/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" }
Expand Down
20 changes: 20 additions & 0 deletions internal/core/graphics/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, LoadImageError> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is public API.

But I think we don't need this and should re-use the existing load_image_from_embedded_data that is already used by the generated code when the data is embedded. (and same in C++)

I wonder if we need this function call or if we could change

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<Rgb8Pixel>) -> Self {
Expand Down Expand Up @@ -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(
Expand Down
61 changes: 61 additions & 0 deletions internal/core/graphics/image/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -112,6 +114,65 @@ impl ImageCache {
});
}

pub(crate) fn load_image_from_data_url(&mut self, str: &str) -> Option<Image> {
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>,
Expand Down
25 changes: 15 additions & 10 deletions internal/interpreter/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 { .. } => {
Expand Down
Loading