Merge pull request #5535 from dotCooCoo/fix/placed-document-undecoded-marks

Render: placed-document bitmaps draw undecoded-image marks when the view does
This commit is contained in:
BFlat
2026-11-04 15:05:27 +07:00
committed by GitHub
3 changed files with 113 additions and 21 deletions
+61 -18
View File
@@ -6,7 +6,10 @@
//!
//! On screen ([`RenderOptions::progressive_placed`]): a placed document can't be edited, so what
//! it shows is cached as a bitmap per file and zoom step (a power of two, in device pixels per
//! point), shared by every object showing that file. A bitmap not made yet is made on a
//! point), shared by every object showing that file. Images inside it whose pixels can't be
//! decoded are drawn as crossed boxes in the bitmap when the frame marks them
//! ([`RenderOptions::mark_undecoded_images`]), and the bitmaps with and without marks are kept
//! apart. A bitmap not made yet is made on a
//! background worker while the frame shows another step's bitmap, or a light placeholder;
//! [`generation`] changes when one is ready, [`made_between`] says for which files, and [`busy`]
//! says whether any is coming, so the screen can draw again where those files show
@@ -45,8 +48,9 @@ const MAX_BYTES: usize = 4 * BUDGET;
/// The smallest zoom step (device pixels per point).
const MIN_STEP: f64 = 1.0 / 64.0;
/// A bitmap's identity: the file shown (its key) and the zoom step's bits.
type BitmapKey = (String, u64);
/// A bitmap's identity: the file shown (its key), the zoom step's bits, and whether images whose
/// pixels can't be decoded are marked ([`crate::RenderOptions::mark_undecoded_images`]).
type BitmapKey = (String, u64, bool);
struct Bitmap {
pm: Arc<Pixmap>,
@@ -130,10 +134,19 @@ pub(crate) fn scheduled(key: &str) -> usize {
lock(&SCHEDULED).get(key).copied().unwrap_or(0)
}
/// Mark the bitmap of file `key` at zoom `step` as not kept past [`MAX_BYTES`].
/// Mark the bitmaps of file `key` at zoom `step`, with and without marks on undecoded images, as
/// not kept past [`MAX_BYTES`].
#[cfg(test)]
pub(crate) fn mark_unkept(key: &str, step: f64) {
lock(&UNKEPT).insert((key.to_string(), step.to_bits()));
for mark in [false, true] {
lock(&UNKEPT).insert((key.to_string(), step.to_bits(), mark));
}
}
/// `object`'s file (`blob`) as a bitmap of `step` device pixels per point, made here and now.
#[cfg(test)]
pub(crate) fn bitmap(object: &PlacedDocument, blob: &ImageBlob, step: f64, mark_undecoded_images: bool) -> Option<Pixmap> {
make(&mut Renderer::new(), object, blob, step, mark_undecoded_images)
}
/// The zoom step for `scale` device pixels per point: the power of two at or above it.
@@ -150,11 +163,12 @@ impl Bitmaps {
Some(b.pm.clone())
}
/// The bitmap of another step of the same file, the nearest to `step`.
fn nearest(&self, key: &str, step: f64) -> Option<(Arc<Pixmap>, f64)> {
/// The bitmap of another step of the same file, with the same marks on undecoded images, the
/// nearest to `step`.
fn nearest(&self, key: &str, step: f64, mark: bool) -> Option<(Arc<Pixmap>, f64)> {
self.map
.iter()
.filter(|((k, _), _)| k == key)
.filter(|((k, _, m), _)| k == key && *m == mark)
.min_by(|a, b| (a.1.step / step).log2().abs().total_cmp(&(b.1.step / step).log2().abs()))
.map(|(_, b)| (b.pm.clone(), b.step))
}
@@ -245,14 +259,30 @@ fn depth() -> usize {
/// Render `art` of a placed document's file (`doc`, its resources) into `w`×`h` transparent
/// pixels through `view`, the placed documents inside it one level deeper.
pub fn render_inside(r: &mut Renderer, doc: &vectorcraft_doc::Document, art: &Arc<Node>, w: u16, h: u16, view: Affine) -> Option<crate::Rendered> {
render_inside_marked(r, doc, art, w, h, view, false)
}
/// [`render_inside`], drawing images whose pixels can't be decoded as crossed boxes when
/// `mark_undecoded_images` is set ([`crate::RenderOptions::mark_undecoded_images`]).
fn render_inside_marked(
r: &mut Renderer,
doc: &vectorcraft_doc::Document,
art: &Arc<Node>,
w: u16,
h: u16,
view: Affine,
mark_undecoded_images: bool,
) -> Option<crate::Rendered> {
let _inside = Inside::enter();
r.render_node(doc, art, w, h, view)
let opts = crate::RenderOptions { mark_undecoded_images, ..Default::default() };
r.render_node_with(doc, art, w, h, view, &opts)
}
/// `object`'s file (`blob`) as a bitmap of `step` device pixels per point of its box: from its
/// preview when that is all there is and is fine enough, else from the file (read again when
/// needed; the preview when it can't be).
fn make(r: &mut Renderer, object: &PlacedDocument, blob: &ImageBlob, step: f64) -> Option<Pixmap> {
/// needed; the preview when it can't be). Images whose pixels can't be decoded are drawn as
/// crossed boxes when `mark_undecoded_images` is set.
fn make(r: &mut Renderer, object: &PlacedDocument, blob: &ImageBlob, step: f64, mark_undecoded_images: bool) -> Option<Pixmap> {
let (w, h) = (pixels(object.width * step), pixels(object.height * step));
let preview = if blob.is_proxy() { Some(blob.bytes.clone()) } else { blob.proxy.clone() };
let full = match blob.is_proxy() {
@@ -265,7 +295,7 @@ fn make(r: &mut Renderer, object: &PlacedDocument, blob: &ImageBlob, step: f64)
let f = e.frame;
let (sx, sy) = (w as f64 / f.width(), h as f64 / f.height());
let view = Affine::scale_non_uniform(sx, sy) * Affine::translate((-f.x0, -f.y0));
let img = render_inside(r, &e.doc, &e.art, w, h, view)?;
let img = render_inside_marked(r, &e.doc, &e.art, w, h, view, mark_undecoded_images)?;
let data = img.pixels.as_chunks::<4>().0.iter().map(|&[r, g, b, a]| vello_cpu::color::PremulRgba8 { r, g, b, a }).collect();
Some(Pixmap::from_parts(data, w, h))
}
@@ -281,7 +311,7 @@ fn pixels(v: f64) -> u16 {
fn run(r: &mut Renderer, mut job: Job) {
let scope = job.scope.take();
let mut body = || {
match make(r, &job.object, &job.blob, job.step) {
match make(r, &job.object, &job.blob, job.step, job.key.2) {
Some(pm) => {
if !lock(&BITMAPS).insert(job.key.clone(), pm, job.step, job.keep, (BUDGET, MAX_BYTES)) {
lock(&UNKEPT).insert(job.key.clone());
@@ -413,7 +443,7 @@ impl Renderer {
return false;
}
let Some(blob) = f.doc.images.get(&p.key).cloned() else { return true };
let key = (p.key.clone(), step.to_bits());
let key = (p.key.clone(), step.to_bits(), f.opts.mark_undecoded_images);
// Not kept past [`MAX_BYTES`]: drawn exactly.
if lock(&UNKEPT).contains(&key) {
return false;
@@ -423,7 +453,7 @@ impl Renderer {
// The clock when this frame first drew a placed document's bitmap.
let keep = *self.placed_frame.get_or_insert(b.clock);
let found = b.get(&key);
let nearest = if found.is_none() { b.nearest(&p.key, step) } else { None };
let nearest = if found.is_none() { b.nearest(&p.key, step, f.opts.mark_undecoded_images) } else { None };
(found, nearest, keep)
};
let shown = match found {
@@ -434,7 +464,7 @@ impl Renderer {
schedule(Job { key, object: p.clone(), blob, step, keep, scope: SCOPE_CAPTURE.get().map(|capture| capture()) });
// Made here and now (no threads) or meanwhile another step's.
let mut b = lock(&BITMAPS);
b.get(&(p.key.clone(), step.to_bits())).or(nearest.map(|n| n.0))
b.get(&(p.key.clone(), step.to_bits(), f.opts.mark_undecoded_images)).or(nearest.map(|n| n.0))
}
};
let rect = Rect::new(0.0, 0.0, p.width, p.height);
@@ -480,7 +510,7 @@ mod tests {
let mut b = Bitmaps::default();
// 400 bytes each: a budget of 1,000 bytes and at most 2,000.
let limits = (1000, 2000);
let key = |name: &str| (name.to_string(), 1f64.to_bits());
let key = |name: &str| (name.to_string(), 1f64.to_bits(), false);
for name in ["a", "b", "c"] {
assert!(b.insert(key(name), Pixmap::new(10, 10), 1.0, 0, limits));
}
@@ -489,7 +519,7 @@ mod tests {
let frame = b.clock;
assert!(b.get(&key("a")).is_some());
assert!(b.insert(key("d"), Pixmap::new(10, 10), 1.0, frame, limits));
let mut kept: Vec<&str> = b.map.keys().map(|(name, _)| name.as_str()).collect();
let mut kept: Vec<&str> = b.map.keys().map(|(name, _, _)| name.as_str()).collect();
kept.sort_unstable();
assert_eq!((kept, b.bytes), (vec!["a", "d"], 800), "b and c, not drawn since, are dropped");
for name in ["e", "f", "g"] {
@@ -499,4 +529,17 @@ mod tests {
assert!(!b.insert(key("h"), Pixmap::new(10, 10), 1.0, frame, limits), "past the most: not kept");
assert!(!b.map.contains_key(&key("h")) && b.bytes == 2000);
}
/// While a bitmap is made, another step's bitmap stands in only when it has the same marks on
/// undecoded images.
#[test]
fn a_stand_in_bitmap_has_the_same_marks() {
let mut b = Bitmaps::default();
let limits = (BUDGET, MAX_BYTES);
assert!(b.insert(("a".into(), 1f64.to_bits(), false), Pixmap::new(10, 10), 1.0, 0, limits));
assert!(b.insert(("a".into(), 4f64.to_bits(), true), Pixmap::new(40, 40), 4.0, 0, limits));
assert_eq!(b.nearest("a", 2.0, false).map(|n| n.1), Some(1.0));
assert_eq!(b.nearest("a", 2.0, true).map(|n| n.1), Some(4.0));
assert!(b.nearest("b", 2.0, false).is_none());
}
}
+6 -2
View File
@@ -307,15 +307,19 @@ impl Renderer {
/// Render node `n` of `doc` (its resources) into `w`×`h` transparent pixels through `view`.
pub fn render_node(&mut self, doc: &Document, n: &Arc<Node>, w: u16, h: u16, view: Affine) -> Option<Rendered> {
self.render_node_with(doc, n, w, h, view, &RenderOptions::default())
}
/// [`Self::render_node`] with `opts` (e.g. [`RenderOptions::mark_undecoded_images`]).
pub fn render_node_with(&mut self, doc: &Document, n: &Arc<Node>, w: u16, h: u16, view: Affine, opts: &RenderOptions) -> Option<Rendered> {
self.check_plugins();
self.check_symbols(doc);
let inv = view.inverse();
let visible = inv.transform_rect_bbox(Rect::new(0.0, 0.0, w as f64, h as f64));
let px = 1.0 / view.determinant().abs().sqrt().max(1e-12);
let mut ctx = single_threaded_context(w, h);
let opts = RenderOptions::default();
let symbols = doc.symbol_boxes().quick();
let frame = Frame { mt: false, doc, symbols: &symbols, view, visible, px, opts: &opts, ink: Ink::Display, plates: None };
let frame = Frame { mt: false, doc, symbols: &symbols, view, visible, px, opts, ink: Ink::Display, plates: None };
self.begin_frame();
(self.knockout, self.nested, self.backdrop, self.picture) = (false, 0, None, None);
self.clip_paths.clear();
+46 -1
View File
@@ -8,7 +8,9 @@ use vectorcraft_geom::shapes;
use crate::*;
/// Every file reads as a 40×20 artboard filled blue; a file whose bytes start with `loop` also
/// places itself (the same key) in its middle, and one whose bytes start with `bad` can't be read.
/// places itself (the same key) in its middle, one whose bytes start with `cut` also shows an
/// image whose pixels can't be decoded over its left half, and one whose bytes start with `bad`
/// can't be read.
fn blue_page(p: &PlacedDocument, bytes: &[u8]) -> Option<(Document, Node, Rect)> {
if bytes.starts_with(b"bad") {
return None;
@@ -17,6 +19,19 @@ fn blue_page(p: &PlacedDocument, bytes: &[u8]) -> Option<(Document, Node, Rect)>
d.layers.clear();
let blue = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0);
let mut children = vec![Arc::new(Node::path(NodeId(1), shapes::rectangle(Rect::new(0.0, 0.0, 40.0, 20.0)), blue))];
if bytes.starts_with(b"cut") {
d.images.insert("cut-image".into(), ImageBlob::new("image/png", b"not a PNG: placed document".to_vec()));
let im = vectorcraft_doc::ImageObject {
key: "cut-image".into(),
width: 20,
height: 20,
xf: Affine::IDENTITY,
link: None,
placement: Default::default(),
pixels: None,
};
children.push(Arc::new(Node::new(NodeId(3), NodeKind::Image(Box::new(im)))));
}
if bytes.starts_with(b"loop") {
d.images.insert(p.key.clone(), ImageBlob::new("application/json", bytes.to_vec()));
let me = PlacedDocument { xf: Affine::translate((10.0, 5.0)) * Affine::scale(0.5), ..p.clone() };
@@ -254,6 +269,36 @@ fn a_bitmap_that_cant_be_made_is_not_asked_for_again() {
assert!(shown != [255, 255, 255] && shown != [0, 0, 255], "the placeholder: {shown:?}");
}
/// A bitmap made for a frame that marks images whose pixels can't be decoded shows such an image
/// in the file as a crossed box; one made for a frame that does not shows the art under it.
#[test]
fn a_bitmap_marks_undecoded_images_when_the_frame_does() {
let d = doc_with("cut-1", b"cut");
let Some(NodeKind::PlacedDocument(p)) = d.node(NodeId(50)).map(|n| &n.kind) else { panic!("a placed document") };
let Some(blob) = d.images.get("cut-1") else { panic!("the file's bytes") };
for mark in [false, true] {
// 40 × 20 pixels at one pixel a point; the image covers x 0 to 20.
let Some(pm) = placed_document::bitmap(p, blob, 1.0, mark) else { panic!("a bitmap") };
let pixel = |x: usize, y: usize| pm.data_as_u8_slice()[(y * 40 + x) * 4..][..4].to_vec();
assert_eq!(pixel(30, 10), [0, 0, 255, 255], "mark {mark}: the art beside the image");
assert_eq!(pixel(5, 10) != [0, 0, 255, 255], mark, "mark {mark}: the crossed box over the art: {:?}", pixel(5, 10));
}
}
/// Frames with and without marks on undecoded images ask for bitmaps of their own, so a bitmap
/// made for one is not shown in the other. The file can't be read, so no bitmap is made after
/// the test ends.
#[test]
fn bitmaps_with_and_without_undecoded_marks_are_asked_for_apart() {
let _serial = serial();
let d = doc_with("marks-1", b"bad");
for mark in [false, true] {
let opts = RenderOptions { progressive_placed: true, mark_undecoded_images: mark, ..Default::default() };
Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &opts);
}
assert_eq!(placed_document::scheduled("marks-1"), 2, "one for each");
}
/// Once bitmaps are ready, drawing again where [`placed_area`] says gives what drawing the whole
/// frame again gives. The area of a file covers every object showing it (a group's member and
/// an object with a drop shadow too) and no object showing another file; art of a brush or a