Files
figcraft/crates/render/src/image_budget_tests.rs
T
BFlat e32f8f2e07 M8: large images on the canvas draw from a smaller copy (#1483) (#1518)
* M8: large images on the canvas draw from a smaller copy (#1483)

An image shown smaller than its pixels draws on the canvas from a copy scaled down to 4096 or 8192 px on its longer side, cached beside the image in the same budget. Two 16000 x 10000 images (640 MB each) evicted each other on every frame that drew both; as copies they take 80 MB together zoomed out. Moving one over the other: about 2.1 s a frame before, 11 ms at fit and 15 ms at 30% after. Exports, thumbnails and views at 100% and closer draw from the image itself. Tests in large_image_tests; timings in large_image_bench_tests (ignored).

* M8: drop the ignored large-image benchmark; its numbers are in the PR (#1483)
2026-10-29 18:14:18 +07:00

140 lines
5.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Decoded images within a memory budget (#203): an image too large is refused from its header,
//! before its pixels are decoded, and the decoded-image caches hold a bounded number of bytes
//! however many images, filters and colour profiles a document uses.
use std::sync::Arc;
use figcraft_color::ColorProfile;
use tiny_skia::Pixmap;
use super::*;
fn png(w: u32, h: u32) -> Vec<u8> {
let img = image::RgbaImage::from_pixel(w, h, image::Rgba([200, 40, 40, 255]));
let mut out = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).unwrap();
out
}
/// A PNG whose header claims `w` × `h` but which holds no pixel data at all.
fn png_header_only(w: u32, h: u32) -> Vec<u8> {
let mut ihdr = Vec::new();
ihdr.extend_from_slice(&w.to_be_bytes());
ihdr.extend_from_slice(&h.to_be_bytes());
ihdr.extend_from_slice(&[8, 6, 0, 0, 0]);
let mut out = b"\x89PNG\r\n\x1a\n".to_vec();
out.extend_from_slice(&13u32.to_be_bytes());
let chunk: Vec<u8> = b"IHDR".iter().copied().chain(ihdr).collect();
out.extend_from_slice(&chunk);
out.extend_from_slice(&crc32(&chunk).to_be_bytes());
out
}
fn crc32(data: &[u8]) -> u32 {
let mut crc = !0u32;
for b in data {
crc ^= u32::from(*b);
for _ in 0..8 {
crc = if crc & 1 == 1 { (crc >> 1) ^ 0xEDB8_8320 } else { crc >> 1 };
}
}
!crc
}
#[test]
fn an_image_within_the_limits_decodes() {
let (p, _) = images::decode_within(&png(30, 20), ColorProfile::Srgb, 30, 30 * 20 * 4).unwrap();
assert_eq!((p.width(), p.height()), (30, 20));
}
#[test]
fn an_image_wider_or_taller_than_the_limit_is_scaled_down_to_fit() {
let (p, _) = images::decode_within(&png(30, 20), ColorProfile::Srgb, 15, u64::MAX).unwrap();
assert_eq!((p.width(), p.height()), (15, 10));
let (p, _) = images::decode_within(&png(20, 30), ColorProfile::Srgb, 15, u64::MAX).unwrap();
assert_eq!((p.width(), p.height()), (10, 15));
}
#[test]
fn an_image_whose_pixels_pass_the_byte_budget_is_refused() {
assert!(images::decode_within(&png(30, 20), ColorProfile::Srgb, 1000, 30 * 20 * 4 - 1).is_none());
}
#[test]
fn a_huge_image_is_refused_from_its_header_alone() {
// 16 000 × 16 000 is within the side limit; its pixels (1 GB) aren't within a 1 MB budget, and
// the file has none to decode: only the header is read.
assert!(images::decode_within(&png_header_only(16_000, 16_000), ColorProfile::Srgb, 16_384, 1 << 20).is_none());
// Past the side limit, the shipped decode reads its header and finds no pixels to scale down.
assert!(images::decode(&png_header_only(20_000, 10)).is_none());
}
#[test]
fn an_image_past_the_side_limit_is_drawn_downscaled() {
// #1336: one side over 16 384 px drew nothing.
let p = images::decode(&png(16_385, 4)).unwrap();
assert_eq!(p.width(), 16_384);
assert!(p.height() >= 1 && p.height() <= 4);
assert!(p.pixels().iter().all(|c| c.alpha() == 255));
let p = images::decode(&png(4, 18_078)).unwrap();
assert_eq!(p.height(), 16_384);
assert!(p.width() >= 1 && p.width() <= 4);
}
fn key(name: &str) -> images::Key {
(name.to_string(), [0; 7], ColorProfile::Srgb, 0)
}
fn pixmap(side: u32) -> Arc<Pixmap> {
Arc::new(Pixmap::new(side, side).unwrap())
}
#[test]
fn the_image_cache_keeps_within_its_byte_budget() {
// 10 × 10 pixmaps are 400 bytes; 1000 bytes hold two.
let mut c = images::PixmapCache::default();
for name in ["a", "b", "c", "d", "e"] {
c.insert(key(name), pixmap(10), 1000);
assert!(c.bytes() <= 1000, "{} bytes held", c.bytes());
assert!(c.get(&key(name)).is_some(), "{name} was just added");
}
}
#[test]
fn a_full_image_cache_drops_the_least_recently_used_image_only() {
// 1000 bytes hold two 400-byte pixmaps: the third drops the one used longest ago.
let mut c = images::PixmapCache::default();
c.insert(key("a"), pixmap(10), 1000);
c.insert(key("b"), pixmap(10), 1000);
assert!(c.get(&key("a")).is_some());
c.insert(key("c"), pixmap(10), 1000);
assert!(c.get(&key("a")).is_some(), "used since b: kept");
assert!(c.get(&key("b")).is_none(), "used longest ago: dropped");
assert!(c.get(&key("c")).is_some());
assert_eq!(c.bytes(), 800);
}
#[test]
fn an_image_larger_than_the_budget_is_held_alone() {
let mut c = images::PixmapCache::default();
c.insert(key("small"), pixmap(10), 1000);
c.insert(key("big"), pixmap(20), 1000);
assert!(c.get(&key("big")).is_some());
assert!(c.get(&key("small")).is_none());
assert_eq!(c.bytes(), 20 * 20 * 4);
// The next image takes its place.
c.insert(key("next"), pixmap(10), 1000);
assert_eq!(c.bytes(), 400);
}
#[test]
fn filter_and_profile_variants_count_against_one_budget() {
let mut c = images::PixmapCache::default();
for (i, profile) in [ColorProfile::Srgb, ColorProfile::DisplayP3].into_iter().enumerate() {
for f in 0..4 {
c.insert(("img".to_string(), [f, 0, 0, 0, 0, 0, i as i32], profile, 0), pixmap(10), 1000);
assert!(c.bytes() <= 1000, "{} bytes held", c.bytes());
}
}
}