| 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 | /*! |
| 5 | This module contains image and caching related types for the run-time library. |
| 6 | */ |
| 7 | |
| 8 | use super::{CachedPath, Image, ImageCacheKey, ImageInner, SharedImageBuffer, SharedPixelBuffer}; |
| 9 | use crate::{slice::Slice, SharedString}; |
| 10 | |
| 11 | struct ImageWeightInBytes; |
| 12 | |
| 13 | impl 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. |
| 36 | pub(crate) struct ImageCache( |
| 37 | clru::CLruCache< |
| 38 | ImageCacheKey, |
| 39 | ImageInner, |
| 40 | std::collections::hash_map::RandomState, |
| 41 | ImageWeightInBytes, |
| 42 | >, |
| 43 | ); |
| 44 | |
| 45 | crate::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 | |
| 56 | impl 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 | |
| 156 | fn 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 |
| 175 | pub 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" ))] |
| 184 | mod 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 | |