Skip to content

Commit

Permalink
Image: data url support
Browse files Browse the repository at this point in the history
Data url format is useful when one tries slintpad with images.

Image {
    source: @image-url("data:image/png;base64,iV....=");
}

Fixes: #4905
  • Loading branch information
task-jp committed Dec 24, 2024
1 parent d6f83a2 commit 6a138e9
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 14 deletions.
3 changes: 3 additions & 0 deletions api/cpp/cbindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
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
8 changes: 7 additions & 1 deletion internal/compiler/generator/cpp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
6 changes: 5 additions & 1 deletion internal/compiler/generator/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 8 additions & 1 deletion internal/compiler/passes/resolving.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
1 change: 1 addition & 0 deletions internal/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
17 changes: 17 additions & 0 deletions internal/core/graphics/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, LoadImageError> {
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 @@ -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(
Expand Down
55 changes: 55 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 @@ -110,6 +112,59 @@ 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 = 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>,
Expand Down
24 changes: 14 additions & 10 deletions internal/interpreter/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 { .. } => {
Expand Down
13 changes: 12 additions & 1 deletion tests/cases/elements/image.slint
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,23 @@ TestCase := Rectangle {
source: @image-url("image.slint");
}


out property <image> with-border: @image-url("dog.jpg", nine-slice(12 13 14 15));

out property <image> data-url-plain-text: @image-url("data:,Hello%2C%20World%21");

// slint-logo-small-light.png
out property <image> data-url-image-png-base64: @image-url("data:image/png;base64,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");

out property <image> 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 <length> img_width: img.width;
property <length> img_height: img.height;
property <bool> 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 <bool> 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;
}

/*
Expand Down

0 comments on commit 6a138e9

Please sign in to comment.