1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4/*!
5This module contains image and caching related types for the run-time library.
6*/
7
8use super::{CachedPath, Image, ImageCacheKey, ImageInner, SharedImageBuffer, SharedPixelBuffer};
9use crate::{slice::Slice, SharedString};
10
11struct ImageWeightInBytes;
12
13impl clru::WeightScale<ImageCacheKey, ImageInner> for ImageWeightInBytes {
14 fn weight(&self, _key: &ImageCacheKey, value: &ImageInner) -> usize {
15 match value {
16 ImageInner::None => 0,
17 ImageInner::EmbeddedImage { buffer: &SharedImageBuffer, .. } => match buffer {
18 SharedImageBuffer::RGB8(pixels: &SharedPixelBuffer>) => pixels.as_bytes().len(),
19 SharedImageBuffer::RGBA8(pixels: &SharedPixelBuffer>) => pixels.as_bytes().len(),
20 SharedImageBuffer::RGBA8Premultiplied(pixels: &SharedPixelBuffer>) => pixels.as_bytes().len(),
21 },
22 #[cfg(feature = "svg")]
23 ImageInner::Svg(_) => 512, // Don't know how to measure the size of the parsed SVG tree...
24 #[cfg(target_arch = "wasm32")]
25 ImageInner::HTMLImage(_) => 512, // Something... the web browser maintainers its own cache. The purpose of this cache is to reduce the amount of DOM elements.
26 ImageInner::StaticTextures(_) => 0,
27 ImageInner::BackendStorage(x: &VRc) => vtable::VRc::borrow(this:x).size().area() as usize,
28 #[cfg(not(target_arch = "wasm32"))]
29 ImageInner::BorrowedOpenGLTexture(..) => 0, // Assume storage in GPU memory
30 ImageInner::NineSlice(nine: &VRc) => self.weight(_key, &nine.0),
31 }
32 }
33}
34
35/// Cache used to avoid repeatedly decoding images from disk.
36pub(crate) struct ImageCache(
37 clru::CLruCache<
38 ImageCacheKey,
39 ImageInner,
40 std::collections::hash_map::RandomState,
41 ImageWeightInBytes,
42 >,
43);
44
45crate::thread_local!(pub(crate) static IMAGE_CACHE: core::cell::RefCell<ImageCache> =
46 core::cell::RefCell::new(
47 ImageCache(
48 clru::CLruCache::with_config(
49 clru::CLruCacheConfig::new(core::num::NonZeroUsize::new(5 * 1024 * 1024).unwrap())
50 .with_scale(ImageWeightInBytes)
51 )
52 )
53 )
54);
55
56impl ImageCache {
57 // Look up the given image cache key in the image cache and upgrade the weak reference to a strong one if found,
58 // otherwise a new image is created/loaded from the given callback.
59 fn lookup_image_in_cache_or_create(
60 &mut self,
61 cache_key: ImageCacheKey,
62 image_create_fn: impl Fn(ImageCacheKey) -> Option<ImageInner>,
63 ) -> Option<Image> {
64 Some(Image(if let Some(entry) = self.0.get(&cache_key) {
65 entry.clone()
66 } else {
67 let new_image = image_create_fn(cache_key.clone())?;
68 self.0.put_with_weight(cache_key, new_image.clone()).ok();
69 new_image
70 }))
71 }
72
73 pub(crate) fn load_image_from_path(&mut self, path: &SharedString) -> Option<Image> {
74 if path.is_empty() {
75 return None;
76 }
77 let cache_key = ImageCacheKey::Path(CachedPath::new(path.as_str()));
78 #[cfg(target_arch = "wasm32")]
79 return self.lookup_image_in_cache_or_create(cache_key, |_| {
80 return Some(ImageInner::HTMLImage(vtable::VRc::new(
81 super::htmlimage::HTMLImage::new(&path),
82 )));
83 });
84 #[cfg(not(target_arch = "wasm32"))]
85 return self.lookup_image_in_cache_or_create(cache_key, |cache_key| {
86 if cfg!(feature = "svg") && (path.ends_with(".svg") || path.ends_with(".svgz")) {
87 return Some(ImageInner::Svg(vtable::VRc::new(
88 super::svg::load_from_path(path, cache_key).map_or_else(
89 |err| {
90 crate::debug_log!("Error loading SVG from {}: {}", &path, err);
91 None
92 },
93 Some,
94 )?,
95 )));
96 }
97
98 image::open(std::path::Path::new(&path.as_str())).map_or_else(
99 |decode_err| {
100 crate::debug_log!("Error loading image from {}: {}", &path, decode_err);
101 None
102 },
103 |image| {
104 Some(ImageInner::EmbeddedImage {
105 cache_key,
106 buffer: dynamic_image_to_shared_image_buffer(image),
107 })
108 },
109 )
110 });
111 }
112
113 pub(crate) fn load_image_from_embedded_data(
114 &mut self,
115 data: Slice<'static, u8>,
116 format: Slice<'_, u8>,
117 ) -> Option<Image> {
118 let cache_key = ImageCacheKey::from_embedded_image_data(data.as_slice());
119 self.lookup_image_in_cache_or_create(cache_key, |cache_key| {
120 #[cfg(feature = "svg")]
121 if format.as_slice() == b"svg" || format.as_slice() == b"svgz" {
122 return Some(ImageInner::Svg(vtable::VRc::new(
123 super::svg::load_from_data(data.as_slice(), cache_key).map_or_else(
124 |svg_err| {
125 crate::debug_log!("Error loading SVG: {}", svg_err);
126 None
127 },
128 Some,
129 )?,
130 )));
131 }
132
133 let format = std::str::from_utf8(format.as_slice())
134 .ok()
135 .and_then(image::ImageFormat::from_extension);
136 let maybe_image = if let Some(format) = format {
137 image::load_from_memory_with_format(data.as_slice(), format)
138 } else {
139 image::load_from_memory(data.as_slice())
140 };
141
142 match maybe_image {
143 Ok(image) => Some(ImageInner::EmbeddedImage {
144 cache_key,
145 buffer: dynamic_image_to_shared_image_buffer(image),
146 }),
147 Err(decode_err) => {
148 crate::debug_log!("Error decoding embedded image: {}", decode_err);
149 None
150 }
151 }
152 })
153 }
154}
155
156fn dynamic_image_to_shared_image_buffer(dynamic_image: image::DynamicImage) -> SharedImageBuffer {
157 if dynamic_image.color().has_alpha() {
158 let rgba8image: ImageBuffer, Vec<…>> = dynamic_image.to_rgba8();
159 SharedImageBuffer::RGBA8(SharedPixelBuffer::clone_from_slice(
160 pixel_slice:rgba8image.as_raw(),
161 rgba8image.width(),
162 rgba8image.height(),
163 ))
164 } else {
165 let rgb8image: ImageBuffer, Vec<…>> = dynamic_image.to_rgb8();
166 SharedImageBuffer::RGB8(SharedPixelBuffer::clone_from_slice(
167 pixel_slice:rgb8image.as_raw(),
168 rgb8image.width(),
169 rgb8image.height(),
170 ))
171 }
172}
173
174/// Replace the cached image key with the given value
175pub fn replace_cached_image(key: ImageCacheKey, value: ImageInner) {
176 if key == ImageCacheKey::Invalid {
177 return;
178 }
179 let _ =
180 IMAGE_CACHE.with(|global_cache: &RefCell| global_cache.borrow_mut().0.put_with_weight(key, value));
181}
182
183#[cfg(all(test, feature = "std"))]
184mod tests {
185 use crate::graphics::Rgba8Pixel;
186
187 #[test]
188 fn test_path_cache_invalidation() {
189 let temp_dir = tempfile::tempdir().unwrap();
190
191 let test_path = [temp_dir.path(), std::path::Path::new("testfile.png")]
192 .iter()
193 .collect::<std::path::PathBuf>();
194
195 let red_image = image::RgbImage::from_pixel(10, 10, image::Rgb([255, 0, 0]));
196 red_image.save(&test_path).unwrap();
197 let red_slint_image = crate::graphics::Image::load_from_path(&test_path).unwrap();
198 let buffer = red_slint_image.to_rgba8().unwrap();
199 assert!(buffer
200 .as_slice()
201 .iter()
202 .all(|pixel| *pixel == Rgba8Pixel { r: 255, g: 0, b: 0, a: 255 }));
203
204 let green_image = image::RgbImage::from_pixel(10, 10, image::Rgb([0, 255, 0]));
205
206 std::thread::sleep(std::time::Duration::from_secs(2));
207
208 green_image.save(&test_path).unwrap();
209
210 /* Can't use this until we use Rust 1.78
211 let mod_time = std::fs::metadata(&test_path).unwrap().modified().unwrap();
212 std::fs::File::options()
213 .write(true)
214 .open(&test_path)
215 .unwrap()
216 .set_modified(mod_time.checked_add(std::time::Duration::from_secs(2)).unwrap())
217 .unwrap();
218 */
219
220 let green_slint_image = crate::graphics::Image::load_from_path(&test_path).unwrap();
221 let buffer = green_slint_image.to_rgba8().unwrap();
222 assert!(buffer
223 .as_slice()
224 .iter()
225 .all(|pixel| *pixel == Rgba8Pixel { r: 0, g: 255, b: 0, a: 255 }));
226 }
227}
228

Provided by KDAB

Privacy Policy