mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 03:37:00 -06:00
Merge pull request #2283 from marcoarnulfo/fix/svg-mask-type-alpha
SVG import: an alpha mask counts by its art's alpha, not its luminance
This commit is contained in:
+33
-1
@@ -3,7 +3,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vectorcraft_color::BlendMode;
|
||||
use vectorcraft_color::{BlendMode, Color, Paint};
|
||||
use vectorcraft_geom::corners::cut_corners;
|
||||
use vectorcraft_geom::shapes::{self, CornerKind};
|
||||
use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, Point, Rect};
|
||||
@@ -876,6 +876,38 @@ impl OpacityMask {
|
||||
}
|
||||
}
|
||||
|
||||
/// Every paint of `n` (and its children) white, opacities kept: an alpha mask's art as an opacity
|
||||
/// mask's, whose luminance over the black outside the art is then the art's alpha.
|
||||
pub fn whiten(n: &mut Node) {
|
||||
let white = |p: &mut Paint| match p {
|
||||
Paint::Solid { color, swatch, tint } => {
|
||||
(*color, *swatch, *tint) = (Color::WHITE, None, 1.0);
|
||||
}
|
||||
Paint::Gradient(g) => {
|
||||
for s in &mut g.gradient.stops {
|
||||
(s.color, s.swatch, s.tint) = (Color::WHITE, None, 1.0);
|
||||
}
|
||||
}
|
||||
Paint::None | Paint::Pattern { .. } => {}
|
||||
};
|
||||
for item in &mut n.appearance.items {
|
||||
white(item.paint_mut());
|
||||
}
|
||||
match &mut n.kind {
|
||||
NodeKind::Mesh(m) => m.points.iter_mut().for_each(|p| p.color = Color::WHITE),
|
||||
NodeKind::Text(t) => t.runs.iter_mut().for_each(|r| {
|
||||
white(&mut r.style.fill);
|
||||
white(&mut r.style.stroke);
|
||||
}),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(ch) = n.children_mut() {
|
||||
for c in ch {
|
||||
whiten(Arc::make_mut(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub fn new(id: NodeId, kind: NodeKind) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::sync::Arc;
|
||||
use kurbo::{Rect, Shape};
|
||||
use vectorcraft_color::blend::MASK_LUM;
|
||||
use vectorcraft_color::{BlendMode, Color, Paint};
|
||||
use vectorcraft_doc::node::whiten;
|
||||
use vectorcraft_doc::{AppearanceItem, Node, NodeKind, OpacityMask};
|
||||
|
||||
/// What a soft mask does to what it masks.
|
||||
@@ -52,38 +53,6 @@ pub(crate) fn contains(outer: Rect, inner: Rect) -> bool {
|
||||
outer.x0 <= inner.x0 && outer.y0 <= inner.y0 && outer.x1 >= inner.x1 && outer.y1 >= inner.y1
|
||||
}
|
||||
|
||||
/// Every paint of `n` (and its children) white, opacities kept: an alpha mask's art as a
|
||||
/// luminosity mask's.
|
||||
fn whiten(n: &mut Node) {
|
||||
let white = |p: &mut Paint| match p {
|
||||
Paint::Solid { color, swatch, tint } => {
|
||||
(*color, *swatch, *tint) = (Color::WHITE, None, 1.0);
|
||||
}
|
||||
Paint::Gradient(g) => {
|
||||
for s in &mut g.gradient.stops {
|
||||
(s.color, s.swatch, s.tint) = (Color::WHITE, None, 1.0);
|
||||
}
|
||||
}
|
||||
Paint::None | Paint::Pattern { .. } => {}
|
||||
};
|
||||
for item in &mut n.appearance.items {
|
||||
white(item.paint_mut());
|
||||
}
|
||||
match &mut n.kind {
|
||||
NodeKind::Mesh(m) => m.points.iter_mut().for_each(|p| p.color = Color::WHITE),
|
||||
NodeKind::Text(t) => t.runs.iter_mut().for_each(|r| {
|
||||
white(&mut r.style.fill);
|
||||
white(&mut r.style.stroke);
|
||||
}),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(ch) = n.children_mut() {
|
||||
for c in ch {
|
||||
whiten(Arc::make_mut(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How a soft mask drawing `art` acts: `alpha` for an alpha mask, `backdrop` the luminance outside
|
||||
/// the art, `inverted` when its transfer function inverts it. `page`: the area masks cover.
|
||||
pub(crate) fn mask_spec(
|
||||
|
||||
@@ -1091,13 +1091,11 @@ impl Importer {
|
||||
Some(n)
|
||||
}
|
||||
|
||||
/// `<mask>` → opacity mask (luminance; alpha masks are approximated by their luminance). A
|
||||
/// mask our export wrote gets its options back ([`mask_flags`]) and its art without what
|
||||
/// stands for them: the inverting filter group and the backdrop rectangle.
|
||||
/// `<mask>` → opacity mask. An alpha mask's art comes in white with its opacity kept, so its
|
||||
/// luminance is its alpha (images in it keep their colours, with a warning). A mask our export wrote gets its
|
||||
/// options back ([`mask_flags`]) and its art without what stands for them: the inverting
|
||||
/// filter group and the backdrop rectangle.
|
||||
fn opacity_mask(&mut self, m: &usvg::Mask, ts: Affine, label: &str) -> Option<Box<vectorcraft_doc::OpacityMask>> {
|
||||
if m.kind() == usvg::MaskType::Alpha {
|
||||
self.warn(format!("alpha mask on {label} imported as a luminance opacity mask"));
|
||||
}
|
||||
if m.mask().is_some() {
|
||||
self.warn(format!("nested mask on {label} ignored"));
|
||||
}
|
||||
@@ -1115,6 +1113,14 @@ impl Importer {
|
||||
nodes = rest;
|
||||
}
|
||||
let mut children: Vec<Arc<Node>> = nodes.iter().filter_map(|c| self.node(c, ts)).map(Arc::new).collect();
|
||||
if m.kind() == usvg::MaskType::Alpha {
|
||||
children.iter_mut().for_each(|c| vectorcraft_doc::node::whiten(Arc::make_mut(c)));
|
||||
let mut image = false;
|
||||
children.iter().for_each(|c| c.walk(&mut |n| image |= matches!(n.kind, NodeKind::Image(_))));
|
||||
if image {
|
||||
self.warn(format!("image in the alpha mask on {label} counts by its luminance"));
|
||||
}
|
||||
}
|
||||
// Mask art that is all hidden draws nothing: that is an empty mask, not no mask (black all
|
||||
// over when clipped, white when not, inverted when the mask is).
|
||||
let art = match children.len() {
|
||||
|
||||
@@ -102,3 +102,28 @@ fn a_mask_whose_art_is_all_hidden_hides_its_object() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_alpha_mask_uses_its_art_alpha_not_its_luminance() {
|
||||
// `mask-type: alpha` (CSS Masking §7.2): an opaque dark red shows the square fully, black at
|
||||
// half opacity shows it half, whatever their luminance. As a property and as an attribute.
|
||||
for mask_type in [r#"style="mask-type:alpha""#, r#"mask-type="alpha""#] {
|
||||
let svg = format!(
|
||||
r##"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
|
||||
<mask id="m" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100" {mask_type}>
|
||||
<rect width="50" height="100" fill="#b43333"/><rect x="50" width="50" height="100" fill="black" fill-opacity="0.5"/></mask>
|
||||
<rect width="100" height="100" fill="#1a73e8" mask="url(#m)"/></svg>"##
|
||||
);
|
||||
let (d, warnings) = import_with_report(&svg).unwrap();
|
||||
let [m] = &masks(&d)[..] else { panic!("one mask: {svg}") };
|
||||
assert!(m.clip && !m.invert);
|
||||
let (canvas, reference) = (render_artboard(&d), resvg_render(&svg, 100, 100));
|
||||
for (x, y, want) in [(25, 50, [26, 115, 232]), (75, 50, [141, 185, 244])] {
|
||||
let (got, r) = (canvas.over_white(x, y), reference.over_white(x, y));
|
||||
let near = |a: [u8; 3]| a.iter().zip(want).all(|(a, b)| a.abs_diff(b) <= 3);
|
||||
assert!(near(r), "resvg ({x},{y}) {r:?}: {svg}");
|
||||
assert!(near(got), "canvas ({x},{y}) {got:?}, want {want:?}: {svg}");
|
||||
}
|
||||
assert!(warnings.is_empty(), "{warnings:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user