mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-15 23:02:32 +00:00
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:
1 parent
12e0e2341c
commit
f06f11b52f
8 files changed
+711
-124
No files matched your search
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ mod decode;
|
||||
mod draw;
|
||||
mod driver;
|
||||
mod pdf_raster;
|
||||
mod prepare;
|
||||
mod stroke;
|
||||
mod types;
|
||||
|
||||
|
||||
+215
-47
@@ -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"] {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user