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.
This commit is contained in:
aiongg committed 2026-11-14 12:48:13 +00:00
1 parent 12e0e2341c
commit f06f11b52f
8 files changed
+711 -124

No files matched your search

+97 -19
View File
@@ -12,34 +12,75 @@ use designcraft_images::Tone;
use vello_cpu::Pixmap;
use vello_cpu::color::PremulRgba8;
use crate::prepare::Prepared;
type Tones = HashMap<SourceKey, Option<Tone>>;
/// 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<Tone> {
type Tones = HashMap<SourceKey, Option<Tone>>;
const MAX: usize = 256;
static TONES: Mutex<Option<Tones>> = 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<Option<Tones>> = Mutex::new(None);
fn known_tone(key: SourceKey) -> Option<Option<Tone>> {
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<Tone> {
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<Pixmap>) -> Option<Arc<Pixmap>> {
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<Option<Tone>> {
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<Pixmap>, wait: bool) -> Prepared<Option<Arc<Pixmap>>> {
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<Pixmap>) -> Arc<Pixmap> {
// 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());
}
}
+1 -1
View File
@@ -113,7 +113,7 @@ fn wanted_in(f: &Frame, it: &Item, parent: Affine, depth: usize, out: &mut Vec<i
let Some(asset) = f.doc.assets.get(&g.asset) else { return };
let hidden = it.has_pdf_layer_override().then(|| it.pdf_hidden_layers.clone());
let previous = HIDDEN_LAYERS.with(|h| h.replace(hidden));
if let Some((want, ..)) = image_want(f, asset, g, xf) {
if let Some((crate::stroke::Source::Want(want), ..)) = image_want(f, asset, g, xf) {
out.push(want);
}
HIDDEN_LAYERS.with(|h| h.replace(previous));
+34 -7
View File
@@ -161,7 +161,7 @@ pub fn unavailable(data: &AssetData) -> Option<Unavailable> {
/// The bytes of `data` to decode. Linked bytes the store doesn't hold are loaded, `gate`d at
/// [`GATE`] for their size; a failure is remembered ([`unavailable_now`]).
fn load_bytes(data: &AssetData, gate: bool) -> Option<Arc<Vec<u8>>> {
pub(crate) fn load_bytes(data: &AssetData, gate: bool) -> Option<Arc<Vec<u8>>> {
if let Some(bytes) = data.resident() {
return Some(bytes);
}
@@ -487,6 +487,33 @@ pub fn mip_now(data: &AssetData, page: u32, width_pt: f64, on_screen: f64) -> Mi
Mip::Pending(stand_in)
}
/// What is cached of the bytes `id` names, page `page`, drawn `width_pt` points wide at
/// `on_screen` device pixels per point, without needing the bytes: the level asked for (`true`),
/// else the nearest coarser, else the nearest finer one.
pub(crate) fn cached(id: SourceKey, page: u32, width_pt: f64, on_screen: f64) -> Option<(Arc<Pixmap>, bool)> {
let Some(level) = known_dims(id, page).map(|size| level_for(size, width_pt, on_screen)) else {
return (0..=MAX_LEVEL).find_map(|l| lookup((id, page, l))).map(|pm| (pm, false));
};
if let Some(pm) = lookup((id, page, level)) {
return Some((pm, true));
}
(level + 1..=MAX_LEVEL).find_map(|c| lookup((id, page, c))).or_else(|| (0..level).rev().find_map(|f| lookup((id, page, f)))).map(|pm| (pm, false))
}
/// Count this frame as waiting for images ([`take_deferred`]): work it needs is being done in
/// the background.
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn defer() {
DEFERRED.with(|d| d.set(true));
}
/// Advance [`generation`] (and run the waker): background work a deferred frame waited for is
/// done.
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn advance() {
background::advance();
}
/// Advances each time a background decode queued by [`mip_now`] finishes.
pub fn generation() -> u64 {
background::GENERATION.load(Ordering::Acquire)
@@ -687,7 +714,7 @@ mod background {
}
/// Advance the generation and run the waker.
fn advance() {
pub(super) fn advance() {
GENERATION.fetch_add(1, Ordering::AcqRel);
let waker = WAKER.lock().unwrap_or_else(PoisonError::into_inner).clone();
if let Some(wake) = waker {
@@ -1177,7 +1204,7 @@ pub fn clear() {
}
#[cfg(test)]
mod tests {
pub(crate) mod tests {
use super::*;
use crate::decode::decoded_bytes;
@@ -1661,7 +1688,7 @@ mod tests {
}
/// Linked files for these tests: bytes by path, served by a loader that counts its reads.
mod linked {
pub(crate) mod linked {
use super::*;
use designcraft_doc::asset_data::{Loaded, Loader};
use designcraft_doc::{Fingerprint, LinkedSource};
@@ -1681,7 +1708,7 @@ mod tests {
}
/// A linked asset whose file is `bytes` (`None`: missing for now) at the unique `path`.
pub(super) fn file(path: &str, bytes: Option<Vec<u8>>, len: u64) -> AssetData {
pub(crate) fn file(path: &str, bytes: Option<Vec<u8>>, len: u64) -> AssetData {
static INSTALL: std::sync::Once = std::sync::Once::new();
INSTALL.call_once(|| designcraft_doc::asset_data::install_loader(Arc::new(Files)));
if let Some(b) = bytes {
@@ -1690,11 +1717,11 @@ mod tests {
AssetData::linked(LinkedSource { path: path.into(), relative: None, document: None, len, fingerprint: None, hash: None })
}
pub(super) fn put(path: &str, bytes: Vec<u8>) {
pub(crate) fn put(path: &str, bytes: Vec<u8>) {
FILES.lock().unwrap().get_or_insert_with(HashMap::new).insert(path.into(), bytes);
}
pub(super) fn reads(path: &str) -> usize {
pub(crate) fn reads(path: &str) -> usize {
READS.lock().unwrap().iter().filter(|p| *p == path).count()
}
}
+1
View File
@@ -39,6 +39,7 @@ mod decode;
mod draw;
mod driver;
mod pdf_raster;
mod prepare;
mod stroke;
mod types;
+215 -47
View File
@@ -239,68 +239,166 @@ pub fn pdf_hide_layers(bytes: &[u8], hidden: &[String]) -> Option<Vec<u8>> {
Some(out)
}
/// What [`layered`] has for a placed PDF with some layers hidden.
pub(crate) enum Layered {
/// The variant.
Ready(AssetData),
/// Being made in the background, or not needed: what is cached of an earlier copy of it
/// (decodes under this key) is drawn meanwhile.
Making(Option<SourceKey>),
/// The file can't be rewritten with these layers off, or can't be had.
Failed,
}
/// A source's bytes and the layers hidden in them.
type Key = (SourceKey, Vec<String>);
enum Variant {
/// The file couldn't be rewritten with these layers off.
Failed,
Made(WeakAssetData),
}
/// Shared copies of layer variants (by source and hidden set). A variant keeps its identity
/// (the same [`SourceKey`]) across rebuilds: the image cache keys decodes by it, so a variant
/// rebuilt under a new key would miss decodes it already has and be decoded again. The bytes of
/// a variant are a function of its source's and the hidden set, so the same key always names the
/// same bytes. Recent variants are held here; older ones are made again when asked for.
pub(crate) fn layered(data: &AssetData, hidden: &[String]) -> Option<AssetData> {
type Key = (SourceKey, Vec<String>);
enum Variant {
/// The file couldn't be rewritten with these layers off.
Failed,
Made(WeakAssetData),
}
type Known = HashMap<Key, Variant>;
static CACHE: Mutex<Option<(Known, VecDeque<AssetData>)>> = Mutex::new(None);
/// How many recent variants are kept alive here (each is a copy of its file).
const KEEP: usize = 64;
let key = (data.key(), hidden.to_vec());
let mut c = CACHE.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let (known, keep) = c.get_or_insert_with(Default::default);
let id = match known.get(&key) {
Some(Variant::Failed) => return None,
Some(Variant::Made(weak)) => match weak.upgrade() {
Some(v) => return Some(v),
None => weak.key(),
},
None => SourceKey::fresh(),
};
// A source that can't be loaded now isn't a file that can't be rewritten: asked again later.
let bytes = data.load().ok()?;
let v = pdf_hide_layers(&bytes, hidden).map(|b| AssetData::embedded_under(id, b));
known.insert(key, v.as_ref().map_or(Variant::Failed, |v| Variant::Made(v.downgrade())));
if let Some(v) = &v {
keep.push_back(v.clone());
while keep.len() > KEEP {
keep.pop_front();
#[derive(Default)]
struct Variants {
known: HashMap<Key, Variant>,
/// Variants kept alive, oldest first.
keep: VecDeque<AssetData>,
/// Their bytes.
kept_bytes: u64,
}
/// How many recent variants are kept alive here.
const KEEP: usize = 64;
/// The bytes of the variants kept alive (each is a copy of its file); the newest is kept
/// whatever its size, so the one just made is there to draw.
const KEEP_BYTES: u64 = 256 << 20;
static VARIANTS: Mutex<Option<Variants>> = Mutex::new(None);
fn with<R>(f: impl FnOnce(&mut Variants) -> R) -> R {
f(VARIANTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get_or_insert_with(Default::default))
}
impl Variants {
fn keep(&mut self, v: AssetData) {
self.kept_bytes = self.kept_bytes.saturating_add(v.len());
self.keep.push_back(v);
while self.keep.len() > KEEP || (self.kept_bytes > KEEP_BYTES && self.keep.len() > 1) {
let Some(old) = self.keep.pop_front() else { break };
self.kept_bytes = self.kept_bytes.saturating_sub(old.len());
}
}
// Forget variants nothing holds and nothing decoded is cached of.
if known.len() > 4 * KEEP {
known.retain(|_, variant| match variant {
/// Forget variants nothing holds and nothing decoded is cached of.
fn forget(&mut self) {
if self.known.len() <= 4 * KEEP {
return;
}
self.known.retain(|_, variant| match variant {
Variant::Failed => true,
Variant::Made(weak) => weak.upgrade().is_some() || crate::images::has_decodes(weak.key()),
});
// Failures are cheap to find again; a hostile run of hidden sets doesn't grow this.
if known.len() > 4 * KEEP {
known.retain(|_, variant| matches!(variant, Variant::Made(_)));
if self.known.len() > 4 * KEEP {
self.known.retain(|_, variant| matches!(variant, Variant::Made(_)));
}
if known.len() > 4 * KEEP {
known.retain(|_, variant| matches!(variant, Variant::Made(weak) if weak.upgrade().is_some()));
if self.known.len() > 4 * KEEP {
self.known.retain(|_, variant| matches!(variant, Variant::Made(weak) if weak.upgrade().is_some()));
}
}
v
}
/// What is known of the variant `key` names.
enum Found {
Ready(AssetData),
Failed,
/// Made before and let go since, under this key.
Gone(SourceKey),
Unknown,
}
fn find(key: &Key) -> Found {
with(|v| match v.known.get(key) {
Some(Variant::Failed) => Found::Failed,
Some(Variant::Made(weak)) => weak.upgrade().map_or_else(|| Found::Gone(weak.key()), Found::Ready),
None => Found::Unknown,
})
}
/// Make the variant `key` names from its source's `bytes` (outside the lock) and remember it.
/// A variant another thread made meanwhile is the one returned.
fn make(key: Key, bytes: &[u8]) -> Option<AssetData> {
let made = pdf_hide_layers(bytes, &key.1);
with(|v| {
let id = match v.known.get(&key) {
Some(Variant::Made(weak)) => match weak.upgrade() {
Some(live) => return Some(live),
None => weak.key(),
},
_ => SourceKey::fresh(),
};
let made = made.map(|b| AssetData::embedded_under(id, b));
v.known.insert(key, made.as_ref().map_or(Variant::Failed, |m| Variant::Made(m.downgrade())));
if let Some(m) = &made {
v.keep(m.clone());
}
v.forget();
made
})
}
/// The variant of `data` (a placed PDF) with the layers named in `hidden` off. With `wait`, it is
/// made here if need be (reading a linked file). Without, nothing is read or rewritten on this
/// thread: the variant is made in the background ([`crate::prepare`]), unless one made before
/// is gone and what is cached of it (decodes under its key) is `covered` enough to draw. A
/// linked file that can't be had isn't asked for again until the links change
/// ([`crate::images::unavailable`]).
pub(crate) fn layered(data: &AssetData, hidden: &[String], wait: bool, covered: impl Fn(SourceKey) -> bool) -> Layered {
let key = (data.key(), hidden.to_vec());
let gone = match find(&key) {
Found::Ready(v) => return Layered::Ready(v),
Found::Failed => return Layered::Failed,
Found::Gone(id) => Some(id),
Found::Unknown => None,
};
if crate::images::unavailable(data).is_some() {
return Layered::Failed;
}
if wait {
return crate::images::load_bytes(data, false).and_then(|bytes| make(key, &bytes)).map_or(Layered::Failed, Layered::Ready);
}
if gone.is_some_and(&covered) {
return Layered::Making(gone);
}
let (source, job) = (data.clone(), key.clone());
let queued = crate::prepare::later(crate::prepare::job_key(("pdf layers", &key)), move || {
if let Some(bytes) = crate::images::load_bytes(&source, true) {
make(job, &bytes);
}
});
match queued {
crate::prepare::Queued::Yes => Layered::Making(gone),
crate::prepare::Queued::Ran => match find(&key) {
Found::Ready(v) => Layered::Ready(v),
_ => Layered::Failed,
},
}
}
#[cfg(test)]
mod tests {
pub(crate) mod tests {
use super::*;
use craft_pdf::syntax::reader::{Reader, ReaderExt};
use std::sync::Arc;
/// A one-page PDF with two layers, Blue and Red.
fn two_layer_pdf() -> Vec<u8> {
pub(crate) fn two_layer_pdf() -> Vec<u8> {
let content = "/OC /a BDC 0 0 1 rg 0 0 50 100 re f EMC /OC /b BDC 1 0 0 rg 50 0 50 100 re f EMC";
let objs = [
"<</Type/Catalog/Pages 2 0 R/OCProperties<</OCGs[5 0 R 6 0 R]/D<</Order[5 0 R 6 0 R]/Name(Default)>>>>>>".to_string(),
@@ -326,34 +424,104 @@ mod tests {
out
}
/// [`layered`], waiting.
fn made(data: &AssetData, hidden: &[String]) -> Option<AssetData> {
match layered(data, hidden, true, |_| false) {
Layered::Ready(v) => Some(v),
Layered::Making(_) | Layered::Failed => None,
}
}
/// A variant something still holds comes back under the same key however many others were
/// made since, so decodes keyed by it keep matching; one nothing holds is made again.
#[test]
fn a_held_layer_variant_keeps_its_identity_past_the_cache() {
let data = AssetData::embedded(two_layer_pdf());
let blue = vec!["Blue".to_string()];
let first = layered(&data, &blue).expect("Blue can be hidden");
assert_eq!(first.key(), layered(&data, &blue).unwrap().key());
let first = made(&data, &blue).expect("Blue can be hidden");
assert_eq!(first.key(), made(&data, &blue).unwrap().key());
// More variants than are kept, each with a set of its own.
for i in 0..200 {
let _ = layered(&data, &["Red".to_string(), format!("unknown {i}")]);
let _ = made(&data, &["Red".to_string(), format!("unknown {i}")]);
}
assert_eq!(first.key(), layered(&data, &blue).unwrap().key(), "still the same variant");
assert_eq!(first.key(), made(&data, &blue).unwrap().key(), "still the same variant");
let red = vec!["Red".to_string()];
let variant = layered(&data, &red).unwrap();
let variant = made(&data, &red).unwrap();
assert!(crate::images::mip(&variant, 0, 100.0, 1.0).is_some(), "the variant draws");
let weak = variant.downgrade();
drop(variant);
for i in 0..300 {
let _ = layered(&data, &["Blue".to_string(), format!("other {i}")]);
let _ = made(&data, &["Blue".to_string(), format!("other {i}")]);
}
assert!(weak.upgrade().is_none(), "a variant nothing holds is let go");
let again = layered(&data, &red).expect("made again when asked for");
let again = made(&data, &red).expect("made again when asked for");
assert_eq!(again.key(), weak.key(), "under the key its decodes are cached by");
assert!(crate::images::has_decodes(again.key()));
assert_eq!(again.load().unwrap(), pdf_hide_layers(&data.load().unwrap(), &red).map(Arc::new).unwrap());
}
/// The variants kept alive have a byte budget: a big one is let go once others are made,
/// though the newest is always kept.
#[test]
fn kept_variants_are_bounded_by_bytes() {
let mut v = Variants::default();
let big = || AssetData::embedded(vec![0u8; (KEEP_BYTES / 2 + 1) as usize]);
let first = big();
v.keep(first.clone());
assert_eq!(v.keep.len(), 1, "the newest is kept");
v.keep(big());
assert_eq!(v.keep.len(), 1, "two don't fit");
assert!(v.keep.iter().all(|k| k.key() != first.key()), "the older is let go");
assert!(v.kept_bytes <= KEEP_BYTES);
let huge = AssetData::embedded(vec![0u8; (KEEP_BYTES + 1) as usize]);
v.keep(huge.clone());
assert_eq!(v.keep.iter().map(AssetData::key).collect::<Vec<_>>(), [huge.key()], "kept whatever its size while newest");
}
/// A frame that doesn't wait reads no linked file: the variant is made in the background. A
/// linked file that can't be had is read once, not on every frame after.
#[test]
fn a_linked_variant_is_made_off_the_drawing_thread_and_a_missing_file_is_not_read_each_frame() {
use crate::images::tests::linked;
let bytes = two_layer_pdf();
let blue = vec!["Blue".to_string()];
let data = linked::file("mem:/layers.pdf", Some(bytes.clone()), bytes.len() as u64);
let mut ready = None;
for _ in 0..500 {
match layered(&data, &blue, false, |_| false) {
Layered::Ready(v) => {
ready = Some(v);
break;
}
Layered::Making(_) => std::thread::sleep(std::time::Duration::from_millis(10)),
Layered::Failed => break,
}
}
let ready = ready.expect("made in the background");
assert_eq!(ready.load().unwrap(), pdf_hide_layers(&bytes, &blue).map(Arc::new).unwrap());
assert_eq!(linked::reads("mem:/layers.pdf"), 1);
let missing = linked::file("mem:/missing-layers.pdf", None, bytes.len() as u64);
// Other tests say the links changed, which asks for missing files again.
let generation = designcraft_doc::asset_data::link_generation();
let mut failed = false;
for _ in 0..500 {
if matches!(layered(&missing, &blue, false, |_| false), Layered::Failed) {
failed = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert!(failed, "a missing file fails");
for _ in 0..10 {
assert!(matches!(layered(&missing, &blue, false, |_| false), Layered::Failed));
assert!(matches!(layered(&missing, &blue, true, |_| false), Layered::Failed));
}
if designcraft_doc::asset_data::link_generation() == generation {
assert_eq!(linked::reads("mem:/missing-layers.pdf"), 1, "not read again before the links change");
}
}
#[test]
fn trailer_lookup_does_not_take_guard_values_from_xref_comments() {
for guard in ["/Encrypt 12 0 R", "/XRefStm 123"] {
+99 -16
View File
@@ -1,5 +1,6 @@
use super::types::Rendered;
use super::{Affine, HashMap, Pixmap, Rect, pdf_bounds, pdf_hide_layers, pdf_layers, vello_cpu};
use crate::prepare::Prepared;
/// MIME type guess for encoded image bytes.
/// Is this a PDF file (placed PDFs are graphics)?
@@ -226,35 +227,74 @@ pub fn pdf_crop_box(bytes: &[u8], page: usize, crop: designcraft_doc::PdfCrop) -
/// Box crop that import left unmeasured is measured here, once per file, page and crop: that
/// interprets the page's drawing, which only drawing or exporting the graphic needs.
pub fn shown_box(asset: &designcraft_doc::Asset) -> Option<[f64; 4]> {
match shown_box_now(asset, true) {
Prepared::Ready(b) => b,
Prepared::Pending | Prepared::Failed => None,
}
}
/// [`shown_box`]; without `wait`, a crop still to measure is measured in the background
/// ([`Prepared::Pending`]).
pub(crate) fn shown_box_now(asset: &designcraft_doc::Asset, wait: bool) -> Prepared<Option<[f64; 4]>> {
if !asset.content_box_pending() {
return asset.shown_box();
return Prepared::Ready(asset.shown_box());
}
match content_box(&asset.data, asset.page, asset.pdf_crop, wait) {
Prepared::Ready(b) => Prepared::Ready(asset.shown(b)),
Prepared::Pending => Prepared::Pending,
Prepared::Failed => Prepared::Ready(None),
}
asset.shown(content_box(&asset.data, asset.page, asset.pdf_crop)?)
}
type ContentBoxes = HashMap<(designcraft_doc::SourceKey, u32, designcraft_doc::PdfCrop), Option<[f64; 4]>>;
/// [`pdf_crop_box`]'s box, cached by the file's key.
fn content_box(data: &designcraft_doc::AssetData, page: u32, crop: designcraft_doc::PdfCrop) -> Option<[f64; 4]> {
/// Boxes remembered; all are forgotten beyond this (measuring one again costs a render).
const MAX: usize = 1024;
static CACHE: std::sync::Mutex<Option<ContentBoxes>> = std::sync::Mutex::new(None);
let key = (data.key(), page, crop);
let cached = CACHE.lock().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref().and_then(|c| c.get(&key).copied());
if let Some(b) = cached {
return b;
}
// Render outside the lock: other graphics keep drawing meanwhile.
let b = pdf_crop_box(&data.load().ok()?, page as usize, crop).map(|(frac, _)| frac);
let mut cache = CACHE.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
/// Boxes remembered; all are forgotten beyond this (measuring one again costs a render).
const MAX_CONTENT_BOXES: usize = 1024;
static CONTENT_BOXES: std::sync::Mutex<Option<ContentBoxes>> = std::sync::Mutex::new(None);
fn known_content_box(key: &(designcraft_doc::SourceKey, u32, designcraft_doc::PdfCrop)) -> Option<Option<[f64; 4]>> {
CONTENT_BOXES.lock().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref().and_then(|c| c.get(key).copied())
}
/// Measure the box `key` names in `bytes` (outside the lock) and remember it.
fn measure_content_box(key: (designcraft_doc::SourceKey, u32, designcraft_doc::PdfCrop), bytes: &[u8]) -> Option<[f64; 4]> {
let b = pdf_crop_box(bytes, key.1 as usize, key.2).map(|(frac, _)| frac);
let mut cache = CONTENT_BOXES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let cache = cache.get_or_insert_with(HashMap::new);
if cache.len() >= MAX {
if cache.len() >= MAX_CONTENT_BOXES {
cache.clear();
}
cache.insert(key, b);
b
}
/// [`pdf_crop_box`]'s box, cached by the file's key. With `wait` it is measured 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 content_box(data: &designcraft_doc::AssetData, page: u32, crop: designcraft_doc::PdfCrop, wait: bool) -> Prepared<[f64; 4]> {
let key = (data.key(), page, crop);
let known = |b: Option<[f64; 4]>| b.map_or(Prepared::Failed, Prepared::Ready);
if let Some(b) = known_content_box(&key) {
return known(b);
}
if crate::images::unavailable(data).is_some() {
return Prepared::Failed;
}
if wait {
return crate::images::load_bytes(data, false).map_or(Prepared::Failed, |bytes| known(measure_content_box(key, &bytes)));
}
let source = data.clone();
let queued = crate::prepare::later(crate::prepare::job_key(("content box", key)), move || {
if let Some(bytes) = crate::images::load_bytes(&source, true) {
measure_content_box(key, &bytes);
}
});
match queued {
crate::prepare::Queued::Yes => Prepared::Pending,
crate::prepare::Queued::Ran => known_content_box(&key).map_or(Prepared::Failed, known),
}
}
/// The part of `pm` inside `frac` (left, top, right, bottom as fractions of it); what lies outside
/// `pm` is transparent.
pub fn crop_pixmap(pm: &Pixmap, frac: [f64; 4]) -> Option<Pixmap> {
@@ -304,4 +344,47 @@ mod crop_tests {
// 60,000 × 60,000 pixels: within u16 each way, far past the page raster budget.
assert!(crop_pixmap(&pm, [-3000.0, -3000.0, 3000.0, 3000.0]).is_none());
}
/// A Bounding Box crop of a linked PDF is measured off the drawing thread, and one whose file
/// is missing is read once, not on every frame.
#[test]
fn a_linked_crop_is_measured_off_the_drawing_thread_and_a_missing_file_is_read_once() {
use crate::images::tests::linked;
use designcraft_doc::PdfCrop;
let generation = designcraft_doc::asset_data::link_generation();
let missing = linked::file("mem:/missing-crop.pdf", None, 100);
let mut failed = false;
for _ in 0..500 {
match content_box(&missing, 0, PdfCrop::ContentVisible, false) {
Prepared::Failed => {
failed = true;
break;
}
Prepared::Pending => std::thread::sleep(std::time::Duration::from_millis(10)),
Prepared::Ready(_) => break,
}
}
assert!(failed);
for _ in 0..5 {
assert!(matches!(content_box(&missing, 0, PdfCrop::ContentVisible, true), Prepared::Failed));
assert!(matches!(content_box(&missing, 0, PdfCrop::ContentVisible, false), Prepared::Failed));
}
if designcraft_doc::asset_data::link_generation() == generation {
assert_eq!(linked::reads("mem:/missing-crop.pdf"), 1);
}
let pdf = crate::pdf_layers::tests::two_layer_pdf();
let data = linked::file("mem:/crop.pdf", Some(pdf.clone()), pdf.len() as u64);
let mut measured = None;
for _ in 0..500 {
match content_box(&data, 0, PdfCrop::ContentVisible, false) {
Prepared::Ready(b) => {
measured = Some(b);
break;
}
Prepared::Pending => std::thread::sleep(std::time::Duration::from_millis(10)),
Prepared::Failed => break,
}
}
assert_eq!(measured, pdf_crop_box(&pdf, 0, PdfCrop::ContentVisible).map(|(frac, _)| frac));
}
}
+165
View File
@@ -0,0 +1,165 @@
//! Work a graphic needs before it draws, other than decoding its pixels: rewriting a placed PDF
//! with layers hidden, measuring a Bounding Box crop, finding an image's tone. Each needs the
//! file's bytes, and for a linked file that means reading it. A frame that doesn't wait for
//! images ([`crate::RenderOptions::defer_images`]) never does this work on its own thread: it
//! queues it here ([`later`]), draws a stand-in, and draws again when
//! [`crate::images::generation`] advances.
use std::hash::{DefaultHasher, Hash, Hasher};
/// The outcome of work a graphic needs before it draws.
pub(crate) enum Prepared<T> {
Ready(T),
/// Queued ([`later`]); the frame is drawn again when it is done.
Pending,
/// It can't be done: the file is unreadable or can't be had.
Failed,
}
/// Names one piece of work, so a request made each frame while it waits is queued once.
pub(crate) fn job_key(what: impl Hash) -> u64 {
let mut h = DefaultHasher::new();
what.hash(&mut h);
h.finish()
}
/// What [`later`] did with a job.
pub(crate) enum Queued {
/// Queued, or already running: the frame counts as waiting for images.
Yes,
/// Done: it ran now (no background thread can run it), or it panicked before and isn't run
/// again. What it made, if anything, is where it records it.
Ran,
}
/// Run `job` on a background thread, at most once while it is queued or running under `key`.
/// The image generation advances when it is done.
pub(crate) fn later(key: u64, job: impl FnOnce() + Send + 'static) -> Queued {
background::queue(key, Box::new(job))
}
#[cfg(not(target_arch = "wasm32"))]
mod background {
use std::collections::VecDeque;
use std::sync::{Condvar, Mutex, PoisonError};
use super::Queued;
type Job = Box<dyn FnOnce() + Send>;
/// Threads: the work reads files, which may be slow, so one slow read doesn't hold up the rest.
const THREADS: usize = 2;
/// Jobs kept waiting; the oldest are dropped beyond this (asked for again if still in view).
const MAX_QUEUED: usize = 64;
/// Jobs remembered as having panicked; the oldest is forgotten beyond this.
const MAX_PANICKED: usize = 64;
struct Queue {
jobs: VecDeque<(u64, Job)>,
busy: Vec<u64>,
threads: usize,
/// Jobs that panicked: they record nothing, and run again they would panic every frame.
panicked: VecDeque<u64>,
}
impl Queue {
fn panicked(&mut self, key: u64) {
self.panicked.push_back(key);
while self.panicked.len() > MAX_PANICKED {
self.panicked.pop_front();
}
}
}
static QUEUE: Mutex<Queue> = Mutex::new(Queue { jobs: VecDeque::new(), busy: Vec::new(), threads: 0, panicked: VecDeque::new() });
static READY: Condvar = Condvar::new();
fn lock() -> std::sync::MutexGuard<'static, Queue> {
QUEUE.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(super) fn queue(key: u64, job: Job) -> Queued {
let mut q = lock();
if q.panicked.contains(&key) {
return Queued::Ran;
}
if q.busy.contains(&key) || q.jobs.iter().any(|(k, _)| *k == key) {
crate::images::defer();
return Queued::Yes;
}
if q.threads < THREADS {
if std::thread::Builder::new().name("designcraft-prepare".into()).spawn(run).is_ok() {
q.threads += 1;
} else if q.threads == 0 {
drop(q);
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(job)).is_err() {
lock().panicked(key);
}
return Queued::Ran;
}
}
q.jobs.push_back((key, job));
while q.jobs.len() > MAX_QUEUED {
q.jobs.pop_front();
}
READY.notify_one();
crate::images::defer();
Queued::Yes
}
fn next() -> (u64, Job) {
let mut q = lock();
loop {
// Newest first: what was just scrolled into view is prepared before what was scrolled past.
if let Some((key, job)) = q.jobs.pop_back() {
q.busy.push(key);
return (key, job);
}
q = READY.wait(q).unwrap_or_else(PoisonError::into_inner);
}
}
/// Takes a job off the busy list when dropped, however it ended.
struct Busy(u64);
impl Drop for Busy {
fn drop(&mut self) {
lock().busy.retain(|k| *k != self.0);
}
}
/// Counts its thread out when dropped, so a new one is started for the next job.
struct Alive;
impl Drop for Alive {
fn drop(&mut self) {
let mut q = lock();
q.threads = q.threads.saturating_sub(1);
}
}
fn run() {
let _alive = Alive;
loop {
let (key, job) = next();
let busy = Busy(key);
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(job)).is_err() {
lock().panicked(key);
}
drop(busy);
crate::images::advance();
}
}
}
#[cfg(target_arch = "wasm32")]
mod background {
use super::Queued;
/// No threads: run in place.
pub(super) fn queue(_: u64, job: Box<dyn FnOnce() + Send>) -> Queued {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(job));
Queued::Ran
}
}
+99 -34
View File
@@ -1,35 +1,64 @@
use super::color::{HIDDEN_LAYERS, PLATE, color_of, overprints, plate_pixmap, screen_color, set_fill_paint};
use super::pdf_raster::{is_pdf, shown_box};
use super::pdf_raster::{is_pdf, shown_box_now};
use super::prepare::Prepared;
use super::types::{DisplayQuality, Frame};
use super::{
Affine, BezPath, BlendMode, Compose, Item, Mix, Rect, RenderContext, Renderer, Shape, StrokeAlign, StrokeType, colorize, images, kurbo,
pdf_layers, peniko, vello_cpu,
};
/// The pixels drawing placed graphic `g` takes from the image cache, the page rectangle they
/// cover in graphic space and that page's height (points). `None` for video and sound, Fast
/// display and a PDF whose hidden layers can't be applied (none of these draw image pixels).
pub(crate) fn image_want(f: &Frame, asset: &designcraft_doc::Asset, g: &designcraft_doc::Graphic, xf: Affine) -> Option<(images::Want, Rect, f64)> {
/// What a placed graphic takes from the image cache.
pub(crate) enum Source {
/// The pixels of these bytes.
Want(images::Want),
/// The bytes to draw aren't known yet: a PDF layer variant is being made, or the crop of a
/// placed PDF measured, in the background. What is cached under `cached` (an earlier copy of
/// the variant) is drawn meanwhile, else a light box.
Waiting { cached: Option<designcraft_doc::SourceKey>, page: u32, width_pt: f64, on_screen: f64 },
}
/// What drawing placed graphic `g` takes from the image cache, the page rectangle it covers in
/// graphic space and that page's height (points). `None` for video and sound, Fast display and a
/// PDF whose hidden layers can't be applied (none of these draw image pixels). A frame that
/// doesn't wait for images reads no file and rewrites none here.
pub(crate) fn image_want(f: &Frame, asset: &designcraft_doc::Asset, g: &designcraft_doc::Graphic, xf: Affine) -> Option<(Source, Rect, f64)> {
if designcraft_doc::media_kind(&asset.mime).is_some() || f.opts.quality == DisplayQuality::Fast {
return None;
}
let wait = !f.opts.defer_images;
// Pick a mip level close to the on-screen size (Typical: a 72 ppi proxy).
let on_screen = (f.view * xf * g.xf).determinant().abs().sqrt();
let on_screen = if f.opts.quality == DisplayQuality::Typical { on_screen.min(1.0) } else { on_screen };
// A placed PDF cropped to a box: the whole page, placed so that the box spans graphic space.
let shown = match shown_box_now(asset, wait) {
Prepared::Ready(b) => b,
Prepared::Failed => None,
Prepared::Pending => {
let waiting = Source::Waiting { cached: None, page: asset.page, width_pt: g.size.0, on_screen };
return Some((waiting, Rect::new(0.0, 0.0, g.size.0, g.size.1), g.size.1));
}
};
let [l, t, r, b] = shown.unwrap_or([0.0, 0.0, 1.0, 1.0]);
let (page_w, page_h) = (g.size.0 / (r - l), g.size.1 / (b - t));
let page = Rect::new(-l * page_w, -t * page_h, (1.0 - l) * page_w, (1.0 - t) * page_h);
let hidden = HIDDEN_LAYERS.with(|h| h.borrow().clone());
// Linked bytes not in memory aren't loaded to ask: their type says.
let pdf = asset.data.resident().map_or(asset.mime == "application/pdf", |b| is_pdf(&b));
let data = match hidden.as_deref().filter(|_| pdf) {
let want = |bytes| Source::Want(images::Want { bytes, page: asset.page, width_pt: page_w, on_screen });
let source = match hidden.as_deref().filter(|_| pdf) {
// An explicit choice (including all-on) that cannot be rewritten fails closed:
// stale persisted/relinked choices must not reveal hidden source content.
Some(hidden) => pdf_layers::layered(&asset.data, hidden)?,
None => asset.data.clone(),
Some(hidden) => {
let covered = |id| images::cached(id, asset.page, page_w, on_screen).is_some_and(|(_, exact)| exact);
match pdf_layers::layered(&asset.data, hidden, wait, covered) {
pdf_layers::Layered::Ready(v) => want(v),
pdf_layers::Layered::Making(cached) => Source::Waiting { cached, page: asset.page, width_pt: page_w, on_screen },
pdf_layers::Layered::Failed => return None,
}
}
None => want(asset.data.clone()),
};
// A placed PDF cropped to a box: the whole page, placed so that the box spans graphic space.
let [l, t, r, b] = shown_box(asset).unwrap_or([0.0, 0.0, 1.0, 1.0]);
let (page_w, page_h) = (g.size.0 / (r - l), g.size.1 / (b - t));
let page = Rect::new(-l * page_w, -t * page_h, (1.0 - l) * page_w, (1.0 - t) * page_h);
Some((images::Want { bytes: data, page: asset.page, width_pt: page_w, on_screen }, page, page_h))
Some((source, page, page_h))
}
/// What a placed graphic draws.
@@ -228,38 +257,74 @@ impl Renderer {
ctx.fill_rect(&Rect::new(0.0, 0.0, g.size.0, g.size.1));
return;
}
let Some((want, page, page_h)) = image_want(f, asset, g, xf) else { return };
let Some((source, page, page_h)) = image_want(f, asset, g, xf) else { return };
let loading = |ctx: &mut RenderContext| {
ctx.set_transform(f.view * xf * g.xf);
ctx.set_paint(screen_color([222, 222, 222], 255));
ctx.fill_rect(&Rect::new(0.0, 0.0, g.size.0, g.size.1).intersect(page));
};
let want = match source {
Source::Want(want) => want,
Source::Waiting { cached, page: pdf_page, width_pt, on_screen } => {
// A layer variant: a PDF, which takes no swatch.
match cached.and_then(|id| images::cached(id, pdf_page, width_pt, on_screen)) {
Some((pm, _)) => {
let pm = match PLATE.with(std::cell::Cell::get) {
Some(plate) => plate_pixmap(&pm, plate),
None => pm,
};
paint_pixels(ctx, f, g, xf, page, width_pt, page_h, pm);
}
None => loading(ctx),
}
return;
}
};
let page_w = want.width_pt;
// A layer variant has no proxy: the asset's shows the file's default layers.
let proxy = asset.proxy.as_ref().map(|p| &p.data).filter(|_| want.bytes.key() == asset.data.key());
let (pm, data) = match pixels(f, &want, proxy) {
Drawn::Pixels(pm, data) => (pm, data),
Drawn::Loading => {
ctx.set_transform(f.view * xf * g.xf);
ctx.set_paint(screen_color([222, 222, 222], 255));
ctx.fill_rect(&Rect::new(0.0, 0.0, g.size.0, g.size.1).intersect(page));
return;
}
Drawn::Loading => return loading(ctx),
Drawn::Nothing => return,
};
// A coloured image is already in the plate's inks (its colours come from `color_of`).
let pm = match (colorize::colorized(f.doc, &g.fill, data, &pm), PLATE.with(std::cell::Cell::get)) {
let coloured = match colorize::colorized(f.doc, &g.fill, data, &pm, !f.opts.defer_images) {
Prepared::Ready(coloured) => coloured,
Prepared::Pending | Prepared::Failed => return loading(ctx),
};
let pm = match (coloured, PLATE.with(std::cell::Cell::get)) {
(Some(coloured), _) => coloured,
(None, Some(plate)) => plate_pixmap(&pm, plate),
(None, None) => pm,
};
// Only where the page is: a box reaching past it (a media box larger than the crop box)
// shows nothing there.
let rect = Rect::new(0.0, 0.0, g.size.0, g.size.1).intersect(page);
ctx.set_transform(f.view * xf * g.xf);
let sx = page_w / pm.width().max(1) as f64;
let sy = page_h / pm.height().max(1) as f64;
ctx.set_paint(vello_cpu::Image {
image: vello_cpu::ImageSource::Pixmap(pm),
sampler: peniko::ImageSampler::default().with_quality(peniko::ImageQuality::Medium),
});
ctx.set_paint_transform(Affine::translate((page.x0, page.y0)) * Affine::scale_non_uniform(sx, sy));
ctx.fill_rect(&rect);
ctx.reset_paint_transform();
paint_pixels(ctx, f, g, xf, page, page_w, page_h, pm);
}
}
/// Draw `pm`, the pixels of a page `page_w` × `page_h` points at `page` in graphic `g`'s space.
#[allow(clippy::too_many_arguments)]
fn paint_pixels(
ctx: &mut RenderContext,
f: &Frame,
g: &designcraft_doc::Graphic,
xf: Affine,
page: Rect,
page_w: f64,
page_h: f64,
pm: std::sync::Arc<vello_cpu::Pixmap>,
) {
// Only where the page is: a box reaching past it (a media box larger than the crop box)
// shows nothing there.
let rect = Rect::new(0.0, 0.0, g.size.0, g.size.1).intersect(page);
ctx.set_transform(f.view * xf * g.xf);
let sx = page_w / pm.width().max(1) as f64;
let sy = page_h / pm.height().max(1) as f64;
ctx.set_paint(vello_cpu::Image {
image: vello_cpu::ImageSource::Pixmap(pm),
sampler: peniko::ImageSampler::default().with_quality(peniko::ImageQuality::Medium),
});
ctx.set_paint_transform(Affine::translate((page.x0, page.y0)) * Affine::scale_non_uniform(sx, sy));
ctx.fill_rect(&rect);
ctx.reset_paint_transform();
}