WIP: Make image decoding a feature of the core library

This includes the cache of decoded images, the HTMLImage element support
and the SVG rendering adapter.

The objective is that Image holds an ImageInner, which is not a path
anymore that the backend has to process, but instead always either
decoded image data, a pointer to a static texture or an SVG tree that
can be rendered to the desired size.
This commit is contained in:
Simon Hausmann 2022-06-02 13:23:22 +02:00 committed by Simon Hausmann
parent 29d28dc73e
commit 67a2f0ce3f
22 changed files with 448 additions and 282 deletions

View file

@ -198,6 +198,7 @@ fn gen_corelib(
"slint_color_darker", "slint_color_darker",
"slint_image_size", "slint_image_size",
"slint_image_path", "slint_image_path",
"slint_image_load_from_path",
"Coord", "Coord",
] ]
.iter() .iter()
@ -260,6 +261,7 @@ fn gen_corelib(
"Size", "Size",
"slint_image_size", "slint_image_size",
"slint_image_path", "slint_image_path",
"slint_image_load_from_path",
"SharedPixelBuffer", "SharedPixelBuffer",
"SharedImageBuffer", "SharedImageBuffer",
"StaticTextures", "StaticTextures",
@ -311,6 +313,7 @@ fn gen_corelib(
"slint_color_darker", "slint_color_darker",
"slint_image_size", "slint_image_size",
"slint_image_path", "slint_image_path",
"slint_image_load_from_path",
] ]
.iter() .iter()
.filter(|exclusion| !rust_types.iter().any(|inclusion| inclusion == *exclusion)) .filter(|exclusion| !rust_types.iter().any(|inclusion| inclusion == *exclusion))

View file

@ -21,7 +21,7 @@ public:
static Image load_from_path(const SharedString &file_path) static Image load_from_path(const SharedString &file_path)
{ {
Image img; Image img;
img.data = Data::ImageInner_AbsoluteFilePath(file_path); cbindgen_private::types::slint_image_load_from_path(&file_path, &img.data);
return img; return img;
} }

View file

