mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
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:
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user