mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 05:07:51 -06:00
Envelopes: Anti-Alias and Preserve Shape Using change how distorted images draw
Envelope Options stored Anti-Alias and Preserve Shape Using, but nothing that draws an image read them: a distorted image was always smoothed and always cut into clip groups. - With Anti-Alias off, each piece of a distorted image is marked pixelated (saved as `pixelated`). The renderer draws it with nearest-neighbor sampling and without mips, SVG export writes `image-rendering: pixelated`, and PDF export writes it without /Interpolate. - With Preserve Shape Using Transparency, each piece is the image with a white opacity mask in the piece's shape instead of a clip group. The renderer paints an image whose mask is one plain white path straight into that path, as it does for an image in a clip group, when the path lies inside the image's frame. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -913,12 +913,92 @@ fn images_take_their_nearest_pixel_without_smoothing() {
|
||||
let im = ImageObject { key: "bw".into(), width: 2, height: 1, xf: Affine::scale(20.0), link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(d.layers[0].id), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
// How many pixels across the image are neither black nor white.
|
||||
let greys = |smooth_images| {
|
||||
let r = Renderer::new().render(&d, 40, 20, Affine::IDENTITY, &RenderOptions { smooth_images, ..Default::default() });
|
||||
let greys = |d: &Document, smooth_images| {
|
||||
let r = Renderer::new().render(d, 40, 20, Affine::IDENTITY, &RenderOptions { smooth_images, ..Default::default() });
|
||||
(0..40).filter(|x| (16..240).contains(&r.pixel(*x, 10)[0])).count()
|
||||
};
|
||||
assert!(greys(true) >= 4, "smooth: a ramp from black to white");
|
||||
assert_eq!(greys(false), 0, "nearest: black, then white");
|
||||
assert!(greys(&d, true) >= 4, "smooth: a ramp from black to white");
|
||||
assert_eq!(greys(&d, false), 0, "nearest: black, then white");
|
||||
// A pixelated image (an envelope's with Anti-Alias off) takes its nearest pixel either way.
|
||||
d.node_mut(id).unwrap().extras_mut().pixelated = true;
|
||||
assert_eq!(greys(&d, true), 0, "pixelated: black, then white");
|
||||
}
|
||||
|
||||
/// An image masked by one white shape with Clip on draws as an image clipped by that shape, with
|
||||
/// nothing drawn offscreen; any other mask goes through the offscreen mask.
|
||||
#[test]
|
||||
fn an_image_masked_by_a_white_shape_draws_as_if_clipped() {
|
||||
use vectorcraft_doc::{ImageBlob, ImageObject, NodeKind, OpacityMask};
|
||||
let img = image::RgbaImage::from_pixel(2, 2, image::Rgba([255, 0, 0, 255]));
|
||||
let mut png = vec![];
|
||||
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
let doc_masked = |mask_color: Color, shape: Rect| {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
d.images.insert("red".into(), ImageBlob::new("image/png", png.clone()));
|
||||
let id = d.alloc_id();
|
||||
let im = ImageObject {
|
||||
key: "red".into(),
|
||||
width: 2,
|
||||
height: 2,
|
||||
xf: Affine::translate((10.0, 10.0)) * Affine::scale(40.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let shape = rect_node(&mut d, shape, Paint::solid(mask_color), Paint::None, 0.0);
|
||||
let n = Node::new(id, NodeKind::Image(Box::new(im))).with_mask(Some(Box::new(OpacityMask::new(shape, true))));
|
||||
d.insert(Some(d.layers[0].id), 0, n).unwrap();
|
||||
d
|
||||
};
|
||||
let doc = |mask_color: Color| doc_masked(mask_color, Rect::new(10.0, 10.0, 50.0, 90.0));
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let draw = |d: &Document| {
|
||||
let mut r = Renderer::new();
|
||||
r.threads = 0;
|
||||
OFFSCREEN_PIXELS.with(|n| n.set(0));
|
||||
let img = r.render(d, 100, 100, Affine::IDENTITY, &opts);
|
||||
(img, OFFSCREEN_PIXELS.with(|n| n.get()))
|
||||
};
|
||||
let (img, px) = draw(&doc(Color::WHITE));
|
||||
assert_eq!(px, 0, "no offscreen mask");
|
||||
assert_eq!(img.pixel(30, 50), [255, 0, 0, 255], "inside the shape");
|
||||
assert_eq!(img.pixel(70, 50), [255, 255, 255, 255], "outside it");
|
||||
let (img, px) = draw(&doc(Color::rgb(0.5, 0.5, 0.5)));
|
||||
assert!(px > 0, "a grey mask is drawn offscreen");
|
||||
assert!((100..160).contains(&img.pixel(30, 50)[1]), "at about half opacity: {:?}", img.pixel(30, 50));
|
||||
// A white mask reaching past the image's frame shows nothing outside the frame.
|
||||
let (img, _) = draw(&doc_masked(Color::WHITE, Rect::new(0.0, 10.0, 50.0, 90.0)));
|
||||
assert_eq!(img.pixel(5, 50), [255, 255, 255, 255], "left of the frame");
|
||||
assert_eq!(img.pixel(30, 50), [255, 0, 0, 255]);
|
||||
// In isolation mode, outside the isolated object, it is dimmed like the art around it.
|
||||
let mut d = doc(Color::WHITE);
|
||||
let other = rect_node(&mut d, Rect::new(60.0, 60.0, 90.0, 90.0), Paint::solid(Color::BLACK), Paint::None, 0.0);
|
||||
let other = d.insert(Some(d.layers[0].id), usize::MAX, other).unwrap();
|
||||
let isolated = RenderOptions { isolated: Some(other), ..opts.clone() };
|
||||
let img = Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &isolated);
|
||||
assert!(img.pixel(30, 50)[1] > 60, "dimmed: {:?}", img.pixel(30, 50));
|
||||
}
|
||||
|
||||
/// A pixelated image drawn reduced with `image_mips` on takes its nearest full pixel, not a
|
||||
/// mip's average.
|
||||
#[test]
|
||||
fn a_pixelated_image_draws_reduced_without_mips() {
|
||||
use vectorcraft_doc::{ImageBlob, ImageObject, NodeKind};
|
||||
let img = image::RgbaImage::from_fn(64, 64, |x, y| image::Rgba(if (x + y) % 2 == 0 { [255, 0, 0, 255] } else { [0, 0, 255, 255] }));
|
||||
let mut png = vec![];
|
||||
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
let mut d = Document::new(64.0, 64.0);
|
||||
d.images.insert("check".into(), ImageBlob::new("image/png", png));
|
||||
let id = d.alloc_id();
|
||||
let im =
|
||||
ImageObject { key: "check".into(), width: 64, height: 64, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
let mut n = Node::new(id, NodeKind::Image(Box::new(im)));
|
||||
n.extras_mut().pixelated = true;
|
||||
d.insert(Some(d.layers[0].id), 0, n).unwrap();
|
||||
let on = RenderOptions { image_mips: true, ..Default::default() };
|
||||
let r = Renderer::new().render(&d, 16, 16, Affine::scale(0.25), &on);
|
||||
let mixed = (0..16).flat_map(|y| (0..16).map(move |x| (x, y))).filter(|(x, y)| r.pixel(*x, *y)[0] > 40 && r.pixel(*x, *y)[2] > 40).count();
|
||||
assert_eq!(mixed, 0, "each pixel red or blue");
|
||||
}
|
||||
|
||||
/// A PNG that says it is `w` × `h` 1-bit gray pixels (its header and no rows).
|
||||
|
||||
Reference in New Issue
Block a user