@ -254,12 +254,9 @@ fn to_js_value<'cx>(
Value::Bool(b) => JsBoolean::new(cx, b).as_value(cx), Value::Bool(b) => JsBoolean::new(cx, b).as_value(cx),
Value::Image(r) => match (&r).into() { Value::Image(r) => match (&r).into() {
&ImageInner::None => JsUndefined::new().as_value(cx), &ImageInner::None => JsUndefined::new().as_value(cx),
&ImageInner::AbsoluteFilePath(ref path) => { &ImageInner::EmbeddedImage { .. }
JsString::new(cx, path.as_str()).as_value(cx) | &ImageInner::StaticTextures { .. }
} | &ImageInner::Svg(..) => JsNull::new().as_value(cx), // TODO: maybe pass around node buffers?
&ImageInner::EmbeddedData { .. }
| &ImageInner::EmbeddedImage { .. }
| &ImageInner::StaticTextures { .. } => JsNull::new().as_value(cx), // TODO: maybe pass around node buffers?
}, },
Value::Model(model) => { Value::Model(model) => {
if let Some(js_model) = model.as_any().downcast_ref::<js_model::JsModel>() { if let Some(js_model) = model.as_any().downcast_ref::<js_model::JsModel>() {

View file

@ -1116,7 +1116,8 @@ impl GLItemRenderer {
let image = source_property.get(); let image = source_property.get();
let image_inner: &ImageInner = (&image).into(); let image_inner: &ImageInner = (&image).into();
let target_size_for_scalable_source = image_inner.is_svg().then(|| { let target_size_for_scalable_source =
matches!(image_inner, ImageInner::Svg(..)).then(|| {
// get the scale factor as a property again, to ensure the cache is invalidated when the scale factor changes // get the scale factor as a property again, to ensure the cache is invalidated when the scale factor changes
let scale_factor = self.window().scale_factor(); let scale_factor = self.window().scale_factor();
[ [

View file

@ -204,9 +204,8 @@ impl CachedImage {
pub fn new_from_resource(resource: &ImageInner) -> Option<Self> { pub fn new_from_resource(resource: &ImageInner) -> Option<Self> {
match resource { match resource {
ImageInner::None => None, ImageInner::None => None,
ImageInner::AbsoluteFilePath(path) => Self::new_from_path(path), ImageInner::Svg { .. } => unimplemented!(),
ImageInner::EmbeddedData { data, format } => Self::new_from_data(data, format), ImageInner::EmbeddedImage { buffer, .. } => {
ImageInner::EmbeddedImage(buffer) => {
Some(Self(RefCell::new(ImageData::EmbeddedImage(buffer.clone())))) Some(Self(RefCell::new(ImageData::EmbeddedImage(buffer.clone()))))
} }
ImageInner::StaticTextures { .. } => todo!(), ImageInner::StaticTextures { .. } => todo!(),
@ -394,17 +393,9 @@ impl ImageCacheKey {
pub fn new(resource: &ImageInner) -> Option<Self> { pub fn new(resource: &ImageInner) -> Option<Self> {
Some(match resource { Some(match resource {
ImageInner::None => return None, ImageInner::None => return None,
ImageInner::AbsoluteFilePath(path) => {
if path.is_empty() {
return None;
}
path.clone().into()
}
ImageInner::EmbeddedData { data, format: _ } => {
by_address::ByAddress(data.as_slice()).into()
}
ImageInner::EmbeddedImage { .. } => return None, ImageInner::EmbeddedImage { .. } => return None,
ImageInner::StaticTextures { .. } => return None, ImageInner::StaticTextures { .. } => return None,
ImageInner::Svg { .. } => return None,
}) })
} }
} }

View file

@ -9,7 +9,6 @@ extern crate alloc;
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use i_slint_core::graphics::{Image, IntSize};
use i_slint_core::window::Window; use i_slint_core::window::Window;
mod glwindow; mod glwindow;
@ -141,14 +140,4 @@ impl i_slint_core::backend::Backend for Backend {
}); });
} }
} }
fn image_size(&'static self, image: &Image) -> IntSize {
IMAGE_CACHE.with(|image_cache| {
image_cache
.borrow_mut()
.load_image_resource(image.into())
.and_then(|image| image.size())
.unwrap_or_default()
})
}
} }

View file

@ -126,7 +126,7 @@ mod the_backend {
use i_slint_core::items::ItemRef; use i_slint_core::items::ItemRef;
use i_slint_core::window::PlatformWindow; use i_slint_core::window::PlatformWindow;
use i_slint_core::window::Window; use i_slint_core::window::Window;
use i_slint_core::{Coord, ImageInner, StaticTextures}; use i_slint_core::Coord;
thread_local! { static WINDOWS: RefCell<Option<Rc<McuWindow>>> = RefCell::new(None) } thread_local! { static WINDOWS: RefCell<Option<Rc<McuWindow>>> = RefCell::new(None) }
@ -506,21 +506,6 @@ mod the_backend {
self.with_inner(|inner| inner.post_event(McuEvent::Custom(event))); self.with_inner(|inner| inner.post_event(McuEvent::Custom(event)));
} }
fn image_size(
&'static self,
image: &i_slint_core::graphics::Image,
) -> i_slint_core::graphics::IntSize {
let inner: &ImageInner = image.into();
match inner {
ImageInner::None => Default::default(),
ImageInner::AbsoluteFilePath(_) | ImageInner::EmbeddedData { .. } => {
unimplemented!()
}
ImageInner::EmbeddedImage(buffer) => buffer.size(),
ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size,
}
}
#[cfg(feature = "std")] #[cfg(feature = "std")]
fn register_font_from_memory( fn register_font_from_memory(
&'static self, &'static self,

View file

@ -10,7 +10,6 @@ use embedded_graphics::prelude::*;
use embedded_graphics_simulator::SimulatorDisplay; use embedded_graphics_simulator::SimulatorDisplay;
use i_slint_core::api::{euclid, PhysicalPx}; use i_slint_core::api::{euclid, PhysicalPx};
use i_slint_core::component::ComponentRc; use i_slint_core::component::ComponentRc;
use i_slint_core::graphics::{Image, ImageInner, StaticTextures};
use i_slint_core::input::KeyboardModifiers; use i_slint_core::input::KeyboardModifiers;
use i_slint_core::item_rendering::DirtyRegion; use i_slint_core::item_rendering::DirtyRegion;
use i_slint_core::items::{Item, ItemRef, WindowItem}; use i_slint_core::items::{Item, ItemRef, WindowItem};
@ -475,14 +474,4 @@ impl i_slint_core::backend::Backend for SimulatorBackend {
.unwrap() .unwrap()
.send_event(self::event_loop::CustomEvent::UserEvent(event)); .send_event(self::event_loop::CustomEvent::UserEvent(event));
} }
fn image_size(&'static self, image: &Image) -> i_slint_core::graphics::IntSize {
let inner: &ImageInner = image.into();
match inner {
ImageInner::None => Default::default(),
ImageInner::AbsoluteFilePath(_) | ImageInner::EmbeddedData { .. } => unimplemented!(),
ImageInner::EmbeddedImage(buffer) => buffer.size(),
ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size,
}
}
} }

View file

@ -9,14 +9,7 @@
extern crate alloc; extern crate alloc;
#[cfg(not(no_qt))]
use i_slint_core::api::euclid;
use i_slint_core::graphics::{Image, IntSize};
#[cfg(not(no_qt))]
use i_slint_core::items::ImageFit;
use i_slint_core::window::Window; use i_slint_core::window::Window;
#[cfg(not(no_qt))]
use i_slint_core::ImageInner;
#[cfg(not(no_qt))] #[cfg(not(no_qt))]
mod qt_accessible; mod qt_accessible;
@ -296,23 +289,4 @@ impl i_slint_core::backend::Backend for Backend {
}} }}
}; };
} }
fn image_size(&'static self, _image: &Image) -> IntSize {
#[cfg(not(no_qt))]
{
let inner: &ImageInner = _image.into();
match inner {
i_slint_core::ImageInner::None => Default::default(),
i_slint_core::ImageInner::EmbeddedImage(buffer) => buffer.size(),
_ => qt_window::load_image_from_resource(inner, None, ImageFit::fill)
.map(|img| {
let qsize = img.size();
euclid::size2(qsize.width, qsize.height)
})
.unwrap_or_default(),
}
}
#[cfg(no_qt)]
Default::default()
}
} }

View file

@ -164,11 +164,7 @@ impl NativeButton {
Some(StandardButtonKind::retry) => QStyle_StandardPixmap_SP_DialogRetryButton, Some(StandardButtonKind::retry) => QStyle_StandardPixmap_SP_DialogRetryButton,
Some(StandardButtonKind::ignore) => QStyle_StandardPixmap_SP_DialogIgnoreButton, Some(StandardButtonKind::ignore) => QStyle_StandardPixmap_SP_DialogIgnoreButton,
None => { None => {
return crate::qt_window::load_image_from_resource( return crate::qt_window::image_to_pixmap((&self.icon()).into(), None)
(&self.icon()).into(),
None,
Default::default(),
)
.unwrap_or_default(); .unwrap_or_default();
} }
}; };

View file

