From f06f11b52f71df04770baf8714dea3e8ef8cbc98 Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 14 Nov 2026 12:43:11 +0000 Subject: [PATCH] DES-10 review: PDF layer variants, crop boxes and tones read no file on the drawing thread A frame that doesn't wait for images queues the work (reading a linked file, rewriting a PDF with layers hidden, measuring a Bounding Box crop, finding an image's tone) on a background thread and draws a stand-in: decodes cached under a layer variant's key, else the loading box. Each checks images::unavailable first, so a missing or modified link is read once until the links change, and loads happen outside the caches' locks. Kept layer variants have a 256 MiB byte budget besides the count limit. --- crates/render/src/colorize.rs | 116 +++++++++++--- crates/render/src/draw.rs | 2 +- crates/render/src/images.rs | 41 ++++- crates/render/src/lib.rs | 1 + crates/render/src/pdf_layers.rs | 262 ++++++++++++++++++++++++++------ crates/render/src/pdf_raster.rs | 115 ++++++++++++-- crates/render/src/prepare.rs | 165 ++++++++++++++++++++ crates/render/src/stroke.rs | 133 +++++++++++----- 8 files changed, 711 insertions(+), 124 deletions(-) create mode 100644 crates/render/src/prepare.rs diff --git a/crates/render/src/colorize.rs b/crates/render/src/colorize.rs index 606471b25..11b12f6ea 100644 --- a/crates/render/src/colorize.rs +++ b/crates/render/src/colorize.rs @@ -12,34 +12,75 @@ use designcraft_images::Tone; use vello_cpu::Pixmap; use vello_cpu::color::PremulRgba8; +use crate::prepare::Prepared; + +type Tones = HashMap>; +/// Tones remembered; all are forgotten beyond this. +const MAX_TONES: usize = 256; /// The tone of each image file a swatch was applied to. A GIF is decoded to know, so this is /// not done every frame. -fn tone_of(data: &AssetData) -> Option { - type Tones = HashMap>; - const MAX: usize = 256; - static TONES: Mutex> = Mutex::new(None); - let key = data.key(); - if let Some(t) = TONES.lock().unwrap_or_else(PoisonError::into_inner).as_ref().and_then(|m| m.get(&key)) { - return *t; - } - let tone = designcraft_images::tone(&data.load().ok()?); +static TONES: Mutex> = Mutex::new(None); + +fn known_tone(key: SourceKey) -> Option> { + TONES.lock().unwrap_or_else(PoisonError::into_inner).as_ref().and_then(|m| m.get(&key).copied()) +} + +/// Find the tone of `bytes` (named by `key`, outside the lock) and remember it. +fn find_tone(key: SourceKey, bytes: &[u8]) -> Option { + let tone = designcraft_images::tone(bytes); let mut guard = TONES.lock().unwrap_or_else(PoisonError::into_inner); let map = guard.get_or_insert_with(HashMap::new); - if map.len() >= MAX { + if map.len() >= MAX_TONES { map.clear(); } map.insert(key, tone); tone } -/// `pm` (decoded from `data`) coloured by `fill`, cached per image and on-screen colour. `None` -/// when the image keeps its own colours: no swatch, one that no longer exists, or a colour image. -pub(crate) fn colorized(doc: &Document, fill: &Fill, data: &AssetData, pm: &Arc) -> Option> { - if fill.is_none() { - return None; +/// The tone of `data`. With `wait` it is found here (reading a linked file); without, in the +/// background. A linked file that can't be had isn't asked for again until the links change +/// ([`crate::images::unavailable`]). +fn tone_of(data: &AssetData, wait: bool) -> Prepared> { + let key = data.key(); + if let Some(t) = known_tone(key) { + return Prepared::Ready(t); } - let color = doc.resolve_color(&fill.swatch, fill.tint)?; - let tone = tone_of(data)?; + if crate::images::unavailable(data).is_some() { + return Prepared::Failed; + } + if wait { + return crate::images::load_bytes(data, false).map_or(Prepared::Failed, |bytes| Prepared::Ready(find_tone(key, &bytes))); + } + let source = data.clone(); + let queued = crate::prepare::later(crate::prepare::job_key(("tone", key)), move || { + if let Some(bytes) = crate::images::load_bytes(&source, true) { + find_tone(key, &bytes); + } + }); + match queued { + crate::prepare::Queued::Yes => Prepared::Pending, + crate::prepare::Queued::Ran => known_tone(key).map_or(Prepared::Failed, Prepared::Ready), + } +} + +/// `pm` (decoded from `data`) coloured by `fill`, cached per image and on-screen colour; `None` +/// when the image keeps its own colours: no swatch, one that no longer exists, a colour image, +/// or one whose file can't be had. Without `wait`, an image whose tone isn't known yet is +/// [`Prepared::Pending`] while it is found in the background. +pub(crate) fn colorized(doc: &Document, fill: &Fill, data: &AssetData, pm: &Arc, wait: bool) -> Prepared>> { + if fill.is_none() { + return Prepared::Ready(None); + } + let Some(color) = doc.resolve_color(&fill.swatch, fill.tint) else { return Prepared::Ready(None) }; + match tone_of(data, wait) { + Prepared::Ready(Some(tone)) => Prepared::Ready(Some(coloured(color, tone, pm))), + Prepared::Ready(None) | Prepared::Failed => Prepared::Ready(None), + Prepared::Pending => Prepared::Pending, + } +} + +/// `pm`, a `tone` image, coloured `color`. +fn coloured(color: designcraft_color::Color, tone: Tone, pm: &Arc) -> Arc { // The swatch at each ink coverage as the screen shows it (or the plate being previewed). let lut: Vec<[u8; 4]> = (0..=255u8) .map(|i| { @@ -63,7 +104,7 @@ pub(crate) fn colorized(doc: &Document, fill: &Fill, data: &AssetData, pm: &Arc< if let Some((previous, map, _)) = guard.as_ref() { if *previous == settings { if let Some((_, derived)) = map.get(&key) { - return Some(derived.clone()); + return derived.clone(); } } else { *guard = None; @@ -105,5 +146,42 @@ pub(crate) fn colorized(doc: &Document, fill: &Fill, data: &AssetData, pm: &Arc< *used += bytes; } } - Some(out) + out +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::images::tests::linked; + + /// A linked image whose file is missing is read once for its tone, not on every frame; a + /// frame that doesn't wait finds the tone in the background. + #[test] + fn a_tone_is_found_off_the_drawing_thread_and_a_missing_file_is_read_once() { + let generation = designcraft_doc::asset_data::link_generation(); + let missing = linked::file("mem:/missing-tone.png", None, 100); + for _ in 0..5 { + assert!(matches!(tone_of(&missing, true), Prepared::Failed)); + assert!(matches!(tone_of(&missing, false), Prepared::Failed)); + } + if designcraft_doc::asset_data::link_generation() == generation { + assert_eq!(linked::reads("mem:/missing-tone.png"), 1); + } + let mut grey = Vec::new(); + image::GrayImage::from_pixel(4, 4, image::Luma([128])).write_to(&mut std::io::Cursor::new(&mut grey), image::ImageFormat::Png).unwrap(); + let data = linked::file("mem:/grey-tone.png", Some(grey.clone()), grey.len() as u64); + let mut tone = None; + for _ in 0..500 { + match tone_of(&data, false) { + Prepared::Ready(t) => { + tone = t; + break; + } + Prepared::Pending => std::thread::sleep(std::time::Duration::from_millis(10)), + Prepared::Failed => break, + } + } + assert_eq!(tone, designcraft_images::tone(&grey)); + assert!(tone.is_some()); + } } diff --git a/crates/render/src/draw.rs b/crates/render/src/draw.rs index 95e0a159c..70b3e2712 100644 --- a/crates/render/src/draw.rs +++ b/crates/render/src/draw.rs @@ -113,7 +113,7 @@ fn wanted_in(f: &Frame, it: &Item, parent: Affine, depth: usize, out: &mut Vec