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:
dotCooCoo
2026-10-29 11:07:57 +07:00
committed by bflatastic
parent 4651e80973
commit ab69d8f6d9
15 changed files with 345 additions and 28 deletions
+53 -1
View File
@@ -90,12 +90,64 @@ pub(crate) fn plain_image(n: &Node, im: &vectorcraft_doc::ImageObject, region: &
if !(n.visible && n.has_default_transparency() && n.appearance.items.is_empty() && n.appearance.effects.is_empty()) {
return false;
}
inside_frame(im, region, 1.0)
}
/// Is `region` inside image `im`'s frame (within `slack` pixels)?
fn inside_frame(im: &vectorcraft_doc::ImageObject, region: &BezPath, slack: f64) -> bool {
let det = im.xf.determinant();
if !(det.abs() > 1e-12 && det.is_finite()) {
return false;
}
let b = im.xf.inverse().transform_rect_bbox(region.bounding_box());
b.x0 >= -1.0 && b.y0 >= -1.0 && b.x1 <= im.width as f64 + 1.0 && b.y1 <= im.height as f64 + 1.0
b.x0 >= -slack && b.y0 >= -slack && b.x1 <= im.width as f64 + slack && b.y1 <= im.height as f64 + slack
}
/// Can image node `n` (`im`), whose opacity mask is one plain white path with Clip on (see
/// [`white_shape_mask`]) shaped like `region`, be painted straight into the region: the mask shows
/// the image fully inside the shape and hides it outside, as a clipping path does. The region must
/// lie inside the image's frame, since outside the frame the mask shows nothing.
pub(crate) fn shape_masked_image(n: &Node, im: &vectorcraft_doc::ImageObject, region: &BezPath) -> bool {
n.visible
&& n.opacity >= 1.0
&& n.blend == vectorcraft_color::BlendMode::Normal
&& !n.isolate
&& n.knockout == vectorcraft_doc::Knockout::Neutral
&& !n.knockout_shape
&& n.appearance.items.is_empty()
&& n.appearance.effects.is_empty()
&& inside_frame(im, region, 1e-6)
}
/// Is opacity mask `m` one path filled with opaque white and nothing else, with Clip on and
/// Invert off (each piece of an envelope's distorted image with Preserve Shape Using
/// Transparency)?
pub(crate) fn white_shape_mask(m: &vectorcraft_doc::OpacityMask) -> bool {
use vectorcraft_color::{BlendMode, Color, Paint};
let a = &m.art;
let shows = |p: &Paint| !matches!(p, Paint::None);
let white = |p: &Paint| matches!(p, Paint::Solid { color, tint, .. } if *color == Color::WHITE && *tint >= 1.0);
let mut fills = 0;
for i in &a.appearance.items {
match i {
AppearanceItem::Fill(f) if f.visible && shows(&f.paint) => {
if !(white(&f.paint) && f.opacity >= 1.0 && f.blend == BlendMode::Normal && f.effects.is_empty()) {
return false;
}
fills += 1;
}
AppearanceItem::Stroke(s) if s.visible && shows(&s.paint) => return false,
_ => {}
}
}
m.clip
&& !m.invert
&& !m.disabled
&& fills == 1
&& a.visible
&& a.has_default_transparency()
&& a.appearance.effects.is_empty()
&& matches!(a.kind, NodeKind::Path { guide: false, .. })
}
/// Opacity-mask coverage of one premultiplied pixel: luminance, with the area outside the mask