@ -332,12 +332,8 @@ impl Item for NativeTab {
fn layout_info(self: Pin<&Self>, orientation: Orientation, _window: &WindowRc) -> LayoutInfo { fn layout_info(self: Pin<&Self>, orientation: Orientation, _window: &WindowRc) -> LayoutInfo {
let text: qttypes::QString = self.title().as_str().into(); let text: qttypes::QString = self.title().as_str().into();
let icon: qttypes::QPixmap = crate::qt_window::load_image_from_resource( let icon: qttypes::QPixmap =
(&self.icon()).into(), crate::qt_window::image_to_pixmap((&self.icon()).into(), None).unwrap_or_default();
None,
Default::default(),
)
.unwrap_or_default();
let tab_index: i32 = self.tab_index(); let tab_index: i32 = self.tab_index();
let num_tabs: i32 = self.num_tabs(); let num_tabs: i32 = self.num_tabs();
let size = cpp!(unsafe [ let size = cpp!(unsafe [
@ -436,10 +432,9 @@ impl Item for NativeTab {
fn_render! { this dpr size painter widget initial_state => fn_render! { this dpr size painter widget initial_state =>
let down: bool = this.pressed(); let down: bool = this.pressed();
let text: qttypes::QString = this.title().as_str().into(); let text: qttypes::QString = this.title().as_str().into();
let icon: qttypes::QPixmap = crate::qt_window::load_image_from_resource( let icon: qttypes::QPixmap = crate::qt_window::image_to_pixmap(
(&this.icon()).into(), (&this.icon()).into(),
None, None,
Default::default(),
) )
.unwrap_or_default(); .unwrap_or_default();
let enabled: bool = this.enabled(); let enabled: bool = this.enabled();

View file

@ -11,7 +11,7 @@ use i_slint_core::graphics::rendering_metrics_collector::{
RenderingMetrics, RenderingMetricsCollector, RenderingMetrics, RenderingMetricsCollector,
}; };
use i_slint_core::graphics::{ use i_slint_core::graphics::{
Brush, Color, FontRequest, Image, Point, Rect, SharedImageBuffer, Size, Brush, Color, FontRequest, Image, IntSize, Point, Rect, SharedImageBuffer, Size,
}; };
use i_slint_core::input::{KeyEvent, KeyEventType, MouseEvent}; use i_slint_core::input::{KeyEvent, KeyEventType, MouseEvent};
use i_slint_core::item_rendering::{ItemCache, ItemRenderer}; use i_slint_core::item_rendering::{ItemCache, ItemRenderer};
@ -960,22 +960,7 @@ impl ItemRenderer for QtItemRenderer<'_> {
} }
} }
pub(crate) fn load_image_from_resource( fn shared_image_buffer_to_pixmap(buffer: &SharedImageBuffer) -> Option<qttypes::QPixmap> {
resource: &ImageInner,
source_size: Option<qttypes::QSize>,
image_fit: ImageFit,
) -> Option<qttypes::QPixmap> {
let (is_path, data, format) = match resource {
ImageInner::None => return None,
ImageInner::AbsoluteFilePath(path) => {
(true, qttypes::QByteArray::from(path.as_str()), Default::default())
}
ImageInner::EmbeddedData { data, format } => (
false,
qttypes::QByteArray::from(data.as_slice()),
qttypes::QByteArray::from(format.as_slice()),
),
ImageInner::EmbeddedImage(buffer) => {
let (format, bytes_per_line, buffer_ptr) = match buffer { let (format, bytes_per_line, buffer_ptr) = match buffer {
SharedImageBuffer::RGBA8(img) => { SharedImageBuffer::RGBA8(img) => {
(qttypes::ImageFormat::RGBA8888, img.stride() * 4, img.as_bytes().as_ptr()) (qttypes::ImageFormat::RGBA8888, img.stride() * 4, img.as_bytes().as_ptr())
@ -996,52 +981,21 @@ pub(crate) fn load_image_from_resource(
return QPixmap::fromImage(img); return QPixmap::fromImage(img);
} }; } };
return Some(pixmap); return Some(pixmap);
} }
pub(crate) fn image_to_pixmap(
image: &ImageInner,
source_size: Option<IntSize>,
) -> Option<qttypes::QPixmap> {
match image {
ImageInner::None => return None,
ImageInner::EmbeddedImage { buffer, .. } => shared_image_buffer_to_pixmap(buffer),
ImageInner::StaticTextures { .. } => todo!(), ImageInner::StaticTextures { .. } => todo!(),
}; ImageInner::Svg(svg) => {
let size_requested = is_svg(resource) && source_size.is_some(); let pixel_buffer = svg.render(source_size.unwrap_or_default()).ok()?;
let source_size = source_size.unwrap_or_default(); shared_image_buffer_to_pixmap(&SharedImageBuffer::RGBA8Premultiplied(pixel_buffer))
debug_assert_eq!(ImageFit::contain as i32, 1);
debug_assert_eq!(ImageFit::cover as i32, 2);
Some(cpp! { unsafe [
data as "QByteArray",
is_path as "bool",
format as "QByteArray",
size_requested as "bool",
source_size as "QSize",
image_fit as "int"] -> qttypes::QPixmap as "QPixmap" {
QImageReader reader;
QBuffer buffer;
if (is_path) {
reader.setFileName(QString::fromUtf8(data));
} else {
buffer.setBuffer(const_cast<QByteArray *>(&data));
reader.setDevice(&buffer);
}
if (!reader.canRead()) {
QString fileName = reader.fileName();
if (!fileName.isEmpty()) {
qWarning("Error loading image \"%s\": %s", QFile::encodeName(fileName).constData(), qPrintable(reader.errorString()));
} else {
qWarning("Error loading image of format %s: %s", format.constData(), qPrintable(reader.errorString()));
}
return QPixmap();
}
if (size_requested) {
if (reader.supportsOption(QImageIOHandler::ScaledSize)) {
auto target_size = source_size;
if (image_fit == 1) { //ImageFit::contain
QSizeF s = reader.size();
target_size = (s * qMin(source_size.width() / s.width(), source_size.height() / s.height())).toSize();
} else if (image_fit == 2) { //ImageFit::cover
QSizeF s = reader.size();
target_size = (s * qMax(source_size.width() / s.width(), source_size.height() / s.height())).toSize();
}
reader.setScaledSize(target_size);
} }
} }
return QPixmap::fromImageReader(&reader);
}})
} }
/// Changes the source or the destination rectangle to respect the image fit /// Changes the source or the destination rectangle to respect the image fit
@ -1083,21 +1037,6 @@ fn adjust_to_image_fit(
}; };
} }
/// Return true if this image is a SVG that is scalable
fn is_svg(resource: &ImageInner) -> bool {
match resource {
ImageInner::None => false,
ImageInner::AbsoluteFilePath(path) => {
path.as_str().ends_with(".svg") || path.as_str().ends_with(".svgz")
}
ImageInner::EmbeddedData { format, .. } => {
format.as_slice() == b"svg" || format.as_slice() == b"svgz"
}
ImageInner::EmbeddedImage { .. } => false,
ImageInner::StaticTextures { .. } => false,
}
}
impl QtItemRenderer<'_> { impl QtItemRenderer<'_> {
fn draw_image_impl( fn draw_image_impl(
&mut self, &mut self,
@ -1128,14 +1067,15 @@ impl QtItemRenderer<'_> {
|| !rect.height.approx_eq(&target_height)) || !rect.height.approx_eq(&target_height))
}); });
let source_size = if !has_source_clipping { let source_size = if !has_source_clipping {
Some(qttypes::QSize { width: target_width as u32, height: target_height as u32 }) Some(IntSize::new(target_width as u32, target_height as u32))
} else { } else {
// Source size & clipping is not implemented yet // Source size & clipping is not implemented yet
None None
}; };
load_image_from_resource((&source_property.get()).into(), source_size, image_fit) image_to_pixmap((&source_property.get()).into(), source_size).map_or_else(
.map_or_else(Default::default, |mut pixmap: qttypes::QPixmap| { Default::default,
|mut pixmap: qttypes::QPixmap| {
let colorize = colorize_property.map_or(Brush::default(), |c| c.get()); let colorize = colorize_property.map_or(Brush::default(), |c| c.get());
if !colorize.is_transparent() { if !colorize.is_transparent() {
let brush: qttypes::QBrush = let brush: qttypes::QBrush =
@ -1147,7 +1087,8 @@ impl QtItemRenderer<'_> {
}); });
} }
pixmap pixmap
}) },
)
}); });
let image_size = pixmap.size(); let image_size = pixmap.size();
@ -1482,15 +1423,9 @@ impl PlatformWindow for QtWindow {
let background: u32 = window_item.background().as_argb_encoded(); let background: u32 = window_item.background().as_argb_encoded();
match (&window_item.icon()).into() { match (&window_item.icon()).into() {
&ImageInner::AbsoluteFilePath(ref path) => {
let icon_name: qttypes::QString = path.as_str().into();
cpp! {unsafe [widget_ptr as "QWidget*", icon_name as "QString"] {
widget_ptr->setWindowIcon(QIcon(icon_name));
}};
}
&ImageInner::None => (), &ImageInner::None => (),
r => { r => {
if let Some(pixmap) = load_image_from_resource(r, None, ImageFit::contain) { if let Some(pixmap) = image_to_pixmap(r, None) {
cpp! {unsafe [widget_ptr as "QWidget*", pixmap as "QPixmap"] { cpp! {unsafe [widget_ptr as "QWidget*", pixmap as "QPixmap"] {
widget_ptr->setWindowIcon(QIcon(pixmap)); widget_ptr->setWindowIcon(QIcon(pixmap));
}}; }};

View file

@ -7,11 +7,8 @@
use i_slint_core::api::euclid; use i_slint_core::api::euclid;
use i_slint_core::api::PhysicalPx; use i_slint_core::api::PhysicalPx;
use i_slint_core::component::ComponentRc; use i_slint_core::component::ComponentRc;
use i_slint_core::graphics::{Image, IntSize, Point, Rect, Size}; use i_slint_core::graphics::{Point, Rect, Size};
use i_slint_core::window::{PlatformWindow, Window}; use i_slint_core::window::{PlatformWindow, Window};
use i_slint_core::{ImageInner, StaticTextures};
use image::GenericImageView;
use std::path::Path;
use std::pin::Pin; use std::pin::Pin;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Mutex; use std::sync::Mutex;
@ -58,25 +55,6 @@ impl i_slint_core::backend::Backend for TestingBackend {
// The event will never be invoked // The event will never be invoked
} }
fn image_size(&'static self, image: &Image) -> IntSize {
let inner: &ImageInner = image.into();
match inner {
ImageInner::None => Default::default(),
ImageInner::EmbeddedImage(buffer) => buffer.size(),
ImageInner::AbsoluteFilePath(path) => image::open(Path::new(path.as_str()))
.map(|img| img.dimensions().into())
.unwrap_or_default(),
ImageInner::EmbeddedData { data, format } => image::load_from_memory_with_format(
data.as_slice(),
image::ImageFormat::from_extension(std::str::from_utf8(format.as_slice()).unwrap())
.unwrap(),
)
.map(|img| img.dimensions().into())
.unwrap_or_default(),
ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size,
}
}
fn duration_since_start(&'static self) -> core::time::Duration { fn duration_since_start(&'static self) -> core::time::Duration {
// The slint::testing::mock_elapsed_time updates the animation tick directly // The slint::testing::mock_elapsed_time updates the animation tick directly
core::time::Duration::from_millis(i_slint_core::animations::current_tick().0) core::time::Duration::from_millis(i_slint_core::animations::current_tick().0)

View file

@ -1825,7 +1825,7 @@ fn compile_expression(expr: &Expression, ctx: &EvaluationContext) -> TokenStream
let format = proc_macro2::Literal::byte_string(extension.as_bytes()); let format = proc_macro2::Literal::byte_string(extension.as_bytes());
quote!( quote!(
slint::re_exports::Image::from( slint::re_exports::Image::from(
slint::re_exports::ImageInner::EmbeddedData{ data: #symbol.into(), format: Slice::from_slice(#format) } ( #symbol.into(), Slice::from_slice(#format) )
) )
) )
} }

View file

@ -23,7 +23,7 @@ libm = ["num-traits/libm", "euclid/libm"]
# Allow the viewer to query at runtime information about item types # Allow the viewer to query at runtime information about item types
rtti = [] rtti = []
# Use the standard library # Use the standard library
std = ["euclid/std", "once_cell/std", "scoped-tls-hkt", "lyon_path", "lyon_algorithms", "lyon_geom", "lyon_svg", "instant"] std = ["euclid/std", "once_cell/std", "scoped-tls-hkt", "lyon_path", "lyon_algorithms", "lyon_geom", "lyon_svg", "instant", "image-decoders", "svg"]
# Unsafe feature meaning that there is only one core running and all thread_local are static. # 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 # You can only enable this feature if you are sure that any API of this crate is only called
# from a single core, and not in a interrupt or signal handler. # from a single core, and not in a interrupt or signal handler.
@ -36,6 +36,9 @@ swrenderer = ["integer-sqrt", "text_layout"]
unicode = ["unicode-script", "unicode-linebreak"] unicode = ["unicode-script", "unicode-linebreak"]
image-decoders = ["image", "by_address"]
svg = ["resvg", "usvg", "tiny-skia"]
default = ["std", "text_layout", "unicode"] default = ["std", "text_layout", "unicode"]
[dependencies] [dependencies]
@ -73,9 +76,20 @@ unicode-script = { version = "0.5.3", optional = true }
embedded-graphics = { version = "0.7.1", optional = true } embedded-graphics = { version = "0.7.1", optional = true }
integer-sqrt = { version = "0.1.5", optional = true } integer-sqrt = { version = "0.1.5", optional = true }
image = { version = "0.24.0", optional = true, default-features = false, features = [ "png", "jpeg" ] }
by_address = { version = "1.0.4", optional = true }
resvg = { version= "0.23", optional = true, default-features = false }
usvg = { version= "0.23", optional = true, default-features = false, features = ["text"] }
tiny-skia = { version= "0.6", optional = true, default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
instant = { version = "0.1", features = [ "wasm-bindgen", "now" ] } instant = { version = "0.1", features = [ "wasm-bindgen", "now" ] }
wasm-bindgen = { version = "0.2" } wasm-bindgen = { version = "0.2" }
web-sys = { version = "0.3", features = [ "HtmlImageElement" ] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
usvg = { version= "0.23", optional = true, default-features = false, features = ["text", "memmap-fonts"] }
[dev-dependencies] [dev-dependencies]
slint = { path = "../../api/rs/slint", default-features = false, features = ["std", "compat-0-2-0"] } slint = { path = "../../api/rs/slint", default-features = false, features = ["std", "compat-0-2-0"] }

View file

@ -9,7 +9,6 @@ use alloc::boxed::Box;
use alloc::rc::Rc; use alloc::rc::Rc;
use alloc::string::String; use alloc::string::String;
use crate::graphics::{Image, IntSize};
use crate::window::Window; use crate::window::Window;
#[cfg(feature = "std")] #[cfg(feature = "std")]
@ -67,8 +66,6 @@ pub trait Backend: Send + Sync {
/// Send an user event to from another thread that should be run in the GUI event loop /// Send an user event to from another thread that should be run in the GUI event loop
fn post_event(&'static self, event: Box<dyn FnOnce() + Send>); fn post_event(&'static self, event: Box<dyn FnOnce() + Send>);
fn image_size(&'static self, image: &Image) -> IntSize;
fn duration_since_start(&'static self) -> core::time::Duration { fn duration_since_start(&'static self) -> core::time::Duration {
#[cfg(feature = "std")] #[cfg(feature = "std")]
{ {

View file

@ -6,6 +6,13 @@ use crate::{SharedString, SharedVector};
use super::{IntRect, IntSize}; use super::{IntRect, IntSize};
#[cfg(feature = "image-decoders")]
mod cache;
#[cfg(target_arch = "wasm32")]
mod htmlimage;
#[cfg(feature = "svg")]
mod svg;
/// SharedPixelBuffer is a container for storing image data as pixels. It is /// SharedPixelBuffer is a container for storing image data as pixels. It is
/// internally reference counted and cheap to clone. /// internally reference counted and cheap to clone.
/// ///
@ -245,15 +252,14 @@ pub struct StaticTextures {
pub enum ImageInner { pub enum ImageInner {
/// A resource that does not represent any data. /// A resource that does not represent any data.
None, None,
/// A resource that points to a file in the file system EmbeddedImage {
AbsoluteFilePath(SharedString), path: SharedString, // Should be Option, but can't be because of cbindgen, so empty means none.
/// A image file that is embedded in the program as is. The format is the extension buffer: SharedImageBuffer,
EmbeddedData {
data: Slice<'static, u8>,
format: Slice<'static, u8>,
}, },
EmbeddedImage(SharedImageBuffer), Svg(svg::ParsedSVG),
StaticTextures(&'static StaticTextures), StaticTextures(&'static StaticTextures),
#[cfg(target_arch = "wasm32")]
HTMLImage(HTMLImage),
} }
impl Default for ImageInner { impl Default for ImageInner {
@ -262,19 +268,6 @@ impl Default for ImageInner {
} }
} }
impl ImageInner {
/// Returns true if the image is a scalable vector image.
pub fn is_svg(&self) -> bool {
match self {
ImageInner::AbsoluteFilePath(path) => path.ends_with(".svg") || path.ends_with(".svgz"),
ImageInner::EmbeddedData { format, .. } => {
format.as_slice() == b"svg" || format.as_slice() == b"svgz"
}
_ => false,
}
}
}
impl<'a> From<&'a Image> for &'a ImageInner { impl<'a> From<&'a Image> for &'a ImageInner {
fn from(other: &'a Image) -> Self { fn from(other: &'a Image) -> Self {
&other.0 &other.0
@ -362,26 +355,37 @@ pub struct LoadImageError(());
/// let image = Image::from_rgba8_premultiplied(pixel_buffer); /// let image = Image::from_rgba8_premultiplied(pixel_buffer);
/// ``` /// ```
#[repr(transparent)] #[repr(transparent)]
#[derive(Default, Clone, Debug, PartialEq, derive_more::From)] #[derive(Default, Clone, Debug, PartialEq)]
pub struct Image(ImageInner); pub struct Image(ImageInner);
impl Image { impl Image {
#[cfg(feature = "std")] #[cfg(feature = "image-decoders")]
/// Load an Image from a path to a file containing an image /// Load an Image from a path to a file containing an image
pub fn load_from_path(path: &std::path::Path) -> Result<Self, LoadImageError> { pub fn load_from_path(path: &std::path::Path) -> Result<Self, LoadImageError> {
Ok(Image(ImageInner::AbsoluteFilePath(path.to_str().ok_or(LoadImageError(()))?.into()))) self::cache::IMAGE_CACHE.with(|global_cache| {
let path: SharedString = path.to_str().ok_or(LoadImageError(()))?.into();
let image_inner =
global_cache.borrow_mut().load_image_from_path(&path).ok_or(LoadImageError(()))?;
Ok(Image(image_inner))
})
} }
/// Creates a new Image from the specified shared pixel buffer, where each pixel has three color /// Creates a new Image from the specified shared pixel buffer, where each pixel has three color
/// channels (red, green and blue) encoded as u8. /// channels (red, green and blue) encoded as u8.
pub fn from_rgb8(buffer: SharedPixelBuffer<Rgb8Pixel>) -> Self { pub fn from_rgb8(buffer: SharedPixelBuffer<Rgb8Pixel>) -> Self {
Image(ImageInner::EmbeddedImage(SharedImageBuffer::RGB8(buffer))) Image(ImageInner::EmbeddedImage {
path: Default::default(),
buffer: SharedImageBuffer::RGB8(buffer),
})
} }
/// Creates a new Image from the specified shared pixel buffer, where each pixel has four color /// Creates a new Image from the specified shared pixel buffer, where each pixel has four color
/// channels (red, green, blue and alpha) encoded as u8. /// channels (red, green, blue and alpha) encoded as u8.
pub fn from_rgba8(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self { pub fn from_rgba8(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
Image(ImageInner::EmbeddedImage(SharedImageBuffer::RGBA8(buffer))) Image(ImageInner::EmbeddedImage {
path: Default::default(),
buffer: SharedImageBuffer::RGBA8(buffer),
})
} }
/// Creates a new Image from the specified shared pixel buffer, where each pixel has four color /// Creates a new Image from the specified shared pixel buffer, where each pixel has four color
@ -390,22 +394,20 @@ impl Image {
/// ///
/// Only construct an Image with this function if you know that your pixels are encoded this way. /// Only construct an Image with this function if you know that your pixels are encoded this way.
pub fn from_rgba8_premultiplied(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self { pub fn from_rgba8_premultiplied(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
Image(ImageInner::EmbeddedImage(SharedImageBuffer::RGBA8Premultiplied(buffer))) Image(ImageInner::EmbeddedImage {
path: Default::default(),
buffer: SharedImageBuffer::RGBA8Premultiplied(buffer),
})
} }
/// Returns the size of the Image in pixels. /// Returns the size of the Image in pixels.
pub fn size(&self) -> IntSize { pub fn size(&self) -> IntSize {
match &self.0 { match &self.0 {
ImageInner::None => Default::default(), ImageInner::None => Default::default(),
ImageInner::AbsoluteFilePath(_) | ImageInner::EmbeddedData { .. } => { ImageInner::EmbeddedImage { buffer, .. } => buffer.size(),
match crate::backend::instance() {
Some(backend) => backend.image_size(self),
None => panic!("slint::Image::size() called too early (before a graphics backend was chosen). You need to create a component first."),
}
},
ImageInner::EmbeddedImage(buffer) => buffer.size(),
ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size, ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size,
#[cfg(feature = "svg")]
ImageInner::Svg(svg) => svg.size(),
} }
} }
@ -423,12 +425,32 @@ impl Image {
/// ``` /// ```
pub fn path(&self) -> Option<&std::path::Path> { pub fn path(&self) -> Option<&std::path::Path> {
match &self.0 { match &self.0 {
ImageInner::AbsoluteFilePath(path) => Some(std::path::Path::new(path.as_str())), ImageInner::EmbeddedImage { path, .. } => {
(!path.is_empty()).then(|| std::path::Path::new(path.as_str()))
}
_ => None, _ => None,
} }
} }
} }
// FIXME: this shouldn't be in the public API
#[cfg(feature = "image-decoders")]
impl From<(Slice<'static, u8>, Slice<'static, u8>)> for Image {
fn from((data, format): (Slice<'static, u8>, Slice<'static, u8>)) -> Self {
self::cache::IMAGE_CACHE.with(|global_cache| {
let image_inner = global_cache
.borrow_mut()
.load_image_from_embedded_data(data, format)
.unwrap_or_else(|| {
panic!(
"internal error: embedded image data is not supported by run-time library",
)
});
Image(image_inner)
})
}
}
#[test] #[test]
fn test_image_size_from_buffer_without_backend() { fn test_image_size_from_buffer_without_backend() {
{ {
@ -466,6 +488,14 @@ pub(crate) mod ffi {
b: u8, b: u8,
} }
#[no_mangle]
pub unsafe extern "C" fn slint_image_load_from_path(path: &SharedString, image: *mut Image) {
std::ptr::write(
image,
Image::load_from_path(std::path::Path::new(path.as_str())).unwrap_or(Image::default()),
)
}
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn slint_image_size(image: &Image) -> IntSize { pub unsafe extern "C" fn slint_image_size(image: &Image) -> IntSize {
image.size() image.size()
@ -474,7 +504,7 @@ pub(crate) mod ffi {
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn slint_image_path(image: &Image) -> Option<&SharedString> { pub unsafe extern "C" fn slint_image_path(image: &Image) -> Option<&SharedString> {
match &image.0 { match &image.0 {
ImageInner::AbsoluteFilePath(path) => Some(&path), ImageInner::EmbeddedImage { path, .. } => (!path.is_empty()).then(|| path),
_ => None, _ => None,
} }
} }

View file

@ -0,0 +1,136 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
use std::collections::HashMap;
use super::{ImageInner, SharedImageBuffer, SharedPixelBuffer};
use crate::{slice::Slice, SharedString};
#[derive(PartialEq, Eq, Hash, Debug, derive_more::From)]
pub enum ImageCacheKey {
Path(SharedString),
EmbeddedData(by_address::ByAddress<&'static [u8]>),
}
// Cache used to avoid repeatedly decoding images from disk.
#[derive(Default)]
pub(crate) struct ImageCache(HashMap<ImageCacheKey, ImageInner>);
thread_local!(pub(crate) static IMAGE_CACHE: core::cell::RefCell<ImageCache> = Default::default());
impl ImageCache {
// Look up the given image cache key in the image cache and upgrade the weak reference to a strong one if found,
// otherwise a new image is created/loaded from the given callback.
fn lookup_image_in_cache_or_create(
&mut self,
cache_key: ImageCacheKey,
image_create_fn: impl Fn() -> Option<ImageInner>,
) -> Option<ImageInner> {
Some(match self.0.entry(cache_key) {
std::collections::hash_map::Entry::Occupied(existing_entry) => {
existing_entry.get().clone()
}
std::collections::hash_map::Entry::Vacant(vacant_entry) => {
let new_image = image_create_fn()?;
vacant_entry.insert(new_image.clone());
new_image
}
})
}
pub(crate) fn load_image_from_path(&mut self, path: &SharedString) -> Option<ImageInner> {
if path.is_empty() {
return None;
}
let cache_key = ImageCacheKey::from(path.clone());
self.lookup_image_in_cache_or_create(cache_key, || {
if cfg!(feature = "svg") {
if path.ends_with(".svg") || path.ends_with(".svgz") {
return Some(ImageInner::Svg(
super::svg::load_from_path(std::path::Path::new(&path.as_str()))
.map_or_else(
|err| {
eprintln!("Error loading SVG from {}: {}", &path, err);
None
},
Some,
)?,
));
}
}
image::open(std::path::Path::new(&path.as_str())).map_or_else(
|decode_err| {
eprintln!("Error loading image from {}: {}", &path, decode_err);
None
},
|image| {
Some(ImageInner::EmbeddedImage {
path: path.clone(),
buffer: dynamic_image_to_shared_image_buffer(image),
})
},
)
})
}
pub(crate) fn load_image_from_embedded_data(
&mut self,
data: Slice<'static, u8>,
format: Slice<'static, u8>,
) -> Option<ImageInner> {
let cache_key = ImageCacheKey::from(by_address::ByAddress(data.as_slice()));
self.lookup_image_in_cache_or_create(cache_key, || {
#[cfg(feature = "svg")]
if format.as_slice() == b"svg" || format.as_slice() == b"svgz" {
return Some(ImageInner::Svg(
super::svg::load_from_data(data.as_slice()).map_or_else(
|svg_err| {
eprintln!("Error loading SVG: {}", svg_err);
None
},
Some,
)?,
));
}
let format = std::str::from_utf8(format.as_slice())
.ok()
.and_then(image::ImageFormat::from_extension);
let maybe_image = if let Some(format) = format {
image::load_from_memory_with_format(data.as_slice(), format)
} else {
image::load_from_memory(data.as_slice())
};
match maybe_image {
Ok(image) => Some(ImageInner::EmbeddedImage {
path: Default::default(),
buffer: dynamic_image_to_shared_image_buffer(image),
}),
Err(decode_err) => {
eprintln!("Error decoding embedded image: {}", decode_err);
None
}
}
})
}
}
fn dynamic_image_to_shared_image_buffer(dynamic_image: image::DynamicImage) -> SharedImageBuffer {
if dynamic_image.color().has_alpha() {
let rgba8image = dynamic_image.to_rgba8();
SharedImageBuffer::RGBA8(SharedPixelBuffer::clone_from_slice(
rgba8image.as_raw(),
rgba8image.width(),
rgba8image.height(),
))
} else {
let rgb8image = dynamic_image.to_rgb8();
SharedImageBuffer::RGB8(SharedPixelBuffer::clone_from_slice(
rgb8image.as_raw(),
rgb8image.width(),
rgb8image.height(),
))
}
}

View file

@ -0,0 +1,71 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
use alloc::rc::Rc;
use crate::graphics::IntSize;
use crate::Property;
pub struct HTMLImage {
pub dom_element: web_sys::HtmlImageElement,
/// If present, this boolean property indicates whether the image has been uploaded yet or
/// if that operation is still pending. If not present, then the image *is* available. This is
/// used for remote HTML image loading and the property will be used to correctly track dependencies
/// to graphics items that query for the size.
image_load_pending: core::pin::Pin<Rc<Property<bool>>>,
}
impl HTMLImage {
fn new(url: &str) -> Self {
let dom_element = web_sys::HtmlImageElement::new().unwrap();
let image_load_pending = Rc::pin(Property::new(true));
dom_element.set_cross_origin(Some("anonymous"));
dom_element.set_onload(Some(
&wasm_bindgen::closure::Closure::once_into_js({
let image_load_pending = image_load_pending.clone();
move || {
image_load_pending.as_ref().set(false);
// As you can paint on a HTML canvas at any point in time, request_redraw()
// on a winit window only queues an additional internal event, that'll be
// be dispatched as the next event. We are however not in an event loop
// call, so we also need to wake up the event loop and redraw then.
/*
crate::event_loop::GLOBAL_PROXY.with(|global_proxy| {
let mut maybe_proxy = global_proxy.borrow_mut();
let proxy = maybe_proxy.get_or_insert_with(Default::default);
// Calling send_event is usually done by winit at the bottom of the stack,
// in event handlers, and thus winit might decide to process the event
// immediately within that stack.
// To prevent re-entrancy issues that might happen by getting the application
// event processed on top of the current stack, set winit in Poll mode so that
// events are queued and process on top of a clean stack during a requested animation
// frame a few moments later.
// This also allows batching multiple post_event calls and redraw their state changes
// all at once.
proxy.send_event(crate::event_loop::CustomEvent::RedrawAllWindows);
});
*/
}
})
.into(),
));
dom_element.set_src(&url);
Self { dom_element, image_load_pending }
}
pub fn size(&self) -> Option<IntSize> {
match self.image_load_pending.as_ref().get() {
true => None,
false => Some(IntSize::new(self.dom_element.width(), self.dom_element.height())),
}
}
pub fn source(&self) -> String {
self.dom_element.src()
}
}

View file

@ -0,0 +1,86 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
#![cfg(feature = "svg")]
use alloc::rc::Rc;
use super::SharedPixelBuffer;
#[derive(Clone)]
pub struct ParsedSVG(Rc<usvg::Tree>);
impl core::fmt::Debug for ParsedSVG {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("ParsedSVG").finish()
}
}
impl PartialEq for ParsedSVG {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}
impl ParsedSVG {
pub fn size(&self) -> crate::graphics::IntSize {
let size = self.0.svg_node().size.to_screen_size();
[size.width(), size.height()].into()
}
/// Renders the SVG with the specified size.
///
/// NOTE: returned Rgba8Pixel buffer has alpha pre-multiplied
pub fn render(
&self,
size: euclid::default::Size2D<u32>,
) -> Result<SharedPixelBuffer<super::Rgba8Pixel>, usvg::Error> {
let tree = &*self.0;
// resvg doesn't support scaling to width/height, just fit to width.
// FIXME: the fit should actually depends on the image-fit property
let fit = usvg::FitTo::Width(size.width);
let size =
fit.fit_to(tree.svg_node().size.to_screen_size()).ok_or(usvg::Error::InvalidSize)?;
let mut buffer = SharedPixelBuffer::new(size.width(), size.height());
let skia_buffer =
tiny_skia::PixmapMut::from_bytes(buffer.make_mut_bytes(), size.width(), size.height())
.ok_or(usvg::Error::InvalidSize)?;
resvg::render(tree, fit, Default::default(), skia_buffer)
.ok_or(usvg::Error::InvalidSize)?;
Ok(buffer)
}
}
fn with_svg_options<T>(callback: impl FnOnce(usvg::OptionsRef<'_>) -> T) -> T {
// TODO: When the font db cache is a feature in corelib, use it:
/*
crate::fonts::FONT_CACHE.with(|cache| {
let options = usvg::Options::default();
let mut options_ref = options.to_ref();
let cache = cache.borrow();
options_ref.fontdb = &cache.available_fonts;
callback(options_ref)
})
*/
let options = usvg::Options::default();
let options_ref = options.to_ref();
callback(options_ref)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn load_from_path(path: &std::path::Path) -> Result<ParsedSVG, std::io::Error> {
let svg_data = std::fs::read(path)?;
with_svg_options(|options| {
usvg::Tree::from_data(&svg_data, &options)
.map(|svg| ParsedSVG(svg.into()))
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
})
}
pub fn load_from_data(slice: &[u8]) -> Result<ParsedSVG, usvg::Error> {
with_svg_options(|options| {
usvg::Tree::from_data(slice, &options).map(|svg| ParsedSVG(svg.into()))
})
}

View file

@ -618,10 +618,9 @@ impl<T: ProcessScene> SceneBuilder<T> {
let image_inner: &ImageInner = source.into(); let image_inner: &ImageInner = source.into();
match image_inner { match image_inner {
ImageInner::None => (), ImageInner::None => (),
ImageInner::AbsoluteFilePath(_) | ImageInner::EmbeddedData { .. } => { ImageInner::EmbeddedImage { .. } => todo!(),
unimplemented!() #[cfg(feature = "svg")]
} ImageInner::Svg { .. } => todo!(),
ImageInner::EmbeddedImage(_) => todo!(),
ImageInner::StaticTextures(StaticTextures { data, textures, .. }) => { ImageInner::StaticTextures(StaticTextures { data, textures, .. }) => {
let size: euclid::default::Size2D<u32> = source_rect.size.cast(); let size: euclid::default::Size2D<u32> = source_rect.size.cast();
let phys_size = geom.size_length().cast() * self.scale_factor; let phys_size = geom.size_length().cast() * self.scale_factor;

View file

@ -542,10 +542,10 @@ pub fn eval_expression(expression: &Expression, local_context: &mut EvalLocalCon
let virtual_file_extension = std::path::Path::new(static_path).extension().unwrap().to_str().unwrap(); let virtual_file_extension = std::path::Path::new(static_path).extension().unwrap().to_str().unwrap();
debug_assert_eq!(virtual_file_extension, extension); debug_assert_eq!(virtual_file_extension, extension);
Ok(corelib::graphics::Image::from( Ok(corelib::graphics::Image::from(
corelib::graphics::ImageInner::EmbeddedData { (
data: corelib::slice::Slice::from_slice(static_data), corelib::slice::Slice::from_slice(static_data),
format: corelib::slice::Slice::from_slice(virtual_file_extension.as_bytes()) corelib::slice::Slice::from_slice(virtual_file_extension.as_bytes())
} )
)) ))
} else { } else {
corelib::debug_log!("Cannot embed images from disk {}", path); corelib::debug_log!("Cannot embed images from disk {}", path);