i_slint_core/graphics/image/
cache.rs

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, .. } => 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, // 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) => vtable::VRc::borrow(x).size().area() as usize,
28            #[cfg(not(target_arch = "wasm32"))]
29            ImageInner::BorrowedOpenGLTexture(..) => 0, // Assume storage in GPU memory
30            ImageInner::NineSlice(nine) => self.weight(_key, &nine.0),
31            #[cfg(any(feature = "unstable-wgpu-26", feature = "unstable-wgpu-27"))]
32            ImageInner::WGPUTexture(..) => 0, // The texture is imported from the application and will never reside in our cache.
33        }
34    }
35}
36
37/// Cache used to avoid repeatedly decoding images from disk.
38pub(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    // Look up the given image cache key in the image cache and upgrade the weak reference to a strong one if found,
60    // otherwise a new image is created/loaded from the given callback.
61    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
176/// Replace the cached image key with the given value
177pub 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        /* Can't use this until we use Rust 1.78
213        let mod_time = std::fs::metadata(&test_path).unwrap().modified().unwrap();
214        std::fs::File::options()
215            .write(true)
216            .open(&test_path)
217            .unwrap()
218            .set_modified(mod_time.checked_add(std::time::Duration::from_secs(2)).unwrap())
219            .unwrap();
220        */
221
222        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}