Skip to content

Commit 705521c

Browse files
committed
Image: data url support
Data url format is useful when one tries slintpad with images. Image { source: @image-url("data:image/png;base64,iV....="); } Fixes: slint-ui#4905
1 parent d3c0e2c commit 705521c

File tree

13 files changed

+227
-19
lines changed

13 files changed

+227
-19
lines changed

api/cpp/cbindgen.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,7 @@ fn gen_corelib(
485485
"slint_image_size",
486486
"slint_image_path",
487487
"slint_image_load_from_path",
488+
"slint_image_load_from_data_url",
488489
"slint_image_load_from_embedded_data",
489490
"slint_image_from_embedded_textures",
490491
"slint_image_compare_equal",

api/cpp/include/slint_image.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,14 @@ struct Image
130130
cbindgen_private::types::slint_image_load_from_path(&file_path, &img.data);
131131
return img;
132132
}
133+
134+
/// Load an image from data url
135+
[[nodiscard]] static Image load_from_data_url(const SharedString &data_url)
136+
{
137+
Image img;
138+
cbindgen_private::types::slint_image_load_from_data_url(&data_url, &img.data);
139+
return img;
140+
}
133141
#endif
134142

135143
/// Constructs a new Image from an existing OpenGL texture. The texture remains borrowed by

internal/compiler/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ itertools = { workspace = true }
5656
url = "2.2.1"
5757
linked_hash_set = "0.1.4"
5858
typed-index-collections = "3.2"
59+
dataurl = "0.1.2"
5960

6061
# for processing and embedding the rendered image (texture)
6162
image = { workspace = true, optional = true, features = ["default"] }

internal/compiler/embedded_resources.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ pub enum EmbeddedResourcesKind {
9595
ListOnly,
9696
/// Just put the file content as a resource
9797
RawData,
98+
/// Decoded data from a data URL (data, extension)
99+
DecodedData(Vec<u8>, String),
98100
/// The data has been processed in a texture
99101
#[cfg(feature = "software-renderer")]
100102
TextureData(Texture),

internal/compiler/generator/cpp.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -933,7 +933,7 @@ fn embed_resource(
933933
init.push(',');
934934
}
935935
write!(&mut init, "0x{byte:x}").unwrap();
936-
if index % 16 == 0 {
936+
if index > 0 && index % 16 == 0 {
937937
init.push('\n');
938938
}
939939
}
@@ -1136,6 +1136,29 @@ fn embed_resource(
11361136
..Default::default()
11371137
}))
11381138
}
1139+
crate::embedded_resources::EmbeddedResourcesKind::DecodedData(data, _) => {
1140+
let mut init = "{ ".to_string();
1141+
1142+
for (index, byte) in data.iter().enumerate() {
1143+
if index > 0 {
1144+
init.push(',');
1145+
}
1146+
write!(&mut init, "0x{byte:x}").unwrap();
1147+
if index > 0 && index % 16 == 0 {
1148+
init.push('\n');
1149+
}
1150+
}
1151+
1152+
init.push('}');
1153+
1154+
declarations.push(Declaration::Var(Var {
1155+
ty: "const uint8_t".into(),
1156+
name: format_smolstr!("slint_embedded_resource_{}", resource.id),
1157+
array_size: Some(data.len()),
1158+
init: Some(init),
1159+
..Default::default()
1160+
}));
1161+
}
11391162
}
11401163
}
11411164

