Render: raster exports warn about an undecodable image drawn from a kept Rasterize image

When a drop shadow or an outer glow follows Effect › Rasterize, the
renderer makes the Rasterize image on the first draw and keeps it for
the shadow and the content, which draw it again. The kept image held no
count of the images that failed to decode while it was made. With a
blur or a filter after the shadow, the content's raster was made from
the kept image and recorded no failure, so a renderer that drew the
object again took the shadow and the content from its caches and
counted nothing. Pattern tiles are drawn by a renderer that keeps these
rasters across exports: a tile drawn again with the same art, for
example after Pattern Options changes the overlap, gave an export
without the "couldn't be decoded" warning although the picture lacked
the image.

The kept Rasterize image now stores how many decodes failed while it
was made, and each draw from it adds that number to the count again, as
kept effect rasters and pattern tiles do.
This commit is contained in:
dotCooCoo
2026-10-18 12:34:28 +07:00
committed by bflatastic
parent df7daff3e4
commit 2fc47948d5
2 changed files with 74 additions and 4 deletions
+32
View File
@@ -6,6 +6,7 @@ use vectorcraft_doc::pattern::{PATTERN_EDIT_LAYER, RepeatKind, TileType};
use vectorcraft_doc::{Node, NodeKind};
use vectorcraft_geom::{Point, Rect};
use super::tests_menucmds::place_undecodable_png;
use super::*;
fn session() -> Session {
@@ -400,6 +401,37 @@ fn every_raster_export_says_when_a_pattern_holds_an_image_that_could_not_be_deco
}
}
#[test]
fn a_raster_export_says_when_a_pattern_tile_draws_an_undecodable_image_from_its_kept_rasterize_image() {
// A PNG whose pixel data is cut short, with Rasterize, a drop shadow and a blur: the shadow
// draws the Rasterize image, and the blurred content draws the image kept from that draw.
let mut s = session();
let placed = place_undecodable_png(&mut s);
for (effect, params) in [
("rasterize", json!({"background": "white"})),
("stylize.dropShadow", json!({"x": 2, "y": 2, "blur": 1})),
("blur.gaussian", json!({"radius": 4})),
] {
s.execute("effect.apply", &json!({"effect": effect, "params": params, "ids": [placed.0]})).unwrap();
}
sel(&mut s, &[placed]);
s.execute("object.pattern.make", &json!({"name": "Cut", "width": 80, "height": 40})).unwrap();
s.execute("object.pattern.done", &json!({})).unwrap();
sel(&mut s, &[placed]);
s.execute("edit.clear", &json!({})).unwrap();
let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Cut"})).unwrap();
let warns = |s: &mut Session| {
let r = s.execute("document.export", &json!({"format": "png", "background": "transparent"})).unwrap();
r["warnings"].as_array().is_some_and(|w| w.iter().any(|w| w.as_str().is_some_and(|w| w.contains("couldn't be decoded"))))
};
assert!(warns(&mut s), "the first export");
// A new overlap draws a new tile of the same art, and the tile renderer draws its shadow and
// its blurred content from the rasters it kept.
s.execute("pattern.options", &json!({"name": "Cut", "overlap": {"h": "right"}})).unwrap();
assert!(warns(&mut s), "the export after Pattern Options");
}
// ---------- repeat ----------
#[test]
+42 -4
View File
@@ -144,8 +144,10 @@ struct Kept {
#[derive(Clone)]
enum KeptImage {
/// The image on its own grid, as [`Renderer::rasterize_image`] makes it.
Grid(std::rc::Rc<(Vec<u8>, usize, usize)>),
/// The image on its own grid, as [`Renderer::rasterize_image`] makes it, and how many images
/// in the content failed to decode while it was made ([`crate::paint::decode_failures`]):
/// each draw of the image from here counts them again ([`crate::paint::add_decode_failures`]).
Grid(std::rc::Rc<(Vec<u8>, usize, usize)>, u64),
/// An image over [`Renderer::rasterize_image`]'s limits: the content's raster with the
/// Rasterize applied, as the first draw made it, and the transform from its pixels to
/// document space.
@@ -1757,13 +1759,17 @@ impl Renderer {
Some(RasterFx::Rasterize(r)) => {
let rect = r.image(content.bounds.unwrap_or(content.reach), line);
let kept = content.kept.and_then(|k| match &*k.image.borrow() {
Some(KeptImage::Grid(image)) => Some(image.clone()),
Some(KeptImage::Grid(image, failed)) => {
crate::paint::add_decode_failures(*failed);
Some(image.clone())
}
_ => None,
});
let image = kept.or_else(|| {
let failures = crate::paint::decode_failures();
let image = self.rasterize_image(f, r, rect, paint).map(std::rc::Rc::new);
if let (Some(k), Some(image)) = (content.kept, &image) {
*k.image.borrow_mut() = Some(KeptImage::Grid(image.clone()));
*k.image.borrow_mut() = Some(KeptImage::Grid(image.clone(), crate::paint::decode_failures() - failures));
}
image
});
@@ -2757,6 +2763,38 @@ mod tests {
assert_eq!(r.pixel_fx.len(), 1, "the later frames draw from the raster kept");
}
/// An image that can't be decoded, with Rasterize, a drop shadow and a blur: the shadow draws
/// the Rasterize image first, and the blurred content draws the image kept from that draw
/// ([`super::Kept`]). Every frame counts the failure, also when the shadow and the blurred
/// content are drawn from the rasters kept.
#[test]
fn rasters_drawn_from_a_kept_rasterize_image_count_the_images_it_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 = [
("rasterize", json!({"background": "white"})),
("stylize.dropShadow", json!({"mode": "normal", "opacity": 100, "x": 2, "y": 2, "blur": 1, "color": "#0000ff"})),
("blur.gaussian", json!({"radius": 4.0})),
]
.into_iter()
.map(|(id, params)| Effect { id: id.into(), params, visible: true })
.collect();
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.shadows.len(), r.pixel_fx.len()), (1, 1), "the later frames draw from the rasters 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