mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Merge pull request #2621 from dotCooCoo/fix/pattern-cache-keeps-decode-warning
Render: every raster export warns about an undecodable image inside a pattern
This commit is contained in:
@@ -336,6 +336,35 @@ fn pdf_export_expands_pattern_fills() {
|
||||
assert!(rep.bytes.len() > 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_raster_export_says_when_a_pattern_holds_an_image_that_could_not_be_decoded() {
|
||||
// A PNG whose header reads (so it places at its size) but whose pixel data is cut short, made
|
||||
// into a pattern and then cleared from the page: only the pattern's tile draws it.
|
||||
let mut s = session();
|
||||
let mut png = Vec::new();
|
||||
image::RgbaImage::from_pixel(20, 20, image::Rgba([200, 30, 30, 255]))
|
||||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
let idat = png.windows(4).position(|w| w == b"IDAT").unwrap();
|
||||
png.truncate(idat + 8);
|
||||
let placed = s.execute("file.place", &json!({"name": "cut.png", "dataBase64": vectorcraft_format::base64_encode(&png)})).unwrap();
|
||||
let placed = NodeId(placed["ids"][0].as_u64().unwrap());
|
||||
sel(&mut s, &[placed]);
|
||||
s.execute("object.pattern.make", &json!({"name": "Cut", "width": 20, "height": 20})).unwrap();
|
||||
s.execute("object.pattern.done", &json!({})).unwrap();
|
||||
sel(&mut s, &[placed]);
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
assert!(doc(&s).node(placed).is_none());
|
||||
let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
|
||||
s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Cut"})).unwrap();
|
||||
// The first export draws the pattern's tile, and the later ones draw it from the tile kept.
|
||||
for n in 0..3 {
|
||||
let r = s.execute("document.export", &json!({"format": "png", "background": "transparent"})).unwrap();
|
||||
let warnings = r["warnings"].as_array().cloned().unwrap_or_default();
|
||||
assert!(warnings.iter().any(|w| w.as_str().is_some_and(|w| w.contains("couldn't be decoded"))), "export {n}: {r}");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- repeat ----------
|
||||
|
||||
#[test]
|
||||
|
||||
+31
-2
@@ -384,6 +384,10 @@ pub(crate) struct PixelEntry {
|
||||
/// rasters made before their objects moved: only a frame rendered while interacting draws
|
||||
/// it.
|
||||
moved: bool,
|
||||
/// How many images in the content failed to decode while the raster was made
|
||||
/// ([`crate::paint::decode_failures`]): each frame that draws the raster counts them
|
||||
/// again ([`crate::paint::add_decode_failures`]).
|
||||
failed: u64,
|
||||
pub(crate) stamp: u64,
|
||||
}
|
||||
|
||||
@@ -1243,6 +1247,7 @@ impl Renderer {
|
||||
self.stats.moved += usize::from(e.moved);
|
||||
let p = f.view * e.anchor;
|
||||
let (image, at, k) = (e.image.clone(), ((p.x + e.rel.0).round(), (p.y + e.rel.1).round()), e.k);
|
||||
crate::paint::add_decode_failures(e.failed);
|
||||
draw_scaled(ctx, image, at, k);
|
||||
return;
|
||||
}
|
||||
@@ -1252,8 +1257,9 @@ impl Renderer {
|
||||
let px = f.px / k;
|
||||
let frame = Frame { mt: false, view, visible: to_doc.transform_rect_bbox(Rect::new(0.0, 0.0, w, h)), px, ..*f };
|
||||
let mut off = f.offscreen_context(wu, hu);
|
||||
let before = self.stats.moved;
|
||||
let (before, decoded) = (self.stats.moved, crate::paint::decode_failures());
|
||||
self.inside_layer(|r| paint(r, &mut off, &frame));
|
||||
let failed = crate::paint::decode_failures() - decoded;
|
||||
// Content drawn over moved rasters makes a moved raster.
|
||||
let moved = self.stats.moved > before;
|
||||
off.flush();
|
||||
@@ -1293,7 +1299,7 @@ impl Renderer {
|
||||
let rel = (x0 - p.x, y0 - p.y);
|
||||
let owner = content.owner.cloned().or_else(|| self.fx_owner.clone().filter(|o| std::ptr::eq(&**o, content.node)));
|
||||
let node = Drawn::of(content.node, owner.as_ref());
|
||||
self.pixel_fx.insert(key, PixelEntry { node, linear, anchor, rel, k, image: image.clone(), made, inputs, moved, stamp });
|
||||
self.pixel_fx.insert(key, PixelEntry { node, linear, anchor, rel, k, image: image.clone(), made, inputs, moved, failed, stamp });
|
||||
self.trim_fx_rasters(FX_RASTER_BUDGET);
|
||||
}
|
||||
draw_scaled(ctx, image, (x0, y0), k);
|
||||
@@ -2010,6 +2016,29 @@ mod tests {
|
||||
assert_eq!(b.pixels, c.pixels);
|
||||
}
|
||||
|
||||
/// A blurred image that can't be decoded: every frame that draws it counts the failure
|
||||
/// ([`crate::paint::decode_failures`]), from a new raster or from the raster kept.
|
||||
#[test]
|
||||
fn cached_effect_rasters_count_the_images_they_could_not_decode() {
|
||||
use vectorcraft_doc::{ImageBlob, ImageObject, NodeKind};
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
// A PNG signature with nothing after it.
|
||||
d.images.insert("cut".into(), ImageBlob::new("image/png", b"\x89PNG\r\n\x1a\n".to_vec()));
|
||||
let im =
|
||||
ImageObject { key: "cut".into(), width: 40, height: 40, xf: Affine::translate((30.0, 30.0)), link: None, placement: Default::default() };
|
||||
let mut n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
n.appearance.effects = vec![Effect { id: "blur.gaussian".into(), params: json!({"radius": 4.0}), visible: true }];
|
||||
d.insert(Some(d.layers[0].id), 0, n).unwrap();
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let mut r = Renderer::new();
|
||||
for frame in 0..3 {
|
||||
let before = crate::paint::decode_failures();
|
||||
r.render(&d, 100, 100, Affine::IDENTITY, &opts);
|
||||
assert!(crate::paint::decode_failures() > before, "frame {frame}");
|
||||
}
|
||||
assert_eq!(r.pixel_fx.len(), 1, "the later frames draw from the raster kept");
|
||||
}
|
||||
|
||||
/// While interacting, an object that only moved draws its drop shadow and filtered content
|
||||
/// from the rasters of an earlier frame, at the nearest whole pixel, without running the
|
||||
/// effects again (#1257). The frame after the interaction runs them again: it is what a fresh
|
||||
|
||||
@@ -74,7 +74,7 @@ pub fn decode_failures() -> u64 {
|
||||
fn decode(bytes: &[u8]) -> Option<image::DynamicImage> {
|
||||
let img = decode_within_allowance(bytes).ok();
|
||||
if img.is_none() {
|
||||
DECODE_FAILURES.with(|n| n.set(n.get() + 1));
|
||||
add_decode_failures(1);
|
||||
}
|
||||
img
|
||||
}
|
||||
@@ -125,6 +125,13 @@ pub fn decode_pixmap_fit(bytes: &[u8], w: u16, h: u16) -> Option<vello_cpu::Pixm
|
||||
pixmap_of(img.to_rgba8())
|
||||
}
|
||||
|
||||
/// Count `n` images that couldn't be decoded on this thread ([`decode_failures`]). [`decode`]
|
||||
/// counts each failure, and a raster kept in a cache counts the failures it was drawn with again
|
||||
/// each time it is drawn.
|
||||
pub(crate) fn add_decode_failures(n: u64) {
|
||||
DECODE_FAILURES.with(|c| c.set(c.get() + n));
|
||||
}
|
||||
|
||||
/// An RGBA image as a premultiplied pixmap.
|
||||
fn pixmap_of(img: image::RgbaImage) -> Option<vello_cpu::Pixmap> {
|
||||
let (w, h) = img.dimensions();
|
||||
|
||||
@@ -89,6 +89,10 @@ struct TileEntry {
|
||||
sx: f64,
|
||||
sy: f64,
|
||||
pixmap: Arc<Pixmap>,
|
||||
/// How many images in the art failed to decode while the tile was drawn
|
||||
/// ([`crate::paint::decode_failures`]): each frame that draws from the tile counts them
|
||||
/// again ([`crate::paint::add_decode_failures`]).
|
||||
failed: u64,
|
||||
}
|
||||
|
||||
struct PatternCache {
|
||||
@@ -244,17 +248,22 @@ pub(crate) fn set_pattern_paint(ctx: &mut RenderContext, name: &str, xf: Affine,
|
||||
&& std::ptr::eq(e.cms.as_ptr(), Arc::as_ptr(&cms))
|
||||
&& e.deps.as_ref().is_none_or(|d| d.current(doc))
|
||||
})
|
||||
.map(|e| (e.pixmap.clone(), e.sx, e.sy))
|
||||
.map(|e| (e.pixmap.clone(), e.sx, e.sy, e.failed))
|
||||
});
|
||||
let (pm, sx, sy) = match hit {
|
||||
Some(h) => h,
|
||||
Some((pm, sx, sy, failed)) => {
|
||||
crate::paint::add_decode_failures(failed);
|
||||
(pm, sx, sy)
|
||||
}
|
||||
None => {
|
||||
let w = (pw * scale).ceil().clamp(1.0, 4096.0) as u16;
|
||||
let h = (ph * scale).ceil().clamp(1.0, 4096.0) as u16;
|
||||
let before = crate::paint::decode_failures();
|
||||
let Some(pm) = with_tile_renderer(|r| rasterize(r, doc, def, Rect::new(0.0, 0.0, pw, ph), w, h, ink)) else {
|
||||
ctx.set_paint(missing());
|
||||
return true;
|
||||
};
|
||||
let failed = crate::paint::decode_failures() - before;
|
||||
let pm = Arc::new(pm);
|
||||
let (sx, sy) = (w as f64 / pw, h as f64 / ph);
|
||||
let deps = crate::fx::Reads::of_all(def.art.iter().map(|a| &**a)).any().then(|| Deps::of(doc));
|
||||
@@ -266,7 +275,8 @@ pub(crate) fn set_pattern_paint(ctx: &mut RenderContext, name: &str, xf: Affine,
|
||||
let mut c = c.borrow_mut();
|
||||
let tiles = &mut c.tiles[ink as usize];
|
||||
let (t, bytes) = (tiles.tick(), size_of::<TileEntry>() + w as usize * h as usize * 4);
|
||||
tiles.insert(key, TileEntry { art: def.art.clone(), deps, cms: Arc::downgrade(&cms), params, sx, sy, pixmap: pm.clone() }, bytes, t);
|
||||
let entry = TileEntry { art: def.art.clone(), deps, cms: Arc::downgrade(&cms), params, sx, sy, pixmap: pm.clone(), failed };
|
||||
tiles.insert(key, entry, bytes, t);
|
||||
});
|
||||
(pm, sx, sy)
|
||||
}
|
||||
@@ -577,6 +587,27 @@ mod tests {
|
||||
assert!(kept().1 <= budget, "{:?}", kept());
|
||||
}
|
||||
|
||||
/// A pattern whose art is an image that can't be decoded: every frame that draws the pattern
|
||||
/// counts the failure ([`crate::paint::decode_failures`]), from a new tile or from the
|
||||
/// tile kept.
|
||||
#[test]
|
||||
fn kept_tiles_count_the_images_they_could_not_decode() {
|
||||
use vectorcraft_doc::{ImageBlob, ImageObject};
|
||||
let mut d = doc_with_pattern(TileType::Grid);
|
||||
// A PNG signature with nothing after it.
|
||||
d.images.insert("cut".into(), ImageBlob::new("image/png", b"\x89PNG\r\n\x1a\n".to_vec()));
|
||||
let im = ImageObject { key: "cut".into(), width: 10, height: 10, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
d.patterns[0].art = vec![Arc::new(Node::new(NodeId(901), NodeKind::Image(Box::new(im))))];
|
||||
let mut r = Renderer::new();
|
||||
let start = tiles_rasterized();
|
||||
for n in 0..3 {
|
||||
let before = crate::paint::decode_failures();
|
||||
frame(&mut r, &d, 1.0);
|
||||
assert!(crate::paint::decode_failures() > before, "frame {n}");
|
||||
}
|
||||
assert_eq!(tiles_rasterized() - start, 1, "the later frames draw from the tile kept");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_pattern_is_grey() {
|
||||
let mut d = doc_with_pattern(TileType::Grid);
|
||||
|
||||
Reference in New Issue
Block a user