i_slint_core/graphics/image/
cache.rs1use 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, .. } => match buffer {
18 SharedImageBuffer::RGB8(pixels) => pixels.as_bytes().len(),
19 SharedImageBuffer::RGBA8(pixels) => pixels.as_bytes().len(),
20 SharedImageBuffer::RGBA8Premultiplied(pixels) => pixels.as_bytes().len(),
21 },
22 #[cfg(feature = "svg")]
23 ImageInner::Svg(_) => 512, #[cfg(target_arch = "wasm32")]
25 ImageInner::HTMLImage(_) => 512, ImageInner::StaticTextures(_) => 0,
27 ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).size().area() as usize,
28 #[cfg(not(target_arch = "wasm32"))]
29 ImageInner::BorrowedOpenGLTexture(..) => 0, ImageInner::NineSlice(nine) => self.weight(_key, &nine.0),
31 #[cfg(any(feature = "unstable-wgpu-26", feature = "unstable-wgpu-27"))]
32 ImageInner::WGPUTexture(..) => 0, }
34 }
35}
36
37pub(crate) struct ImageCache(
39 clru::CLruCache<
40 ImageCacheKey,
41 ImageInner,
42 std::collections::hash_map::RandomState,
43 ImageWeightInBytes,
44 >,
45);
46
47crate::thread_local!(pub(crate) static IMAGE_CACHE: core::cell::RefCell<ImageCache> =
48 core::cell::RefCell::new(
49 ImageCache(
50 clru::CLruCache::with_config(
51 clru::CLruCacheConfig::new(core::num::NonZeroUsize::new(5 * 1024 * 1024).unwrap())
52 .with_scale(ImageWeightInBytes)
53 )
54 )
55 )
56);
57
58impl ImageCache {
59 fn lookup_image_in_cache_or_create(
62 &mut self,
63 cache_key: ImageCacheKey,
64 image_create_fn: impl Fn(ImageCacheKey) -> Option<ImageInner>,
65 ) -> Option<Image> {
66 Some(Image(if let Some(entry) = self.0.get(&cache_key) {
67 entry.clone()
68 } else {
69 let new_image = image_create_fn(cache_key.clone())?;
70 self.0.put_with_weight(cache_key, new_image.clone()).ok();
71 new_image
72 }))
73 }
74
75 pub(crate) fn load_image_from_path(&mut self, path: &SharedString) -> Option<Image> {
76 if path.is_empty() {
77 return None;
78 }
79 let cache_key = ImageCacheKey::Path(CachedPath::new(path.as_str()));
80 #[cfg(target_arch = "wasm32")]
81 return self.lookup_image_in_cache_or_create(cache_key, |_| {
82 return Some(ImageInner::HTMLImage(vtable::VRc::new(
83 super::htmlimage::HTMLImage::new(&path),
84 )));
85 });
86 #[cfg(not(target_arch = "wasm32"))]
87 return self.lookup_image_in_cache_or_create(cache_key, |cache_key| {
88 if cfg!(feature = "svg") && (path.ends_with(".svg") || path.ends_with(".svgz")) {
89 return Some(ImageInner::Svg(vtable::VRc::new(
90 super::svg::load_from_path(path, cache_key).map_or_else(
91 |err| {
92 crate::debug_log!("Error loading SVG from {}: {}", &path, err);
93 None
94 },
95 Some,
96 )?,
97 )));
98 }
99
100 image::open(std::path::Path::new(&path.as_str())).map_or_else(
101 |decode_err| {
102 crate::debug_log!("Error loading image from {}: {}", &path, decode_err);
103 None
104 },
105 |image| {
106 Some(ImageInner::EmbeddedImage {
107 cache_key,
108 buffer: dynamic_image_to_shared_image_buffer(image),
109 })
110 },
111 )
112 });
113 }
114
115 pub(crate) fn load_image_from_embedded_data(
116 &mut self,
117 data: Slice<'static, u8>,
118 format: Slice<'_, u8>,
119 ) -> Option<Image> {
120 let cache_key = ImageCacheKey::from_embedded_image_data(data.as_slice());
121 self.lookup_image_in_cache_or_create(cache_key, |cache_key| {
122 #[cfg(feature = "svg")]
123 if format.as_slice() == b"svg" || format.as_slice() == b"svgz" {
124 return Some(ImageInner::Svg(vtable::VRc::new(
125 super::svg::load_from_data(data.as_slice(), cache_key).map_or_else(
126 |svg_err| {
127 crate::debug_log!("Error loading SVG: {}", svg_err);
128 None
129 },
130 Some,
131 )?,
132 )));
133 }
134
135 let format = std::str::from_utf8(format.as_slice())
136 .ok()
137 .and_then(image::ImageFormat::from_extension);
138 let maybe_image = if let Some(format) = format {
139 image::load_from_memory_with_format(data.as_slice(), format)
140 } else {
141 image::load_from_memory(data.as_slice())
142 };
143
144 match maybe_image {
145 Ok(image) => Some(ImageInner::EmbeddedImage {
146 cache_key,
147 buffer: dynamic_image_to_shared_image_buffer(image),
148 }),
149 Err(decode_err) => {
150 crate::debug_log!("Error decoding embedded image: {}", decode_err);
151 None
152 }
153 }
154 })
155 }
156}
157
158fn dynamic_image_to_shared_image_buffer(dynamic_image: image::DynamicImage) -> SharedImageBuffer {
159 if dynamic_image.color().has_alpha() {
160 let rgba8image = dynamic_image.to_rgba8();
161 SharedImageBuffer::RGBA8(SharedPixelBuffer::clone_from_slice(
162 rgba8image.as_raw(),
163 rgba8image.width(),
164 rgba8image.height(),
165 ))
166 } else {
167 let rgb8image = dynamic_image.to_rgb8();
168 SharedImageBuffer::RGB8(SharedPixelBuffer::clone_from_slice(
169 rgb8image.as_raw(),
170 rgb8image.width(),
171 rgb8image.height(),
172 ))
173 }
174}
175
176pub fn replace_cached_image(key: ImageCacheKey, value: ImageInner) {
178 if key == ImageCacheKey::Invalid {
179 return;
180 }
181 let _ =
182 IMAGE_CACHE.with(|global_cache| global_cache.borrow_mut().0.put_with_weight(key, value));
183}
184
185#[cfg(all(test, feature = "std"))]
186mod tests {
187 use crate::graphics::Rgba8Pixel;
188
189 #[test]
190 fn test_path_cache_invalidation() {
191 let temp_dir = tempfile::tempdir().unwrap();
192
193 let test_path = [temp_dir.path(), std::path::Path::new("testfile.png")]
194 .iter()
195 .collect::<std::path::PathBuf>();
196
197 let red_image = image::RgbImage::from_pixel(10, 10, image::Rgb([255, 0, 0]));
198 red_image.save(&test_path).unwrap();
199 let red_slint_image = crate::graphics::Image::load_from_path(&test_path).unwrap();
200 let buffer = red_slint_image.to_rgba8().unwrap();
201 assert!(buffer
202 .as_slice()
203 .iter()
204 .all(|pixel| *pixel == Rgba8Pixel { r: 255, g: 0, b: 0, a: 255 }));
205
206 let green_image = image::RgbImage::from_pixel(10, 10, image::Rgb([0, 255, 0]));
207
208 std::thread::sleep(std::time::Duration::from_secs(2));
209
210 green_image.save(&test_path).unwrap();
211
212 let green_slint_image = crate::graphics::Image::load_from_path(&test_path).unwrap();
223 let buffer = green_slint_image.to_rgba8().unwrap();
224 assert!(buffer
225 .as_slice()
226 .iter()
227 .all(|pixel| *pixel == Rgba8Pixel { r: 0, g: 255, b: 0, a: 255 }));
228 }
229}