1use std::collections::HashMap;
52use std::path::{Path, PathBuf};
53use std::sync::{Arc, Mutex};
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
64pub enum PixelFormat {
65 #[default]
68 Rgba8,
69 BgraPremultiplied8,
71}
72
73#[derive(Clone, PartialEq, Eq)]
75pub struct DecodedImage {
76 pub width: u32,
77 pub height: u32,
78 pub format: PixelFormat,
79 pub rgba: Arc<[u8]>,
81}
82
83impl std::fmt::Debug for DecodedImage {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.debug_struct("DecodedImage")
88 .field("width", &self.width)
89 .field("height", &self.height)
90 .field("bytes", &self.rgba.len())
91 .finish()
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum MediaError {
99 Unreadable(String),
101 Undecodable(String),
103 TooLarge { width: u32, height: u32 },
106}
107
108impl std::fmt::Display for MediaError {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 match self {
111 Self::Unreadable(e) => write!(f, "unreadable: {e}"),
112 Self::Undecodable(e) => write!(f, "undecodable: {e}"),
113 Self::TooLarge { width, height } => {
114 write!(f, "too large: {width}x{height}")
115 }
116 }
117 }
118}
119
120pub const MAX_PIXELS: u64 = 64 * 1024 * 1024;
128
129fn is_svg(path: &Path) -> bool {
132 path.extension()
133 .and_then(|e| e.to_str())
134 .is_some_and(|e| e.eq_ignore_ascii_case("svg"))
135}
136
137fn system_fonts() -> std::sync::Arc<resvg::usvg::fontdb::Database> {
150 static FONTS: std::sync::OnceLock<std::sync::Arc<resvg::usvg::fontdb::Database>> =
151 std::sync::OnceLock::new();
152 FONTS
153 .get_or_init(|| {
154 let mut db = resvg::usvg::fontdb::Database::new();
155 db.load_system_fonts();
156 std::sync::Arc::new(db)
157 })
158 .clone()
159}
160
161fn svg_tree(path: &Path, with_fonts: bool) -> Result<resvg::usvg::Tree, MediaError> {
169 let data = std::fs::read(path).map_err(|e| MediaError::Unreadable(e.to_string()))?;
170 let mut opt = resvg::usvg::Options {
171 resources_dir: path.parent().map(std::path::Path::to_path_buf),
172 ..Default::default()
173 };
174 if with_fonts {
175 opt.fontdb = system_fonts();
176 }
177 resvg::usvg::Tree::from_data(&data, &opt).map_err(|e| MediaError::Undecodable(e.to_string()))
178}
179
180fn svg_size(tree: &resvg::usvg::Tree) -> (u32, u32) {
185 let size = tree.size();
186 (
187 size.width().ceil().max(1.0) as u32,
188 size.height().ceil().max(1.0) as u32,
189 )
190}
191
192pub fn probe(path: &Path) -> Result<(u32, u32), MediaError> {
194 if is_svg(path) {
195 let (w, h) = svg_size(&svg_tree(path, false)?);
196 guard_size(w, h)?;
197 return Ok((w, h));
198 }
199 let reader = image::ImageReader::open(path)
200 .map_err(|e| MediaError::Unreadable(e.to_string()))?
201 .with_guessed_format()
202 .map_err(|e| MediaError::Unreadable(e.to_string()))?;
203 let (w, h) = reader
204 .into_dimensions()
205 .map_err(|e| MediaError::Undecodable(e.to_string()))?;
206 guard_size(w, h)?;
207 Ok((w, h))
208}
209
210pub fn decode(
217 path: &Path,
218 target: (u32, u32),
219 format: PixelFormat,
220) -> Result<DecodedImage, MediaError> {
221 if is_svg(path) {
222 return decode_svg(path, target, format);
223 }
224 let (w, h) = probe(path)?;
225 let img = image::ImageReader::open(path)
226 .map_err(|e| MediaError::Unreadable(e.to_string()))?
227 .with_guessed_format()
228 .map_err(|e| MediaError::Unreadable(e.to_string()))?
229 .decode()
230 .map_err(|e| MediaError::Undecodable(e.to_string()))?;
231
232 let (tw, th) = fit_within((w, h), target);
233 let scaled = if (tw, th) == (w, h) {
234 img
235 } else {
236 img.resize(tw, th, image::imageops::FilterType::Triangle)
237 };
238 let rgba = scaled.to_rgba8();
239 let (w, h) = (rgba.width(), rgba.height());
240 let mut bytes = rgba.into_raw();
241 if format == PixelFormat::BgraPremultiplied8 {
242 premultiply_to_bgra(&mut bytes);
243 }
244 Ok(DecodedImage {
245 width: w,
246 height: h,
247 format,
248 rgba: Arc::from(bytes.into_boxed_slice()),
249 })
250}
251
252fn decode_svg(
264 path: &Path,
265 target: (u32, u32),
266 format: PixelFormat,
267) -> Result<DecodedImage, MediaError> {
268 let tree = svg_tree(path, true)?;
269 let natural = svg_size(&tree);
270 guard_size(natural.0, natural.1)?;
271 let (tw, th) = fit_within(natural, target);
272 let mut pixmap = resvg::tiny_skia::Pixmap::new(tw.max(1), th.max(1))
273 .ok_or_else(|| MediaError::Undecodable(format!("cannot allocate a {tw}x{th} pixmap")))?;
274 let size = tree.size();
275 let transform = resvg::tiny_skia::Transform::from_scale(
276 tw as f32 / size.width(),
277 th as f32 / size.height(),
278 );
279 resvg::render(&tree, transform, &mut pixmap.as_mut());
280
281 let (w, h) = (pixmap.width(), pixmap.height());
287 let mut bytes = pixmap.take();
288 match format {
289 PixelFormat::BgraPremultiplied8 => swap_rb(&mut bytes),
290 PixelFormat::Rgba8 => unpremultiply(&mut bytes),
291 }
292 Ok(DecodedImage {
293 width: w,
294 height: h,
295 format,
296 rgba: Arc::from(bytes.into_boxed_slice()),
297 })
298}
299
300fn swap_rb(bytes: &mut [u8]) {
304 for px in bytes.chunks_exact_mut(4) {
305 px.swap(0, 2);
306 }
307}
308
309fn unpremultiply(bytes: &mut [u8]) {
316 for px in bytes.chunks_exact_mut(4) {
317 let a = px[3];
318 if a == 0 || a == 255 {
319 continue;
320 }
321 for c in &mut px[..3] {
322 *c = ((*c as u16 * 255 + (a as u16 / 2)) / a as u16).min(255) as u8;
323 }
324 }
325}
326
327fn premultiply_to_bgra(bytes: &mut [u8]) {
333 for px in bytes.chunks_exact_mut(4) {
334 let (r, g, b, a) = (px[0], px[1], px[2], px[3]);
335 let mul = |c: u8| (((c as u16) * (a as u16) + 127) / 255) as u8;
339 px[0] = mul(b);
340 px[1] = mul(g);
341 px[2] = mul(r);
342 px[3] = a;
343 }
344}
345
346fn guard_size(w: u32, h: u32) -> Result<(), MediaError> {
347 if w == 0 || h == 0 {
348 return Err(MediaError::Undecodable("zero-sized image".into()));
349 }
350 if (w as u64) * (h as u64) > MAX_PIXELS {
351 return Err(MediaError::TooLarge {
352 width: w,
353 height: h,
354 });
355 }
356 Ok(())
357}
358
359pub fn fit_within(src: (u32, u32), bounds: (u32, u32)) -> (u32, u32) {
362 let (sw, sh) = src;
363 let (bw, bh) = bounds;
364 if bw == 0 || bh == 0 || (sw <= bw && sh <= bh) {
365 return src;
366 }
367 let scale = (bw as f64 / sw as f64).min(bh as f64 / sh as f64);
368 (
371 ((sw as f64 * scale).round() as u32).max(1),
372 ((sh as f64 * scale).round() as u32).max(1),
373 )
374}
375
376pub fn block_geometry(
392 intrinsic: (u32, u32),
393 fit: lattice_cells::MediaFit,
394 line_height_px: f32,
395 pane_width_px: f32,
396) -> (u16, f32) {
397 if !line_height_px.is_finite()
403 || !pane_width_px.is_finite()
404 || line_height_px <= 0.0
405 || pane_width_px <= 0.0
406 {
407 return (1, 1.0);
408 }
409 let (iw, ih) = intrinsic;
410 if iw == 0 || ih == 0 {
411 return (1, 1.0);
412 }
413
414 let drawn_h_px = match fit {
415 lattice_cells::MediaFit::Contain => {
418 let scale = (pane_width_px / iw as f32).min(1.0);
419 ih as f32 * scale
420 }
421 lattice_cells::MediaFit::Width => ih as f32 * (pane_width_px / iw as f32),
423 };
424
425 let height_lh = (drawn_h_px / line_height_px).max(MIN_BLOCK_LH);
426 let rows = height_lh.ceil().min(u16::MAX as f32) as u16;
427 (rows.max(1), height_lh)
428}
429
430pub const MIN_BLOCK_LH: f32 = 1.0;
436
437#[derive(Debug, Clone, PartialEq, Eq, Hash)]
443struct CacheKey {
444 path: PathBuf,
445 mtime: Option<i64>,
446 target: (u32, u32),
447 format: PixelFormat,
448}
449
450pub struct MediaCache {
457 inner: Mutex<CacheInner>,
458 budget_bytes: usize,
459}
460
461struct CacheInner {
462 entries: HashMap<CacheKey, Arc<DecodedImage>>,
463 order: Vec<CacheKey>,
464 bytes: usize,
465}
466
467impl MediaCache {
468 pub fn new(budget_bytes: usize) -> Self {
469 Self {
470 inner: Mutex::new(CacheInner {
471 entries: HashMap::new(),
472 order: Vec::new(),
473 bytes: 0,
474 }),
475 budget_bytes,
476 }
477 }
478
479 pub async fn get(
486 self: &Arc<Self>,
487 path: &Path,
488 target: (u32, u32),
489 format: PixelFormat,
490 ) -> Result<Arc<DecodedImage>, MediaError> {
491 let key = CacheKey {
492 path: path.to_path_buf(),
493 mtime: mtime_of(path),
494 target,
495 format,
496 };
497 if let Some(hit) = self.lookup(&key) {
498 return Ok(hit);
499 }
500 let owned = path.to_path_buf();
501 let decoded = tokio::task::spawn_blocking(move || decode(&owned, target, format))
502 .await
503 .map_err(|e| MediaError::Unreadable(format!("decode task failed: {e}")))??;
504 let decoded = Arc::new(decoded);
505 self.insert(key, decoded.clone());
506 Ok(decoded)
507 }
508
509 fn lookup(&self, key: &CacheKey) -> Option<Arc<DecodedImage>> {
510 self.inner.lock().ok()?.entries.get(key).cloned()
511 }
512
513 fn insert(&self, key: CacheKey, value: Arc<DecodedImage>) {
514 let Ok(mut inner) = self.inner.lock() else {
515 tracing::warn!("media cache mutex poisoned; skipping insert");
518 return;
519 };
520 let size = value.rgba.len();
521 if inner.entries.insert(key.clone(), value).is_none() {
522 inner.order.push(key);
523 inner.bytes += size;
524 }
525 while inner.bytes > self.budget_bytes && !inner.order.is_empty() {
526 let oldest = inner.order.remove(0);
527 if let Some(dropped) = inner.entries.remove(&oldest) {
528 inner.bytes = inner.bytes.saturating_sub(dropped.rgba.len());
529 }
530 }
531 }
532
533 pub fn bytes(&self) -> usize {
535 self.inner.lock().map(|i| i.bytes).unwrap_or(0)
536 }
537
538 pub fn clear(&self) {
540 if let Ok(mut inner) = self.inner.lock() {
541 inner.entries.clear();
542 inner.order.clear();
543 inner.bytes = 0;
544 }
545 }
546}
547
548fn mtime_of(path: &Path) -> Option<i64> {
549 let meta = std::fs::metadata(path).ok()?;
550 let modified = meta.modified().ok()?;
551 let dur = modified.duration_since(std::time::UNIX_EPOCH).ok()?;
552 Some(dur.as_secs() as i64)
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558
559 fn write_png(dir: &Path, name: &str, w: u32, h: u32) -> PathBuf {
560 let path = dir.join(name);
561 let buf = image::RgbaImage::from_pixel(w, h, image::Rgba([10, 20, 30, 255]));
562 buf.save(&path).expect("write png");
563 path
564 }
565
566 fn write_svg(dir: &Path, name: &str, body: &str) -> PathBuf {
567 let path = dir.join(name);
568 std::fs::write(&path, body).expect("write svg");
569 path
570 }
571
572 const RED_40X20: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" width="40" height="20">
575 <rect x="0" y="0" width="40" height="20" fill="#ff0000"/>
576 </svg>"##;
577
578 #[test]
582 fn probe_reads_an_svgs_declared_size() {
583 let dir = tempfile::tempdir().unwrap();
584 let path = write_svg(dir.path(), "d.svg", RED_40X20);
585 assert_eq!(probe(&path).unwrap(), (40, 20));
586 }
587
588 #[test]
591 fn an_svgs_size_rounds_up_to_whole_pixels() {
592 let dir = tempfile::tempdir().unwrap();
593 let path = write_svg(
594 dir.path(),
595 "f.svg",
596 r#"<svg xmlns="http://www.w3.org/2000/svg" width="10.2" height="4.1"></svg>"#,
597 );
598 assert_eq!(probe(&path).unwrap(), (11, 5));
599 }
600
601 #[test]
605 fn an_svg_renders_at_the_fitted_size_and_is_never_upscaled() {
606 let dir = tempfile::tempdir().unwrap();
607 let path = write_svg(dir.path(), "d.svg", RED_40X20);
608
609 let small = decode(&path, (20, 10), PixelFormat::Rgba8).unwrap();
610 assert_eq!((small.width, small.height), (20, 10), "scaled down to fit");
611
612 let huge = decode(&path, (4000, 4000), PixelFormat::Rgba8).unwrap();
613 assert_eq!(
614 (huge.width, huge.height),
615 (40, 20),
616 "never upscaled, exactly as a raster of the same size is not"
617 );
618 }
619
620 #[test]
626 fn an_opaque_svg_pixel_survives_both_pixel_formats() {
627 let dir = tempfile::tempdir().unwrap();
628 let path = write_svg(dir.path(), "d.svg", RED_40X20);
629
630 let rgba = decode(&path, (40, 20), PixelFormat::Rgba8).unwrap();
631 assert_eq!(
632 &rgba.rgba[..4],
633 &[255, 0, 0, 255],
634 "straight RGBA: opaque red stays opaque red"
635 );
636
637 let bgra = decode(&path, (40, 20), PixelFormat::BgraPremultiplied8).unwrap();
638 assert_eq!(
639 &bgra.rgba[..4],
640 &[0, 0, 255, 255],
641 "premultiplied BGRA: the channels swap and nothing is multiplied twice"
642 );
643 }
644
645 #[test]
649 fn a_semi_transparent_svg_pixel_is_premultiplied_exactly_once() {
650 let dir = tempfile::tempdir().unwrap();
651 let path = write_svg(
652 dir.path(),
653 "t.svg",
654 r##"<svg xmlns="http://www.w3.org/2000/svg" width="4" height="4">
655 <rect x="0" y="0" width="4" height="4" fill="#ff0000" fill-opacity="0.5"/>
656 </svg>"##,
657 );
658
659 let bgra = decode(&path, (4, 4), PixelFormat::BgraPremultiplied8).unwrap();
660 let (b, g, r, a) = (bgra.rgba[0], bgra.rgba[1], bgra.rgba[2], bgra.rgba[3]);
661 assert_eq!((b, g), (0, 0));
662 assert!((120..=136).contains(&a), "half transparent, got alpha {a}");
663 assert!(
664 r.abs_diff(a) <= 2,
665 "red premultiplied ONCE is ~alpha ({a}); got {r}. Twice would be ~{}",
666 (a as u16 * a as u16 / 255)
667 );
668 }
669
670 #[test]
673 fn a_malformed_svg_is_an_error_not_a_panic() {
674 let dir = tempfile::tempdir().unwrap();
675 let path = write_svg(dir.path(), "bad.svg", "<svg this is not xml");
676 assert!(matches!(probe(&path), Err(MediaError::Undecodable(_))));
677 assert!(decode(&path, (10, 10), PixelFormat::Rgba8).is_err());
678 }
679
680 #[test]
681 fn probe_reads_dimensions_without_decoding() {
682 let dir = tempfile::tempdir().unwrap();
683 let p = write_png(dir.path(), "a.png", 40, 25);
684 assert_eq!(probe(&p).unwrap(), (40, 25));
685 }
686
687 #[test]
690 fn every_failure_mode_is_an_error_not_a_panic() {
691 let dir = tempfile::tempdir().unwrap();
692 assert!(matches!(
693 probe(&dir.path().join("nope.png")),
694 Err(MediaError::Unreadable(_))
695 ));
696
697 let junk = dir.path().join("junk.png");
698 std::fs::write(&junk, b"this is not a png").unwrap();
699 assert!(probe(&junk).is_err(), "garbage is refused, not decoded");
700 }
701
702 #[test]
703 fn fit_never_upscales_and_never_vanishes() {
704 assert_eq!(fit_within((40, 25), (100, 100)), (40, 25));
706 assert_eq!(fit_within((200, 100), (100, 100)), (100, 50));
708 assert_eq!(fit_within((100, 200), (100, 100)), (50, 100));
709 let (w, h) = fit_within((10_000, 3), (100, 100));
712 assert!(h >= 1 && w >= 1, "got {w}x{h}");
713 }
714
715 #[test]
719 fn an_absurd_size_is_refused_before_allocating() {
720 assert!(matches!(
721 guard_size(100_000, 100_000),
722 Err(MediaError::TooLarge { .. })
723 ));
724 assert!(guard_size(0, 10).is_err(), "zero-sized is not an image");
725 assert!(guard_size(1920, 1080).is_ok());
726 }
727
728 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
729 async fn decoding_scales_to_fit_and_caches_the_result() {
730 let dir = tempfile::tempdir().unwrap();
731 let p = write_png(dir.path(), "big.png", 200, 100);
732 let cache = Arc::new(MediaCache::new(8 * 1024 * 1024));
733
734 let first = cache
735 .get(&p, (100, 100), PixelFormat::Rgba8)
736 .await
737 .expect("decodes");
738 assert_eq!((first.width, first.height), (100, 50), "scaled to fit");
739 assert_eq!(first.rgba.len(), 100 * 50 * 4, "RGBA8");
740
741 let again = cache
742 .get(&p, (100, 100), PixelFormat::Rgba8)
743 .await
744 .expect("decodes");
745 assert!(Arc::ptr_eq(&first, &again), "second get is a cache hit");
746
747 let other = cache
749 .get(&p, (40, 40), PixelFormat::Rgba8)
750 .await
751 .expect("decodes");
752 assert_eq!((other.width, other.height), (40, 20));
753 }
754
755 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
758 async fn the_cache_evicts_to_stay_within_its_byte_budget() {
759 let dir = tempfile::tempdir().unwrap();
760 let cache = Arc::new(MediaCache::new(50_000));
762 for i in 0..4 {
763 let p = write_png(dir.path(), &format!("{i}.png"), 100, 100);
764 cache
765 .get(&p, (100, 100), PixelFormat::Rgba8)
766 .await
767 .expect("decodes");
768 assert!(
769 cache.bytes() <= 50_000,
770 "over budget after {i}: {}",
771 cache.bytes()
772 );
773 }
774 cache.clear();
775 assert_eq!(cache.bytes(), 0);
776 }
777
778 #[test]
779 fn geometry_scales_to_the_pane_and_reserves_whole_rows() {
780 use lattice_cells::MediaFit;
781 let (rows, lh) = block_geometry((400, 200), MediaFit::Contain, 20.0, 200.0);
784 assert_eq!(rows, 5);
785 assert!((lh - 5.0).abs() < 0.001, "got {lh}");
786
787 let (rows, lh) = block_geometry((400, 170), MediaFit::Contain, 20.0, 200.0);
791 assert!((lh - 4.25).abs() < 0.001, "got {lh}");
792 assert_eq!(
793 rows, 5,
794 "rows is ceil(height), so the reservation covers it"
795 );
796 }
797
798 #[test]
801 fn contain_never_upscales_but_width_does() {
802 use lattice_cells::MediaFit;
803 let (_, lh_contain) = block_geometry((32, 32), MediaFit::Contain, 20.0, 400.0);
804 assert!(
805 (lh_contain - 1.6).abs() < 0.001,
806 "32px / 20px lines, got {lh_contain}"
807 );
808
809 let (_, lh_width) = block_geometry((32, 32), MediaFit::Width, 20.0, 400.0);
810 assert!(
811 (lh_width - 20.0).abs() < 0.001,
812 "filled the width, got {lh_width}"
813 );
814 }
815
816 #[test]
819 fn degenerate_geometry_yields_one_row_rather_than_nonsense() {
820 use lattice_cells::MediaFit;
821 for args in [
822 ((100, 100), 0.0, 200.0),
823 ((100, 100), 20.0, 0.0),
824 ((0, 100), 20.0, 200.0),
825 ((100, 0), 20.0, 200.0),
826 ] {
827 let (rows, lh) = block_geometry(args.0, MediaFit::Contain, args.1, args.2);
828 assert_eq!((rows, lh), (1, 1.0), "for {args:?}");
829 }
830 }
831
832 #[test]
835 fn an_extreme_ratio_still_draws_at_least_one_line() {
836 use lattice_cells::MediaFit;
837 let (rows, lh) = block_geometry((10_000, 1), MediaFit::Contain, 20.0, 200.0);
838 assert!(lh >= MIN_BLOCK_LH, "got {lh}");
839 assert_eq!(rows, 1);
840 }
841
842 #[test]
847 fn bgra_premultiplication_happens_at_decode_not_at_paint() {
848 let dir = tempfile::tempdir().unwrap();
849 let path = dir.path().join("half.png");
850 image::RgbaImage::from_pixel(1, 1, image::Rgba([255, 0, 0, 128]))
852 .save(&path)
853 .unwrap();
854
855 let straight = decode(&path, (10, 10), PixelFormat::Rgba8).unwrap();
856 assert_eq!(&straight.rgba[..], &[255, 0, 0, 128], "RGBA is untouched");
857
858 let bgra = decode(&path, (10, 10), PixelFormat::BgraPremultiplied8).unwrap();
859 assert_eq!(
861 &bgra.rgba[..],
862 &[0, 0, 128, 128],
863 "channels swapped and premultiplied"
864 );
865 }
866
867 #[test]
871 fn premultiplication_rounds_instead_of_truncating() {
872 let mut px = vec![200u8, 100, 50, 200];
873 premultiply_to_bgra(&mut px);
874 assert_eq!(px[2], 157, "red channel rounded");
876 assert_eq!(px[3], 200, "alpha is left alone");
877 }
878
879 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
882 async fn the_format_is_part_of_the_cache_key() {
883 let dir = tempfile::tempdir().unwrap();
884 let p = write_png(dir.path(), "c.png", 8, 8);
885 let cache = Arc::new(MediaCache::new(8 * 1024 * 1024));
886
887 let a = cache.get(&p, (100, 100), PixelFormat::Rgba8).await.unwrap();
888 let b = cache
889 .get(&p, (100, 100), PixelFormat::BgraPremultiplied8)
890 .await
891 .unwrap();
892 assert!(
893 !Arc::ptr_eq(&a, &b),
894 "different layouts are different entries"
895 );
896 assert_eq!(a.format, PixelFormat::Rgba8);
897 assert_eq!(b.format, PixelFormat::BgraPremultiplied8);
898 }
899
900 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
903 async fn rewriting_the_file_invalidates_its_entry() {
904 let dir = tempfile::tempdir().unwrap();
905 let p = write_png(dir.path(), "x.png", 40, 40);
906 let cache = Arc::new(MediaCache::new(8 * 1024 * 1024));
907 let first = cache.get(&p, (100, 100), PixelFormat::Rgba8).await.unwrap();
908 assert_eq!((first.width, first.height), (40, 40));
909
910 std::thread::sleep(std::time::Duration::from_millis(1100));
913 write_png(dir.path(), "x.png", 20, 20);
914
915 let second = cache.get(&p, (100, 100), PixelFormat::Rgba8).await.unwrap();
916 assert_eq!(
917 (second.width, second.height),
918 (20, 20),
919 "the rewritten file was decoded again, not served from cache"
920 );
921 }
922}