@@ -3342,7 +3365,13 @@ fn compile_expression(expr: &llr::Expression, ctx: &EvaluationContext) -> String
33423365
Expression::ImageReference { resource_ref, nine_slice } => {
33433366
let image = match resource_ref {
33443367
crate::expression_tree::ImageReference::None => r#"slint::Image()"#.to_string(),
3345-
crate::expression_tree::ImageReference::AbsolutePath(path) => format!(r#"slint::Image::load_from_path(slint::SharedString(u8"{}"))"#, escape_string(path.as_str())),
3368+
crate::expression_tree::ImageReference::AbsolutePath(path) => {
3369+
if path.starts_with("data:") {
3370+
format!(r#"slint::Image::load_from_data_url(u8"{}")"#, escape_string(path.as_str()))
3371+
} else {
3372+
format!(r#"slint::Image::load_from_path(u8"{}")"#, escape_string(path.as_str()))
3373+
}
3374+
}
33463375
crate::expression_tree::ImageReference::EmbeddedData { resource_id, extension } => {
33473376
let symbol = format!("slint_embedded_resource_{resource_id}");
33483377
format!(r#"slint::private_api::load_image_from_embedded_data({symbol}, "{}")"#, escape_string(extension))

internal/compiler/generator/rust.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2460,7 +2460,11 @@ fn compile_expression(expr: &Expression, ctx: &EvaluationContext) -> TokenStream
24602460
}
24612461
crate::expression_tree::ImageReference::AbsolutePath(path) => {
24622462
let path = path.as_str();
2463-
quote!(sp::Image::load_from_path(::std::path::Path::new(#path)).unwrap_or_default())
2463+
if path.starts_with("data:") {
2464+
quote!(sp::Image::load_from_data_url(#path).unwrap_or_default())
2465+
} else {
2466+
quote!(sp::Image::load_from_path(::std::path::Path::new(#path)).unwrap_or_default())
2467+
}
24642468
}
24652469
crate::expression_tree::ImageReference::EmbeddedData { resource_id, extension } => {
24662470
let symbol = format_ident!("SLINT_EMBEDDED_RESOURCE_{}", resource_id);
@@ -3411,6 +3415,10 @@ fn generate_resources(doc: &Document) -> Vec<TokenStream> {
34113415
let data = embedded_file_tokens(path);
34123416
quote!(static #symbol: &'static [u8] = #data;)
34133417
}
3418+
crate::embedded_resources::EmbeddedResourcesKind::DecodedData(data, _) => {
3419+
let data = proc_macro2::Literal::byte_string(data);
3420+
quote!(static #symbol: &'static [u8] = #data;)
3421+
}
34143422
#[cfg(feature = "software-renderer")]
34153423
crate::embedded_resources::EmbeddedResourcesKind::TextureData(crate::embedded_resources::Texture {
34163424
data, format, rect,

internal/compiler/passes/embed_images.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ fn embed_images_from_expression(
9797
if embed_files != EmbedResourcesKind::Nothing
9898
&& (embed_files != EmbedResourcesKind::OnlyBuiltinResources
9999
|| path.starts_with("builtin:/"))
100+
&& (path.starts_with("data:image/") || !path.starts_with("data:"))
100101
{
101102
let image_ref = embed_image(
102103
global_embedded_resources,
@@ -143,6 +144,44 @@ fn embed_image(
143144
// Really do nothing with the image!
144145
e.insert(EmbeddedResources { id: maybe_id, kind: EmbeddedResourcesKind::ListOnly });
145146
return ImageReference::None;
147+
} else if path.starts_with("data:") {
148+
// Handle data URLs by decoding them at compile time
149+
if let Ok(data_url) = dataurl::DataUrl::parse(path) {
150+
let media_type = data_url.get_media_type();
151+
if !media_type.starts_with("image/") {
152+
// Non-image data URLs are not embedded, they remain as AbsolutePath
153+
// and will be handled at runtime (e.g., for plain text)
154+
return ImageReference::AbsolutePath(path.into());
155+
}
156+
157+
let extension = media_type.split('/').nth(1).unwrap_or("").to_string();
158+
let decoded_data = if data_url.get_is_base64_encoded() {
159+
data_url.get_data().to_vec()
160+
} else {
161+
data_url.get_text().as_bytes().to_vec()
162+
};
163+
164+
// Check for oversized data URLs (> 1 MiB)
165+
const MAX_DATA_URL_SIZE: usize = 1024 * 1024; // 1 MiB
166+
if decoded_data.len() > MAX_DATA_URL_SIZE {
167+
diag.push_error(
168+
format!(
169+
"Data URL is too large ({} bytes > {} bytes). Consider using a file reference instead.",
170+
decoded_data.len(),
171+
MAX_DATA_URL_SIZE
172+
),
173+
source_location,
174+
);
175+
return ImageReference::None;
176+
}
177+
178+
// For data URLs, store the decoded data with its extension
179+
let kind = EmbeddedResourcesKind::DecodedData(decoded_data, extension);
180+
e.insert(EmbeddedResources { id: maybe_id, kind })
181+
} else {
182+
diag.push_error(format!("Invalid data URL format: {}", path), source_location);
183+
return ImageReference::None;
184+
}
146185
} else if let Some(_file) = crate::fileaccess::load_file(std::path::Path::new(path)) {
147186
#[allow(unused_mut)]
148187
let mut kind = EmbeddedResourcesKind::RawData;
@@ -173,11 +212,14 @@ fn embed_image(
173212
}
174213
};
175214

176-
match e.kind {
215+
match &e.kind {
177216
#[cfg(feature = "software-renderer")]
178217
EmbeddedResourcesKind::TextureData { .. } => {
179218
ImageReference::EmbeddedTexture { resource_id: e.id }
180219
}
220+
EmbeddedResourcesKind::DecodedData(_, extension) => {
221+
ImageReference::EmbeddedData { resource_id: e.id, extension: extension.clone() }
222+
}
181223
_ => ImageReference::EmbeddedData {
182224
resource_id: e.id,
183225
extension: std::path::Path::new(path)

internal/compiler/passes/resolving.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -406,10 +406,16 @@ impl Expression {
406406
}
407407

408408
fn from_at_image_url_node(node: syntax_nodes::AtImageUrl, ctx: &mut LookupCtx) -> Self {
409-
let s = match node
410-
.child_text(SyntaxKind::StringLiteral)
411-
.and_then(|x| crate::literals::unescape_string(&x))
412-
{
409+
let s = match node.child_text(SyntaxKind::StringLiteral).and_then(|x| {
410+
if x.starts_with("\"data:") {
411+
// Remove quotes here because unescape_string() doesn't support \n yet.
412+
let x = x.strip_prefix('"')?;
413+
let x = x.strip_suffix('"')?;
414+
Some(SmolStr::new(x))
415+
} else {
416+
crate::literals::unescape_string(&x)
417+
}
418+
}) {
413419
Some(s) => s,
414420
None => {
415421
ctx.diag.push_error("Cannot parse string literal".into(), &node);

internal/core/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ std = [
4040
"chrono/wasmbind",
4141
"chrono/clock",
4242
"dep:sys-locale",
43+
"dataurl",
4344
]
4445
# Unsafe feature meaning that there is only one core running and all thread_local are static.
4546
# 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"] }
131132
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
132133
fontdb = { workspace = true, optional = true, default-features = true }
133134

135+
[dependencies.dataurl]
136+
version = "0.1.2"
137+
optional = true
138+
134139
[dev-dependencies]
135140
slint = { path = "../../api/rs/slint", default-features = false, features = ["std", "compat-1-2"] }
136141
i-slint-backend-testing = { path = "../backends/testing" }

internal/core/graphics/image.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,14 @@ impl Image {
709709
})
710710
}
711711

712+
#[cfg(feature = "image-decoders")]
713+
/// Load an Image from a data url
714+
pub fn load_from_data_url(data_url: &str) -> Result<Self, LoadImageError> {
715+
self::cache::IMAGE_CACHE.with(|global_cache| {
716+
global_cache.borrow_mut().load_image_from_data_url(&data_url).ok_or(LoadImageError(()))
717+
})
718+
}
719+
712720
/// Creates a new Image from the specified shared pixel buffer, where each pixel has three color
713721
/// channels (red, green and blue) encoded as u8.
714722
pub fn from_rgb8(buffer: SharedPixelBuffer<Rgb8Pixel>) -> Self {
@@ -1325,6 +1333,18 @@ pub(crate) mod ffi {
13251333
)
13261334
}
13271335

1336+
#[cfg(feature = "image-decoders")]
1337+
#[no_mangle]
1338+
pub unsafe extern "C" fn slint_image_load_from_data_url(
1339+
data_url: &SharedString,
1340+
image: *mut Image,
1341+
) {
1342+
core::ptr::write(
1343+
image,
1344+
Image::load_from_data_url(data_url.as_str()).unwrap_or(Image::default()),
1345+
)
1346+
}
1347+
13281348
#[cfg(feature = "std")]
13291349
#[unsafe(no_mangle)]
13301350
pub unsafe extern "C" fn slint_image_load_from_embedded_data(

0 commit comments

Comments
 (0)