1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | /*! |
5 | This module contains image and caching related types for the run-time library. |
6 | */ |
7 | |
8 | use super::{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 | 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(path.clone()); |
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 | eprintln!("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 | eprintln!("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 | eprintln!("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 | eprintln!("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 | |