Skip to content

Commit

Permalink
Fix repeated loading of an image by path if the file was changed sinc…
Browse files Browse the repository at this point in the history
…e last time

Fixes #6030
  • Loading branch information
tronical committed Sep 4, 2024
1 parent 11e2da1 commit 386710c
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 12 deletions.
1 change: 1 addition & 0 deletions internal/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ serde_json = { workspace = true }
tiny-skia = "0.11.0"
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "time", "net", "io-util"] }
async-compat = { version = "0.2.4" }
tempfile = { version = "3.12.0" }

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(slint_debug_property)", "cfg(cbindgen)", "cfg(slint_int_coord)"] }
46 changes: 36 additions & 10 deletions internal/core/graphics/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,30 @@ pub struct StaticTextures {
pub textures: Slice<'static, StaticTexture>,
}

/// A struct that provides a path as a string as well as the last modification
/// time of the file it points to.
#[derive(PartialEq, Eq, Debug, Hash, Clone)]
#[repr(C)]
pub struct CachedPath {
path: SharedString,
/// SystemTime since UNIX_EPOC as secs
last_modified: u64,
}

impl CachedPath {
#[cfg(feature = "std")]
fn new<P: AsRef<std::path::Path>>(path: P) -> Self {
let path_str = SharedString::from(path.as_ref().to_string_lossy().as_ref());
let timestamp = std::fs::metadata(path)
.and_then(|md| md.modified())
.unwrap_or(std::time::UNIX_EPOCH)
.duration_since(std::time::UNIX_EPOCH)
.map(|t| t.as_secs())
.unwrap_or_default();
Self { path: path_str, last_modified: timestamp }
}
}

/// ImageCacheKey encapsulates the different ways of indexing images in the
/// cache of decoded images.
#[derive(PartialEq, Eq, Debug, Hash, Clone)]
Expand All @@ -286,8 +310,8 @@ pub enum ImageCacheKey {
/// This variant indicates that no image cache key can be created for the image.
/// For example this is the case for programmatically created images.
Invalid = 0,
/// The image is identified by its path on the file system.
Path(SharedString) = 1,
/// The image is identified by its path on the file system and the last modification time stamp.
Path(CachedPath) = 1,
/// The image is identified by a URL.
#[cfg(target_arch = "wasm32")]
URL(SharedString) = 2,
Expand Down Expand Up @@ -826,13 +850,15 @@ impl Image {
/// ```
pub fn path(&self) -> Option<&std::path::Path> {
match &self.0 {
ImageInner::EmbeddedImage { cache_key: ImageCacheKey::Path(path), .. } => {
Some(std::path::Path::new(path.as_str()))
}
ImageInner::EmbeddedImage {
cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
..
} => Some(std::path::Path::new(path.as_str())),
ImageInner::NineSlice(nine) => match &nine.0 {
ImageInner::EmbeddedImage { cache_key: ImageCacheKey::Path(path), .. } => {
Some(std::path::Path::new(path.as_str()))
}
ImageInner::EmbeddedImage {
cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
..
} => Some(std::path::Path::new(path.as_str())),
_ => None,
},
_ => None,
Expand Down Expand Up @@ -1260,12 +1286,12 @@ pub(crate) mod ffi {
pub extern "C" fn slint_image_path(image: &Image) -> Option<&SharedString> {
match &image.0 {
ImageInner::EmbeddedImage { cache_key, .. } => match cache_key {
ImageCacheKey::Path(path) => Some(path),
ImageCacheKey::Path(CachedPath { path, .. }) => Some(path),
_ => None,
},
ImageInner::NineSlice(nine) => match &nine.0 {
ImageInner::EmbeddedImage { cache_key, .. } => match cache_key {
ImageCacheKey::Path(path) => Some(path),
ImageCacheKey::Path(CachedPath { path, .. }) => Some(path),
_ => None,
},
_ => None,
Expand Down
50 changes: 48 additions & 2 deletions internal/core/graphics/image/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
This module contains image and caching related types for the run-time library.
*/

use super::{Image, ImageCacheKey, ImageInner, SharedImageBuffer, SharedPixelBuffer};
use super::{CachedPath, Image, ImageCacheKey, ImageInner, SharedImageBuffer, SharedPixelBuffer};
use crate::{slice::Slice, SharedString};

struct ImageWeightInBytes;
Expand Down Expand Up @@ -74,7 +74,7 @@ impl ImageCache {
if path.is_empty() {
return None;
}
let cache_key = ImageCacheKey::Path(path.clone());
let cache_key = ImageCacheKey::Path(CachedPath::new(path.as_str()));
#[cfg(target_arch = "wasm32")]
return self.lookup_image_in_cache_or_create(cache_key, |_| {
return Some(ImageInner::HTMLImage(vtable::VRc::new(
Expand Down Expand Up @@ -179,3 +179,49 @@ pub fn replace_cached_image(key: ImageCacheKey, value: ImageInner) {
let _ =
IMAGE_CACHE.with(|global_cache| global_cache.borrow_mut().0.put_with_weight(key, value));
}

#[cfg(all(test, feature = "std"))]
mod tests {
use crate::graphics::Rgba8Pixel;

#[test]
fn test_path_cache_invalidation() {
let temp_dir = tempfile::tempdir().unwrap();

let test_path = [temp_dir.path(), std::path::Path::new("testfile.png")]
.iter()
.collect::<std::path::PathBuf>();

let red_image = image::RgbImage::from_pixel(10, 10, image::Rgb([255, 0, 0]));
red_image.save(&test_path).unwrap();
let red_slint_image = crate::graphics::Image::load_from_path(&test_path).unwrap();
let buffer = red_slint_image.to_rgba8().unwrap();
assert!(buffer
.as_slice()
.iter()
.all(|pixel| *pixel == Rgba8Pixel { r: 255, g: 0, b: 0, a: 255 }));

let green_image = image::RgbImage::from_pixel(10, 10, image::Rgb([0, 255, 0]));

std::thread::sleep(std::time::Duration::from_secs(2));

green_image.save(&test_path).unwrap();

/* Can't use this until we use Rust 1.78
let mod_time = std::fs::metadata(&test_path).unwrap().modified().unwrap();
std::fs::File::options()
.write(true)
.open(&test_path)
.unwrap()
.set_modified(mod_time.checked_add(std::time::Duration::from_secs(2)).unwrap())
.unwrap();
*/

let green_slint_image = crate::graphics::Image::load_from_path(&test_path).unwrap();
let buffer = green_slint_image.to_rgba8().unwrap();
assert!(buffer
.as_slice()
.iter()
.all(|pixel| *pixel == Rgba8Pixel { r: 0, g: 255, b: 0, a: 255 }));
}
}

0 comments on commit 386710c

Please sign in to comment.