use anyhow::{anyhow, Result}; use core::fmt; use itertools::Itertools; use std::{ collections::HashMap, error, hash::{DefaultHasher, Hash, Hasher}, rc::Rc, sync::{Arc, LazyLock}, }; use strum_macros::EnumIter; use crate::{ assets::asset_cache::{Asset, AssetCache, AssetSource, AssetState}, util::parse_u32, Entity, SingletonEntity, }; use image::{ codecs::{gif::GifDecoder, webp::WebPDecoder}, imageops::FilterType, AnimationDecoder, DynamicImage, Frame, ImageBuffer, ImageFormat, }; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; use pathfinder_geometry::vector::Vector2I; use resvg::{ tiny_skia::{self, IntSize}, usvg, }; const MIN_REFRESH_DELAY_MS: u32 = 50; static SVG_FONT_DB: LazyLock> = LazyLock::new(|| { let mut fontdb = usvg::fontdb::Database::new(); fontdb.load_system_fonts(); Arc::new(fontdb) }); pub fn prewarm_svg_font_db() { LazyLock::force(&SVG_FONT_DB); } #[derive(EnumIter, Debug)] pub enum CustomImageFormat { Rgb, Rgba, } impl CustomImageFormat { fn create_tag(&self) -> String { match self { CustomImageFormat::Rgb => "rgb".into(), CustomImageFormat::Rgba => "rgba".into(), } } } #[derive(Debug, Clone)] enum CustomHeaderParsingError { ExpectedDataSizeMismatch { expected_bytes: usize, actual_bytes: usize, }, InvalidCustomHeaderParam { param_name: String, value: String, }, MissingCustomHeaderParam { param_name: String, }, MissingHeaderIdentifier, } impl error::Error for CustomHeaderParsingError {} impl fmt::Display for CustomHeaderParsingError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::ExpectedDataSizeMismatch { expected_bytes, actual_bytes, } => write!( f, "The custom image's expected bytes ({expected_bytes}) did not match the actual number of bytes ({actual_bytes})." ), Self::InvalidCustomHeaderParam { param_name, value } => write!( f, "Custom header had field {param_name} with an invalid value of {value}" ), Self::MissingCustomHeaderParam { param_name } => { write!(f, "Custom header had {param_name} field missing") } Self::MissingHeaderIdentifier => { write!(f, "Image did not contain the 'warp-img:' prefix.") } } } } #[derive(Debug, Clone)] pub enum CustomHeaderCreationError { ExpectedDataSizeMismatch { expected_bytes: usize, actual_bytes: usize, }, } #[derive(Debug)] pub struct CustomImageHeader { pub width: u32, pub height: u32, pub image_format: CustomImageFormat, } impl CustomImageHeader { pub fn create_header(&self) -> String { format!( "warp-img:{}:{}:{}:", self.image_format.create_tag(), self.width, self.height ) } pub fn prepend_custom_header( mut data: Vec, width: u32, height: u32, image_format: CustomImageFormat, ) -> Result, CustomHeaderCreationError> { let bytes_per_pixel = match image_format { CustomImageFormat::Rgb => 3, CustomImageFormat::Rgba => 4, }; let expected_byte_count = (bytes_per_pixel * width * height) as usize; if expected_byte_count != data.len() { return Err(CustomHeaderCreationError::ExpectedDataSizeMismatch { expected_bytes: expected_byte_count, actual_bytes: data.len(), }); } let custom_header = CustomImageHeader { width, height, image_format, }; data.splice( 0..0, custom_header.create_header().as_bytes().iter().copied(), ); Ok(data) } fn try_from_bytes(data: &[u8]) -> Result<(CustomImageHeader, &[u8]), CustomHeaderParsingError> { if !data.starts_with(b"warp-img:") { return Err(CustomHeaderParsingError::MissingHeaderIdentifier); } let data = match data .iter() .position(|&byte| byte == b':') .map(|position| &data[position + 1..]) { Some(data) => data, None => return Err(CustomHeaderParsingError::MissingHeaderIdentifier), }; let (image_type, data) = match data .iter() .position(|&byte| byte == b':') .map(|position| (&data[..position], &data[position + 1..])) { Some((image_type, data)) => (image_type, data), None => { return Err(CustomHeaderParsingError::MissingCustomHeaderParam { param_name: "image_type".to_string(), }); } }; let image_type = match image_type { b"rgb" => CustomImageFormat::Rgb, b"rgba" => CustomImageFormat::Rgba, _ => { return Err(CustomHeaderParsingError::InvalidCustomHeaderParam { param_name: "image_type".to_string(), value: std::str::from_utf8(image_type) .unwrap_or("Unable to parse utf8") .to_string(), }); } }; let (width, data) = match data .iter() .position(|&byte| byte == b':') .map(|position| (&data[..position], &data[position + 1..])) { Some((width, data)) => (width, data), None => { return Err(CustomHeaderParsingError::MissingCustomHeaderParam { param_name: "width".to_string(), }); } }; let width = match parse_u32(width) { Some(width) => width, None => { return Err(CustomHeaderParsingError::InvalidCustomHeaderParam { param_name: "width".to_string(), value: std::str::from_utf8(width) .unwrap_or("Unable to parse utf8") .to_string(), }); } }; let (height, data) = match data .iter() .position(|&byte| byte == b':') .map(|position| (&data[..position], &data[position + 1..])) { Some((height, data)) => (height, data), None => { return Err(CustomHeaderParsingError::MissingCustomHeaderParam { param_name: "height".to_string(), }); } }; let height = match parse_u32(height) { Some(height) => height, None => { return Err(CustomHeaderParsingError::InvalidCustomHeaderParam { param_name: "height".to_string(), value: std::str::from_utf8(height) .unwrap_or("Unable to parse utf8") .to_string(), }); } }; let bytes_per_pixel = match image_type { CustomImageFormat::Rgb => 3, CustomImageFormat::Rgba => 4, }; let expected_byte_count = (bytes_per_pixel * width * height) as usize; if expected_byte_count != data.len() { return Err(CustomHeaderParsingError::ExpectedDataSizeMismatch { expected_bytes: expected_byte_count, actual_bytes: data.len(), }); } Ok(( CustomImageHeader { width, height, image_format: image_type, }, data, )) } } impl Asset for ImageType { fn try_from_bytes(data: &[u8]) -> anyhow::Result { // SVGs are not handled by the guess_format helper function, so we have to manually check // if it's an SVG ourselves. if data.first() == Some(&b'<') { let options = usvg::Options { fontdb: SVG_FONT_DB.clone(), ..Default::default() }; let svg = Rc::new(usvg::Tree::from_data(data, &options)?); return Ok(ImageType::Svg { svg }); } if data.starts_with(b"warp-img:") { let (custom_warp_header, data) = match CustomImageHeader::try_from_bytes(data) { Ok((custom_warp_header, data)) => (custom_warp_header, data), Err(err) => return Err(anyhow!(err.to_string())), }; let data = data.into(); let Some(img) = (match custom_warp_header.image_format { CustomImageFormat::Rgb => { let dynamic_image = ImageBuffer::from_raw( custom_warp_header.width, custom_warp_header.height, data, ) .map(DynamicImage::ImageRgb8); dynamic_image.map(|dynamic_image| dynamic_image.into_rgba8()) } CustomImageFormat::Rgba => { let dynamic_image = ImageBuffer::from_raw( custom_warp_header.width, custom_warp_header.height, data, ) .map(DynamicImage::ImageRgba8); dynamic_image.map(|dynamic_image| dynamic_image.into_rgba8()) } }) else { return Err(anyhow!( "Could not convert custom warp image into approprate dynamic image." )); }; return Ok(ImageType::StaticBitmap { image: Arc::new(StaticImage { img }), }); } match image::guess_format(data) { Ok(ImageFormat::Jpeg) => { let img = image::ImageReader::with_format( std::io::Cursor::new(data), image::ImageFormat::Jpeg, ) .decode()? .into_rgba8(); Ok(ImageType::StaticBitmap { image: Arc::new(StaticImage { img }), }) } Ok(ImageFormat::Png) => { let img = image::ImageReader::with_format( std::io::Cursor::new(data), image::ImageFormat::Png, ) .decode()? .into_rgba8(); Ok(ImageType::StaticBitmap { image: Arc::new(StaticImage { img }), }) } Ok(ImageFormat::WebP) => { let decoder = WebPDecoder::new(std::io::Cursor::new(data))?; if decoder.has_animation() { let frames = decoder.into_frames().collect_frames()?; Ok(ImageType::AnimatedBitmap { image: Arc::new(AnimatedImage::from(frames)), }) } else { let img = DynamicImage::from_decoder(decoder)?.into_rgba8(); Ok(ImageType::StaticBitmap { image: Arc::new(StaticImage { img }), }) } } Ok(ImageFormat::Gif) => { let decoder = GifDecoder::new(std::io::Cursor::new(data))?; let frames = decoder.into_frames().collect_frames()?; Ok(ImageType::AnimatedBitmap { image: Arc::new(AnimatedImage::from(frames)), }) } _ => Ok(ImageType::Unrecognized), } } fn size_in_bytes(&self) -> usize { match self { ImageType::Svg { .. } => 0, // TODO: How do we calculate svg size in bytes? ImageType::StaticBitmap { image } => image.rgba_bytes().len(), ImageType::AnimatedBitmap { image } => image .frames .iter() .map(|frame| frame.image.rgba_bytes().len()) .reduce(|acc, bytes| acc + bytes) .unwrap_or(0), ImageType::Unrecognized => 0, } } } /// A reference to an image in the asset cache. Can be a static or animated image. #[derive(Clone)] pub enum Image { Static(Arc), Animated(Arc), } /// A representation of an image in the asset cache. pub struct StaticImage { /// The actual RGBA image data, stored as a vector of bytes. img: image::RgbaImage, } impl StaticImage { pub fn size(&self) -> Vector2I { Vector2I::new(self.width() as i32, self.height() as i32) } pub fn width(&self) -> u32 { self.img.width() } pub fn height(&self) -> u32 { self.img.height() } pub fn rgba_bytes(&self) -> &[u8] { self.img.as_raw().as_slice() } } /// A representation of a single frame in an animated image. pub struct AnimatedImageFrame { // The static image representing the current frame of an animated image. pub image: Arc, // Delay until the next frame in ms. pub delay: u32, } /// A representation of an animated image (e.g. gif) in the asset cache. pub struct AnimatedImage { /// The frames of the animated image in sequential order. pub frames: Vec, /// Total duration of the animated image in ms. pub duration: u32, } #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] pub enum AnimatedImageBehavior { #[default] FullAnimation, FirstFramePreview, } #[derive(Clone, Debug, Copy, Eq, Hash, PartialEq)] pub enum FitType { /// Expands the image to fill the entire space while maintaining the aspect ratio. Cover, /// Resizes the image to maximum size that fully fits in the given bounds, /// maintaining the aspect ratio. Contain, /// Stretches the image to fit the given bounds, ignoring the aspect ratio. /// This should likely only be used with SVGs, and not all SVGs are designed /// to be stretched. Stretch, } impl FitType { pub fn should_retain_aspect_ratio(&self) -> bool { match self { FitType::Cover | FitType::Contain => true, FitType::Stretch => false, } } } #[derive(Clone)] pub enum ImageType { Svg { svg: Rc }, StaticBitmap { image: Arc }, AnimatedBitmap { image: Arc }, // TODO: other types Unrecognized, } impl ImageType { /// Returns the size of the underlying asset. pub fn image_size(&self) -> Option { match self { ImageType::Svg { svg } => Some(Vector2I::new( svg.size().width().round() as i32, svg.size().height().round() as i32, )), ImageType::StaticBitmap { image } => Some(image.size()), ImageType::AnimatedBitmap { image } => { image.frames.first().map(|frame| frame.image.size()) } ImageType::Unrecognized => None, } } fn type_str(&self) -> &'static str { match self { ImageType::Svg { .. } => "ImageType::Svg", ImageType::StaticBitmap { .. } => "ImageType::StaticBitmap", ImageType::AnimatedBitmap { .. } => "ImageType::AnimatedBitmap", ImageType::Unrecognized => "ImageType::Unrecognized", } } } #[derive(Clone, Copy, Debug)] pub enum CacheOption { /// Only the specific used sizes are cached. /// Best for situations when the image/asset is used with a fixed size. /// Example: icons, theme picker previews. BySize, /// Only the original asset is cached. /// Best for situations when the image/asset doesn't have a fixed size, and it may change /// significantly on every window resize. /// Example: background image. Original, } impl From> for AnimatedImage { fn from(value: Vec) -> Self { let mut duration = 0; let frames = value .into_iter() .map(|frame| { let (delay_numerator, delay_denominator) = frame.delay().numer_denom_ms(); let delay_ms = (delay_numerator / delay_denominator).max(MIN_REFRESH_DELAY_MS); duration += delay_ms; AnimatedImageFrame { image: Arc::new(StaticImage { img: frame.into_buffer(), }), delay: delay_ms, } }) .collect_vec(); AnimatedImage { frames, duration } } } fn resize_animated_image( image: &AnimatedImage, bounds: Vector2I, fit_type: FitType, ) -> AnimatedImage { let resized_frames = image .frames .iter() .map(|frame| AnimatedImageFrame { image: Arc::new(StaticImage { img: resize_image(&frame.image.img, bounds, fit_type), }), delay: frame.delay, }) .collect_vec(); AnimatedImage { frames: resized_frames, duration: image.duration, } } fn svg_image(svg: &Rc, bounds: Vector2I, fit_type: FitType) -> Result { let svg_size = &svg.size(); let svg_has_wider_ratio = svg_size.width() / svg_size.height() > bounds.x() as f32 / bounds.y() as f32; let fit = match (fit_type, svg_has_wider_ratio) { (FitType::Contain, true) | (FitType::Cover, false) => FitTo::Width(bounds.x() as u32), (FitType::Contain, false) | (FitType::Cover, true) => FitTo::Height(bounds.y() as u32), (FitType::Stretch, _) => FitTo::Bounds(bounds.x() as u32, bounds.y() as u32), }; let svg_size = svg_size.to_int_size(); let size = fit .fit_to_size(svg_size) .ok_or_else(|| anyhow!("Unable to fit SVG image to size"))?; let transform = fit.fit_to_transform(svg_size); let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()) .ok_or_else(|| anyhow!("Could not could create pixmap for bounds {:?}", bounds))?; resvg::render(svg.as_ref(), transform, &mut pixmap.as_mut()); let img = image::RgbaImage::from_vec(pixmap.width(), pixmap.height(), pixmap.take()).ok_or_else(|| anyhow!("Failed to convert tiny_skia::Pixmap into image::ImageBuffer due to buffer size mismatch"))?; Ok(Image::Static(Arc::new(StaticImage { img }))) } fn resize_image(img: &image::RgbaImage, bounds: Vector2I, fit_type: FitType) -> image::RgbaImage { // If the image dimensions match the target bounding box, return a simple // copy of it. if bounds.x() as u32 == img.width() && bounds.y() as u32 == img.height() { return img.clone(); } let filter = FilterType::Triangle; match fit_type { // This logic is adapted from image::DynamicImage::resize_to_fill(). FitType::Cover => { let nwidth = bounds.x() as u32; let nheight = bounds.y() as u32; // Resize the image, maintaining aspect ratio, such that the // smaller dimension equals its bounds. let (iwidth, iheight) = resize_dimensions(img.width(), img.height(), nwidth, nheight, FitType::Cover); let mut intermediate = DynamicImage::from(image::imageops::resize(img, iwidth, iheight, filter)); // Based on the original and new aspect ratios, crop off the excess // image data along either the vertical or horizontal axis. let aspect_ratio = u64::from(iwidth) * u64::from(nheight); let new_aspect_ratio = u64::from(nwidth) * u64::from(iheight); let img = if new_aspect_ratio > aspect_ratio { intermediate.crop(0, (iheight - nheight) / 2, nwidth, nheight) } else { intermediate.crop((iwidth - nwidth) / 2, 0, nwidth, nheight) }; img.into_rgba8() } fit_type => { let (new_width, new_height) = resize_dimensions( img.width(), img.height(), bounds.x() as u32, bounds.y() as u32, fit_type, ); image::imageops::resize(img, new_width, new_height, filter) } } } /// Calculates the width and height an image should be resized to. /// This preserves aspect ratio, and based on the `fit_type` parameter /// will either fill the dimensions to fit inside the smaller constraint /// (will overflow the specified bounds on one axis to preserve /// aspect ratio), or will shrink so that both dimensions are /// completely contained within the given `width` and `height`, /// with empty space on one axis, unless the fit_type is `Stretch`. /// /// This is adapted from image::math::utils::resize_dimensions(). pub fn resize_dimensions( width: u32, height: u32, nwidth: u32, nheight: u32, fit_type: FitType, ) -> (u32, u32) { use std::cmp::max; let wratio = nwidth as f64 / width as f64; let hratio = nheight as f64 / height as f64; let ratio = match fit_type { FitType::Cover => f64::max(wratio, hratio), FitType::Contain => { // Resize the image, maintaining aspect ratio, such that the larger // dimension equals its bounds. f64::min(wratio, hratio) } // Stretch doesn't maintain the aspect ratio FitType::Stretch => return (nwidth, nheight), }; let nw = max((width as f64 * ratio).round() as u64, 1); let nh = max((height as f64 * ratio).round() as u64, 1); if nw > u64::from(u32::MAX) { let ratio = u32::MAX as f64 / width as f64; (u32::MAX, max((height as f64 * ratio).round() as u32, 1)) } else if nh > u64::from(u32::MAX) { let ratio = u32::MAX as f64 / height as f64; (max((width as f64 * ratio).round() as u32, 1), u32::MAX) } else { (nw as u32, nh as u32) } } impl ImageType { /// Converts the ImageType to the Image structure. /// Takes into account bounds, fit_type and whether to resize the image. /// If resize is set to true, the image is first resized to either cover or contain fit within /// the given bounds. Otherwise, the dimensions are ignored, and the image is converted with /// its original size. In this case we may cache the image bytes to avoid repeated conversions. fn to_image( &self, bounds: Vector2I, fit_type: FitType, resize: bool, animated_image_behavior: AnimatedImageBehavior, ) -> Result { match self { ImageType::Unrecognized => Err(anyhow!("Unrecognized image format.")), ImageType::StaticBitmap { image } => { if resize { let img = resize_image(&image.img, bounds, fit_type); Ok(Image::Static(Arc::new(StaticImage { img }))) } else { Ok(Image::Static(image.clone())) } } ImageType::AnimatedBitmap { image } => match animated_image_behavior { AnimatedImageBehavior::FullAnimation => { if resize { Ok(Image::Animated(Arc::new(resize_animated_image( image.as_ref(), bounds, fit_type, )))) } else { Ok(Image::Animated(image.clone())) } } AnimatedImageBehavior::FirstFramePreview => { let first_frame = image .frames .first() .ok_or_else(|| anyhow!("Animated image contained no frames"))? .image .clone(); if resize { let img = resize_image(&first_frame.img, bounds, fit_type); Ok(Image::Static(Arc::new(StaticImage { img }))) } else { Ok(Image::Static(first_frame)) } } }, ImageType::Svg { svg } => svg_image(svg, bounds, fit_type), } } } impl AnimatedImage { /// Calculates the current frame of the animated image based on elapsed time and /// returns a pointer to the image along with the remaining delay in the frame. /// `elapsed` is the time in ms since the animated image started animating. pub fn get_current_frame(&self, elapsed: u32) -> Result<(Arc, u32)> { if self.duration == 0 { return Err(anyhow!( "Animated image has duration 0, which is not supported" )); } // Linear search for the correct frame, this can be optimized. let elapsed = elapsed % self.duration; let mut start = 0; for frame in self.frames.iter() { let end = start + frame.delay; if elapsed >= start && elapsed < end { let remaining_delay = end - elapsed; return Ok((frame.image.clone(), remaining_delay)); } start = end; } // We should only reach here if self.frames is empty. Err(anyhow!("No frame found for elapsed {}", elapsed)) } } /// Image fit options used when rendering an SVG into a bitmap. `resvg` used to support this /// directly, however it was removed in version `0.34`. #[derive(Clone, Debug)] enum FitTo { /// Scale to width, preserving aspect ratio. Width(u32), /// Scale to height, preserving aspect ratio. Height(u32), /// Stretch to fit the given bounds, ignoring the aspect ratio. Bounds(u32, u32), } impl FitTo { /// Adjusts `size` based on the current value of `FitTo`. /// Taken directly from `resvg`: /// . fn fit_to_size(&self, size: IntSize) -> Option { match *self { FitTo::Width(w) => size.scale_to_width(w), FitTo::Height(h) => size.scale_to_height(h), FitTo::Bounds(w, h) => Some(IntSize::from_wh(w, h)?), } } /// Returns a [`tiny_skia::Transform`] that would scale `size` to match the new fitted size /// produced via [`FitTo::fit_to_size`]. /// Taken directly from `resvg`: /// . fn fit_to_transform(&self, size: IntSize) -> tiny_skia::Transform { let original_size = size.to_size(); let fitted_size = match self.fit_to_size(size) { Some(fitted_size) => fitted_size.to_size(), None => return tiny_skia::Transform::default(), }; tiny_skia::Transform::from_scale( fitted_size.width() / original_size.width(), fitted_size.height() / original_size.height(), ) } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] struct RenderedImageCacheKey { bounds: Vector2I, animated_image_behavior: AnimatedImageBehavior, } #[derive(Default)] pub struct ImageCache { /// Map of images of any ImageType already scaled to a certain size. /// Uses the hashed AssetSource and rendered-image properties as a key. images: RwLock>>>, } impl ImageCache { pub fn new() -> Self { Self::default() } pub fn evict_image(&self, asset_source: &AssetSource) { let mut cache = self.images.write(); let mut s = DefaultHasher::new(); asset_source.hash(&mut s); let cache_key = s.finish(); cache.remove(&cache_key); } /// Removes a single cached size entry for an asset. /// /// When the removed `Rc` is the last strong holder of the inner /// `Arc`, that `Arc`'s strong count drops to zero. On the /// next call to `TextureCache::end_frame()`, the corresponding GPU texture /// will be evicted automatically via the `Weak` it holds. /// /// `bounds` must match the resolved bounds used as the cache key inside /// `image()` (i.e., after any `max_dimension` adjustment), not the /// originally requested bounds. // Called by the debounce eviction pass added in the main changeset. /// TODO(APP-3877): remove `#[allow(dead_code)]` once the debounce eviction pass wires this up. #[allow(dead_code)] fn evict_size( &self, asset_source: &AssetSource, bounds: Vector2I, animated_image_behavior: AnimatedImageBehavior, ) { let mut s = DefaultHasher::new(); asset_source.hash(&mut s); let cache_key = s.finish(); let rendered_key = RenderedImageCacheKey { bounds, animated_image_behavior, }; let mut cache = self.images.write(); if let Some(inner_map) = cache.get_mut(&cache_key) { inner_map.remove(&rendered_key); if inner_map.is_empty() { cache.remove(&cache_key); } } } #[allow(clippy::too_many_arguments)] pub fn image( &self, asset_source: AssetSource, bounds: Vector2I, fit_type: FitType, animated_image_behavior: AnimatedImageBehavior, cache_option: CacheOption, max_dimension: Option, asset_cache: &AssetCache, ) -> AssetState { let mut s = DefaultHasher::new(); asset_source.hash(&mut s); let cache_key = s.finish(); match asset_cache.load_asset::(asset_source) { AssetState::Loading { handle } => AssetState::Loading { handle }, AssetState::Evicted => AssetState::Evicted, AssetState::FailedToLoad(err) => AssetState::FailedToLoad(err), AssetState::Loaded { data } => { let (mut needs_resize, mut bounds) = match cache_option { CacheOption::BySize => { // Only store a resized copy of the source asset if a // specific size was requested and it doesn't match the // source asset's size. let needs_resize = data.image_size() != Some(bounds); (needs_resize, bounds) } CacheOption::Original => { // If the caller requested that we cache the asset at // its original size, set its size as the target // bounds. let Some(bounds) = data.image_size() else { return AssetState::FailedToLoad(Rc::new(anyhow!( "Requested CacheOption::Original for {}, which has no inherent size", data.type_str() ))); }; (false, bounds) } }; // If we need to ensure the image isn't larger than a given // size along either dimension, check if a resize is needed // and update needs_resize and bounds accordingly. if let Some(max_dimension) = max_dimension { let width = bounds.x() as u32; let height = bounds.y() as u32; if width > max_dimension || height > max_dimension { needs_resize = true; let (nwidth, nheight) = resize_dimensions( width, height, max_dimension, max_dimension, fit_type, ); bounds = Vector2I::new(nwidth as i32, nheight as i32); } } let rendered_image_cache_key = RenderedImageCacheKey { bounds, animated_image_behavior, }; // If it's already in the image cache at the target size, // return it. let cache = self.images.upgradable_read(); if let Some(inner_map) = cache.get(&cache_key) { if let Some(image) = inner_map.get(&rendered_image_cache_key) { return AssetState::Loaded { data: image.clone(), }; } } // Otherwise, create the correctly-sized image struct and // insert it into the cache (if necessary). let image = match data.to_image(bounds, fit_type, needs_resize, animated_image_behavior) { Ok(image) => Rc::new(image), Err(err) => return AssetState::FailedToLoad(Rc::new(err)), }; if needs_resize { let mut images_cache = RwLockUpgradableReadGuard::upgrade(cache); images_cache .entry(cache_key) .or_default() .insert(rendered_image_cache_key, image.clone()); } AssetState::Loaded { data: image } } } } } impl Entity for ImageCache { type Event = (); } impl SingletonEntity for ImageCache {} #[cfg(test)] #[path = "image_cache_tests.rs"] mod tests; #[cfg(test)] pub(crate) mod test_utils { use super::*; /// Creates an `Arc` with the given dimensions for use in unit tests. pub(crate) fn make_static_image(width: u32, height: u32) -> Arc { Arc::new(StaticImage { img: image::RgbaImage::new(width, height), }) } }