mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
Place: an image whose pixels can't be read is refused instead of placed blank
Squashed from #4257 onto main.
This commit is contained in:
@@ -198,6 +198,18 @@ impl ImageStore {
|
||||
pm
|
||||
}
|
||||
|
||||
/// Keep `pm` as `key` as [`ImageStore::insert`] does, holding no reference to it afterwards,
|
||||
/// then drop entries over budget → whether the store holds `key`'s pixels. Pixels larger than
|
||||
/// the budget are not kept.
|
||||
pub(crate) fn keep(&mut self, key: ImageKey, pm: Arc<Pixmap>) -> bool {
|
||||
if pixmap_bytes(&pm) > self.budget {
|
||||
return false;
|
||||
}
|
||||
drop(self.insert(key.clone(), pm));
|
||||
self.evict();
|
||||
self.entries.contains_key(&key)
|
||||
}
|
||||
|
||||
/// Drop the least recently used entries no one else holds until within budget.
|
||||
fn evict(&mut self) {
|
||||
if self.bytes <= self.budget {
|
||||
@@ -271,6 +283,42 @@ pub fn image_cache_bytes() -> usize {
|
||||
store().bytes()
|
||||
}
|
||||
|
||||
/// Keep `img`, the decoded pixels of `blob`, in the store as a renderer keeps the pixels it
|
||||
/// decodes, so the first render that draws `blob` uses them instead of decoding it. Returns whether
|
||||
/// `img` was kept or the store already held `blob`'s pixels. It returns false, without looking at
|
||||
/// the store, for a CMYK image and for pixels a renderer would refuse to decode: more than the
|
||||
/// decode allowance as RGBA, or more than fits beside the pixels being drawn now. The store then
|
||||
/// drops the least recently used entries beyond its budget, as it does whenever a renderer adds
|
||||
/// pixels, and returns false when that leaves `img` out: pixels larger than the budget are never
|
||||
/// kept.
|
||||
pub fn keep_decoded_image(blob: &ImageBlob, img: image::DynamicImage) -> bool {
|
||||
let (key, lut, _) = crate::blobs::blob_key(blob);
|
||||
if lut.is_some() || !crate::paint::within_allowance(img.width(), img.height()) {
|
||||
return false;
|
||||
}
|
||||
let kept = store().get(&key);
|
||||
let size = match kept {
|
||||
Some(pm) => (pm.width(), pm.height()),
|
||||
None => {
|
||||
let Some(pm) = crate::paint::decoded_pixmap(img) else { return false };
|
||||
let size = (pm.width(), pm.height());
|
||||
if !store().keep(key, Arc::new(pm)) {
|
||||
return false;
|
||||
}
|
||||
size
|
||||
}
|
||||
};
|
||||
remember_size(blob, size);
|
||||
true
|
||||
}
|
||||
|
||||
/// Whether the store holds the decoded pixels of `blob` at full size.
|
||||
pub fn image_cached(blob: &ImageBlob) -> bool {
|
||||
// The key first: working it out takes the store's lock too.
|
||||
let (key, _, _) = crate::blobs::blob_key(blob);
|
||||
store().peek(&key).is_some()
|
||||
}
|
||||
|
||||
/// `blob`'s content key and kind, hashed and sniffed once per blob allocation.
|
||||
pub(crate) fn blob_id(blob: &ImageBlob) -> BlobId {
|
||||
if let Some(id) = store().known_id(&blob.bytes) {
|
||||
@@ -370,6 +418,29 @@ mod tests {
|
||||
assert_eq!(s.bytes(), 400);
|
||||
}
|
||||
|
||||
/// Pixels kept for a first frame are dropped at once when the budget has no room for them,
|
||||
/// since nothing draws them yet.
|
||||
#[test]
|
||||
fn kept_pixels_larger_than_the_budget_are_not_kept() {
|
||||
// 10×10 pixmaps: 400 bytes each.
|
||||
let mut s = ImageStore::new(300);
|
||||
assert!(!s.keep(key("big"), Arc::new(Pixmap::new(10, 10))));
|
||||
assert_eq!(s.bytes(), 0);
|
||||
assert!(s.peek(&key("big")).is_none());
|
||||
// Room for one: the older one, held by no one, goes.
|
||||
let mut s = ImageStore::new(400);
|
||||
assert!(s.keep(key("a"), Arc::new(Pixmap::new(10, 10))));
|
||||
assert!(s.keep(key("b"), Arc::new(Pixmap::new(10, 10))));
|
||||
assert!(s.peek(&key("a")).is_none() && s.peek(&key("b")).is_some());
|
||||
assert_eq!(s.bytes(), 400);
|
||||
// The one kept is held while drawn: a new one that doesn't fit beside it is dropped.
|
||||
let held = s.get(&key("b"));
|
||||
assert!(!s.keep(key("c"), Arc::new(Pixmap::new(10, 10))));
|
||||
assert!(s.peek(&key("c")).is_none());
|
||||
assert_eq!(s.bytes(), 400);
|
||||
drop(held);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn default_budget_is_an_eighth_of_installed_memory_at_most_1_gib() {
|
||||
|
||||
@@ -14,11 +14,12 @@ mod freeform;
|
||||
mod fx;
|
||||
mod group;
|
||||
mod images;
|
||||
pub use images::{image_cached, keep_decoded_image};
|
||||
mod ink;
|
||||
mod live;
|
||||
mod lru;
|
||||
mod paint;
|
||||
pub use paint::{decode_failures, reduced_size};
|
||||
pub use paint::{decode_failures, image_decodes, reduced_size};
|
||||
mod pattern;
|
||||
pub mod placed_document;
|
||||
pub mod proof;
|
||||
|
||||
@@ -75,6 +75,8 @@ thread_local! {
|
||||
/// Images that couldn't be decoded (and so drew nothing) on this thread, counted for exports to
|
||||
/// report.
|
||||
static DECODE_FAILURES: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
|
||||
/// Images decoded, or tried, on this thread ([`image_decodes`]).
|
||||
static DECODES: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -116,6 +118,12 @@ pub fn decode_failures() -> u64 {
|
||||
DECODE_FAILURES.with(std::cell::Cell::get)
|
||||
}
|
||||
|
||||
/// How many images [`decode_pixmap_or_why`] and [`decode_pixmap_fit`] have decoded on this thread so
|
||||
/// far, failed decodes included. Images other threads decode are not counted.
|
||||
pub fn image_decodes() -> u64 {
|
||||
DECODES.with(std::cell::Cell::get)
|
||||
}
|
||||
|
||||
/// [`decode_in_room`]'s image or error.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn decode_within_allowance(bytes: &[u8]) -> image::ImageResult<image::DynamicImage> {
|
||||
@@ -157,6 +165,12 @@ fn room_for(rgba: u64, pinned: u64) -> Result<(), Undecoded> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `w`×`h` pixels may be decoded whole ([`room_for`]) beside the images paints hold now.
|
||||
pub(crate) fn within_allowance(w: u32, h: u32) -> bool {
|
||||
let bytes = (u64::from(w) * u64::from(h)).saturating_mul(4);
|
||||
room_for(bytes, crate::images::store().pinned_bytes() as u64).is_ok()
|
||||
}
|
||||
|
||||
/// Whether `bytes` of decoded pixels fit beside the images paints hold, which can't be evicted from
|
||||
/// the cache: a new image is decoded only while those and it fit twice [`MAX_DECODE_ALLOC`].
|
||||
fn fits_beside_pinned(bytes: u64) -> bool {
|
||||
@@ -397,7 +411,7 @@ pub fn decode_pixmap(bytes: &[u8]) -> Option<vello_cpu::Pixmap> {
|
||||
/// [`reduced_size`] when it has one; a failure is counted in [`decode_failures`].
|
||||
pub(crate) fn decode_pixmap_or_why(bytes: &[u8]) -> Result<vello_cpu::Pixmap, Undecoded> {
|
||||
counted(match decode_or_reduction(bytes) {
|
||||
Ok(img) => pixmap_of(fit_pixmap(img).to_rgba8()).ok_or(Undecoded::Unreadable),
|
||||
Ok(img) => decoded_pixmap(img).ok_or(Undecoded::Unreadable),
|
||||
Err(r) => r.and_then(|r| reduced_pixmap(bytes, &r)),
|
||||
})
|
||||
}
|
||||
@@ -407,6 +421,7 @@ pub(crate) fn decode_pixmap_or_why(bytes: &[u8]) -> Result<vello_cpu::Pixmap, Un
|
||||
fn decode_or_reduction(bytes: &[u8]) -> Result<image::DynamicImage, Result<Reduction, Undecoded>> {
|
||||
#[cfg(test)]
|
||||
DECODE_ATTEMPTS.with(|n| n.set(n.get() + 1));
|
||||
DECODES.with(|n| n.set(n.get() + 1));
|
||||
match decode_in_room(bytes) {
|
||||
Ok(img) => Ok(img),
|
||||
Err((image::ImageError::Limits(_), Undecoded::Unreadable)) => Err(reduction(bytes, allowance()).ok_or(Undecoded::Unreadable)),
|
||||
@@ -471,6 +486,11 @@ fn pixmap_of(img: image::RgbaImage) -> Option<vello_cpu::Pixmap> {
|
||||
Some(vello_cpu::Pixmap::from_parts(data, w as u16, h as u16))
|
||||
}
|
||||
|
||||
/// Decoded image `img` as [`decode_pixmap_or_why`] makes it a pixmap.
|
||||
pub(crate) fn decoded_pixmap(img: image::DynamicImage) -> Option<vello_cpu::Pixmap> {
|
||||
pixmap_of(fit_pixmap(img).to_rgba8())
|
||||
}
|
||||
|
||||
/// The substitution highlights' pink: Preferences › Highlight Substituted Fonts and Highlight
|
||||
/// Alternate Glyphs, and Document Setup's Highlight Substituted Glyphs (screen only).
|
||||
pub(crate) const SUBSTITUTED: peniko::Color = peniko::Color::from_rgba8(255, 120, 190, 110);
|
||||
|
||||
@@ -903,6 +903,39 @@ fn decode_failures_counts_this_threads_images_only() {
|
||||
assert_eq!(decode_failures(), before + 1, "this thread's failure");
|
||||
}
|
||||
|
||||
/// Pixels decoded elsewhere and kept with [`keep_decoded_image`] are the pixels a renderer decodes,
|
||||
/// premultiplied alike, and a render that draws the image uses them without decoding it. A CMYK
|
||||
/// image's pixels are not kept.
|
||||
#[test]
|
||||
fn a_kept_decoded_image_is_drawn_without_decoding_it() {
|
||||
use vectorcraft_doc::cmyk::Inks;
|
||||
use vectorcraft_doc::{ImageBlob, ImageObject, NodeKind};
|
||||
// The image has pixels no other test uses, since every test shares the store, and half of
|
||||
// them are partly transparent.
|
||||
let img = image::RgbaImage::from_fn(37, 23, |x, y| image::Rgba([(x * 7) as u8, (y * 11) as u8, 99, if (x + y) % 2 == 0 { 255 } else { 77 }]));
|
||||
let mut png = vec![];
|
||||
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
let blob = ImageBlob::new("image/png", png.clone());
|
||||
assert!(!image_cached(&blob));
|
||||
assert!(keep_decoded_image(&blob, image::load_from_memory(&png).unwrap()));
|
||||
assert!(image_cached(&blob));
|
||||
let key = blobs::blob_key(&blob).0;
|
||||
let kept = images::store().peek(&key).unwrap();
|
||||
assert!(kept.data() == paint::decode_pixmap(&png).unwrap().data(), "the pixels a renderer decodes");
|
||||
let mut d = Document::new(37.0, 23.0);
|
||||
d.images.insert("kept".into(), blob);
|
||||
let id = d.alloc_id();
|
||||
let im = ImageObject { key: "kept".into(), width: 37, height: 23, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(d.layers[0].id), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let decodes = image_decodes();
|
||||
let r = Renderer::new().render(&d, 37, 23, Affine::IDENTITY, &RenderOptions::default());
|
||||
assert_eq!(image_decodes(), decodes, "drawn from the kept pixels");
|
||||
assert!(r.pixel(18, 11)[3] > 0, "the image is drawn");
|
||||
let cmyk = ImageBlob::cmyk_tiff(&Inks::new(2, 2, [10, 20, 30, 40].repeat(4)).unwrap()).unwrap();
|
||||
assert!(!keep_decoded_image(&cmyk, image::DynamicImage::new_rgba8(2, 2)));
|
||||
assert!(!image_cached(&cmyk));
|
||||
}
|
||||
|
||||
/// An image drawn larger than its pixels samples smoothly, or with `smooth_images` off each screen
|
||||
/// pixel takes its nearest image pixel (Pixel Preview with File Handling › Display Bitmaps as
|
||||
/// Anti-aliased Images off, #394).
|
||||
|
||||
Reference in New Issue
Block a user