464 lines
22 KiB
Rust
464 lines
22 KiB
Rust
//! Object → Flatten Transparency (M3.91): atomic regions with the composited colours, rasterized
|
|
//! complex regions, the options and presets, undo and the look of the result.
|
|
|
|
use serde_json::{Value, json};
|
|
use vectorcraft_color::{BlendMode, Color, Paint};
|
|
use vectorcraft_doc::{AppearanceItem, Node, NodeKind};
|
|
use vectorcraft_geom::{BezPath, Point, Rect, Shape};
|
|
use vectorcraft_render::{Rendered, Renderer};
|
|
|
|
use super::*;
|
|
|
|
/// Colours here go through the process-wide colour settings, which the colour management tests
|
|
/// change: one at a time with them.
|
|
fn settings_lock() -> std::sync::MutexGuard<'static, ()> {
|
|
crate::tests_colormgmt::GLOBAL.lock().unwrap_or_else(|e| e.into_inner())
|
|
}
|
|
|
|
fn session() -> Session {
|
|
let mut s = Session::new();
|
|
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
|
s
|
|
}
|
|
|
|
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
|
s.execute(id, &p).unwrap()
|
|
}
|
|
|
|
/// An unstroked rectangle filled with `color`.
|
|
fn rect(s: &mut Session, x: f64, y: f64, w: f64, h: f64, color: &str) -> u64 {
|
|
let id = run(s, "shape.rectangle", json!({"x": x, "y": y, "width": w, "height": h}))["id"].as_u64().unwrap();
|
|
run(s, "paint.setFill", json!({"color": color, "ids": [id]}));
|
|
run(s, "paint.setStroke", json!({"none": true, "ids": [id]}));
|
|
id
|
|
}
|
|
|
|
fn node(s: &Session, id: u64) -> Node {
|
|
s.doc().unwrap().doc.node(NodeId(id)).unwrap().clone()
|
|
}
|
|
|
|
fn exists(s: &Session, id: u64) -> bool {
|
|
s.doc().unwrap().doc.node(NodeId(id)).is_some()
|
|
}
|
|
|
|
/// The document over white at 1 px per point.
|
|
fn picture(s: &Session) -> Rendered {
|
|
Renderer::new().render_region(&s.doc().unwrap().doc, Rect::new(0.0, 0.0, 200.0, 200.0), 1.0, true)
|
|
}
|
|
|
|
fn flatten(s: &mut Session, p: Value) -> (Vec<u64>, u64, u64) {
|
|
let r = run(s, "object.flattenTransparency", p);
|
|
let ids = r["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
(ids, r["vector"].as_u64().unwrap(), r["rasterized"].as_u64().unwrap())
|
|
}
|
|
|
|
fn outline(n: &Node) -> BezPath {
|
|
match &n.kind {
|
|
NodeKind::Path { path, .. } => path.to_bezpath(),
|
|
NodeKind::Compound { children, .. } => children.iter().fold(BezPath::new(), |mut bp, c| {
|
|
bp.extend(outline(c));
|
|
bp
|
|
}),
|
|
_ => panic!("a region is a path: {}", n.kind_label()),
|
|
}
|
|
}
|
|
|
|
/// The flat-colour regions in a flattened group.
|
|
fn regions(s: &Session, group: u64) -> Vec<Node> {
|
|
let g = node(s, group);
|
|
g.children().unwrap().iter().filter(|c| matches!(c.kind, NodeKind::Path { .. } | NodeKind::Compound { .. })).map(|c| (**c).clone()).collect()
|
|
}
|
|
|
|
/// A pixel centre well inside `n` (2 pt from its edges).
|
|
fn interior(n: &Node) -> Option<Point> {
|
|
let bp = outline(n);
|
|
let b = bp.bounding_box();
|
|
let inside = |p: Point| [(0.0, 0.0), (2.0, 0.0), (-2.0, 0.0), (0.0, 2.0), (0.0, -2.0)].iter().all(|(dx, dy)| bp.winding(p + (*dx, *dy)) != 0);
|
|
(b.y0.floor() as i32..b.y1.ceil() as i32)
|
|
.flat_map(|y| (b.x0.floor() as i32..b.x1.ceil() as i32).map(move |x| Point::new(x as f64 + 0.5, y as f64 + 0.5)))
|
|
.find(|p| inside(*p))
|
|
}
|
|
|
|
/// A region's colour over white, as 8-bit RGB.
|
|
fn shown(n: &Node) -> [u8; 3] {
|
|
let AppearanceItem::Fill(f) = &n.appearance.items[0] else { panic!("a region has one fill") };
|
|
let [r, g, b] = f.paint.color().unwrap().to_rgb();
|
|
let a = n.opacity;
|
|
let q = |v: f32| ((v * a + 1.0 - a) * 255.0).round() as u8;
|
|
[q(r), q(g), q(b)]
|
|
}
|
|
|
|
fn pixel(img: &Rendered, p: Point) -> [u8; 3] {
|
|
let [r, g, b, _] = img.pixel(p.x as u32, p.y as u32);
|
|
[r, g, b]
|
|
}
|
|
|
|
/// Every region of `group` shows the colour `before` had inside it, within `tol`/255: 1 where the
|
|
/// renderer composites once, 2 where it rounds a group's 8-bit pixels again (the regions' own
|
|
/// colours are exact).
|
|
fn assert_regions_match(s: &Session, before: &Rendered, group: u64, tol: u8, what: &str) {
|
|
let regions = regions(s, group);
|
|
assert!(!regions.is_empty(), "{what}: no regions");
|
|
for r in ®ions {
|
|
let Some(p) = interior(r) else { continue };
|
|
let (want, got) = (pixel(before, p), shown(r));
|
|
assert!(want.iter().zip(got).all(|(a, b)| a.abs_diff(b) <= tol), "{what}: at {p:?} the art showed {want:?}, the region has {got:?}");
|
|
}
|
|
}
|
|
|
|
/// Share of pixels whose colour moved by more than 8/255.
|
|
fn changed(a: &Rendered, b: &Rendered) -> f64 {
|
|
let n = a
|
|
.pixels
|
|
.as_chunks::<4>()
|
|
.0
|
|
.iter()
|
|
.zip(b.pixels.as_chunks::<4>().0)
|
|
.filter(|(p, q)| p.iter().zip(q.iter()).any(|(x, y)| x.abs_diff(*y) > 8))
|
|
.count();
|
|
n as f64 / (a.pixels.len() / 4) as f64
|
|
}
|
|
|
|
#[test]
|
|
fn two_half_transparent_rects_give_three_regions_with_their_pixels() {
|
|
let _settings = settings_lock();
|
|
let mut s = session();
|
|
let a = rect(&mut s, 10.0, 10.0, 100.0, 100.0, "#ff0000");
|
|
let b = rect(&mut s, 60.0, 60.0, 100.0, 100.0, "#0000ff");
|
|
run(&mut s, "transparency.set", json!({"ids": [a, b], "opacity": 50}));
|
|
let before = picture(&s);
|
|
let (ids, vector, rasterized) = flatten(&mut s, json!({"ids": [a, b]}));
|
|
assert_eq!((ids.len(), vector, rasterized), (1, 3, 0));
|
|
assert!(!exists(&s, a) && !exists(&s, b), "the objects are replaced");
|
|
let rs = regions(&s, ids[0]);
|
|
assert_eq!(rs.len(), 3);
|
|
assert!(rs.iter().all(|r| r.opacity == 1.0 && r.has_default_transparency()), "regions are opaque");
|
|
assert_regions_match(&s, &before, ids[0], 1, "50% rects");
|
|
// Red alone over white is pink; blue over red over white is purple.
|
|
let at = |p: Point| rs.iter().find(|r| outline(r).winding(p) != 0).map(shown).unwrap();
|
|
assert_eq!(at(Point::new(30.0, 30.0)), [255, 128, 128]);
|
|
assert!(at(Point::new(85.0, 85.0)).iter().zip([128, 64, 191]).all(|(x, y)| x.abs_diff(y) <= 1), "{:?}", at(Point::new(85.0, 85.0)));
|
|
assert_eq!(s.doc().unwrap().selection.objects, vec![NodeId(ids[0])]);
|
|
}
|
|
|
|
#[test]
|
|
fn every_blend_mode_flattens_to_its_pixels() {
|
|
let _settings = settings_lock();
|
|
for mode in BlendMode::ALL {
|
|
// Normal at full opacity is no transparency: nothing to flatten.
|
|
for opacity in if mode == BlendMode::Normal { &[70][..] } else { &[100, 70][..] } {
|
|
let mut s = session();
|
|
let a = rect(&mut s, 10.0, 10.0, 100.0, 100.0, "#cc6633");
|
|
let b = rect(&mut s, 60.0, 60.0, 100.0, 100.0, "#4d99e6");
|
|
run(&mut s, "transparency.set", json!({"ids": [b], "blend": mode.label(), "opacity": opacity}));
|
|
let before = picture(&s);
|
|
let (ids, vector, _) = flatten(&mut s, json!({"ids": [a, b]}));
|
|
assert_eq!(vector, 3, "{mode:?}");
|
|
// The renderer rounds the blended source again before mixing it in by its opacity.
|
|
assert_regions_match(&s, &before, ids[0], 2, &format!("{mode:?} at {opacity}%"));
|
|
// The overlap is the reference formula's colour.
|
|
let [r, g, b] = [0xcc, 0x66, 0x33].map(|v| v as f32 / 255.0);
|
|
let src = [0x4d, 0x99, 0xe6].map(|v| v as f32 / 255.0);
|
|
let want = vectorcraft_color::blend::composite(mode, [r, g, b, 1.0], [src[0], src[1], src[2], *opacity as f32 / 100.0]);
|
|
let overlap = regions(&s, ids[0]).into_iter().find(|r| outline(r).winding(Point::new(85.0, 85.0)) != 0).unwrap();
|
|
let got = shown(&overlap);
|
|
assert!((0..3).all(|i| (want[i] * 255.0 - got[i] as f32).abs() <= 0.5 + 1e-3), "{mode:?} at {opacity}%: {got:?} vs {want:?}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn groups_flatten_as_they_composite() {
|
|
let _settings = settings_lock();
|
|
type Setup = fn(&mut Session, u64, u64, u64);
|
|
let cases: [(&str, Setup); 8] = [
|
|
("group with its own fill", |s, _, b, c| {
|
|
run(s, "select.set", json!({"ids": [b, c]}));
|
|
let g = run(s, "object.group", json!({}))["id"].clone();
|
|
run(s, "appearance.addFill", json!({"ids": [g]}));
|
|
run(s, "appearance.setItem", json!({"ids": [g], "index": 0, "color": "#ffff00", "opacity": 50, "blend": "Multiply"}));
|
|
}),
|
|
("knockout shape", |s, _, b, c| {
|
|
run(s, "transparency.set", json!({"ids": [b], "opacity": 50}));
|
|
run(s, "transparency.set", json!({"ids": [c], "opacity": 40, "knockoutShape": true}));
|
|
run(s, "select.set", json!({"ids": [b, c]}));
|
|
run(s, "object.group", json!({}));
|
|
run(s, "transparency.set", json!({"knockout": "on"}));
|
|
}),
|
|
("knockout group", |s, _, b, c| {
|
|
run(s, "transparency.set", json!({"ids": [b, c], "opacity": 50}));
|
|
run(s, "select.set", json!({"ids": [b, c]}));
|
|
run(s, "object.group", json!({}));
|
|
run(s, "transparency.set", json!({"knockout": "on"}));
|
|
}),
|
|
("non-isolated group with opacity", |s, _, b, c| {
|
|
run(s, "transparency.set", json!({"ids": [c], "blend": "Multiply"}));
|
|
run(s, "select.set", json!({"ids": [b, c]}));
|
|
run(s, "object.group", json!({}));
|
|
run(s, "transparency.set", json!({"opacity": 60}));
|
|
}),
|
|
("isolated group", |s, _, b, c| {
|
|
run(s, "transparency.set", json!({"ids": [c], "blend": "Difference"}));
|
|
run(s, "select.set", json!({"ids": [b, c]}));
|
|
run(s, "object.group", json!({}));
|
|
run(s, "transparency.set", json!({"opacity": 80, "isolate": true}));
|
|
}),
|
|
("blending group with blending content", |s, _, b, c| {
|
|
run(s, "transparency.set", json!({"ids": [c], "blend": "Multiply"}));
|
|
run(s, "select.set", json!({"ids": [b, c]}));
|
|
run(s, "object.group", json!({}));
|
|
run(s, "transparency.set", json!({"blend": "Screen", "opacity": 90}));
|
|
}),
|
|
("non-isolated knockout group", |s, _, b, c| {
|
|
run(s, "transparency.set", json!({"ids": [b], "opacity": 50}));
|
|
run(s, "transparency.set", json!({"ids": [c], "blend": "Multiply", "opacity": 80}));
|
|
run(s, "select.set", json!({"ids": [b, c]}));
|
|
run(s, "object.group", json!({}));
|
|
run(s, "transparency.set", json!({"knockout": "on"}));
|
|
}),
|
|
("clip group", |s, _, b, c| {
|
|
run(s, "transparency.set", json!({"ids": [b], "opacity": 50}));
|
|
let e = run(s, "shape.ellipse", json!({"x": 40, "y": 40, "width": 120, "height": 120}))["id"].clone();
|
|
run(s, "select.set", json!({"ids": [b, c, e]}));
|
|
run(s, "object.clippingMask.make", json!({}));
|
|
}),
|
|
];
|
|
for (what, setup) in cases {
|
|
let mut s = session();
|
|
let a = rect(&mut s, 0.0, 80.0, 200.0, 40.0, "#33aa55");
|
|
let b = rect(&mut s, 10.0, 10.0, 100.0, 100.0, "#e04020");
|
|
let c = rect(&mut s, 60.0, 60.0, 100.0, 100.0, "#2060e0");
|
|
setup(&mut s, a, b, c);
|
|
let before = picture(&s);
|
|
run(&mut s, "select.all", json!({}));
|
|
let (ids, vector, rasterized) = flatten(&mut s, json!({}));
|
|
assert!(ids.len() == 1 && vector >= 3 && rasterized == 0, "{what}: {ids:?} {vector} {rasterized}");
|
|
assert_regions_match(&s, &before, ids[0], 2, what);
|
|
assert!(changed(&before, &picture(&s)) < 0.02, "{what}: the look changed");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn balance_trades_vector_regions_for_an_image() {
|
|
let _settings = settings_lock();
|
|
let setup = || {
|
|
let mut s = session();
|
|
let a = rect(&mut s, 10.0, 10.0, 100.0, 100.0, "#ff8000");
|
|
let b = rect(&mut s, 60.0, 60.0, 100.0, 100.0, "#0080ff");
|
|
run(&mut s, "transparency.set", json!({"ids": [b], "opacity": 50, "blend": "Multiply"}));
|
|
(s, a, b)
|
|
};
|
|
let (mut s, a, b) = setup();
|
|
assert_eq!(flatten(&mut s, json!({"ids": [a, b], "balance": 100})).1, 3);
|
|
let (mut s, a, b) = setup();
|
|
let before = picture(&s);
|
|
let (ids, vector, rasterized) = flatten(&mut s, json!({"ids": [a, b], "balance": 0, "clipComplexRegions": true, "lineArtPpi": 144}));
|
|
assert_eq!((vector, rasterized), (0, 1));
|
|
let g = node(&s, ids[0]);
|
|
let clip = &g.children().unwrap()[0];
|
|
assert!(clip.clips(), "clipped to the regions");
|
|
let NodeKind::Image(im) = &clip.children().unwrap()[1].kind else { panic!("an image") };
|
|
assert!(im.width >= 300, "at the line art resolution: {}", im.width);
|
|
assert!(changed(&before, &picture(&s)) < 0.02);
|
|
// Unclipped, the image is a plain rectangle.
|
|
let (mut s, a, b) = setup();
|
|
let (ids, ..) = flatten(&mut s, json!({"ids": [a, b], "balance": 0, "clipComplexRegions": false}));
|
|
assert!(matches!(node(&s, ids[0]).children().unwrap()[0].kind, NodeKind::Image(_)));
|
|
assert!(changed(&before, &picture(&s)) < 0.02);
|
|
// In between, a group splitting into more regions than the balance allows is rasterized.
|
|
let (mut s, a, b) = setup();
|
|
assert_eq!(flatten(&mut s, json!({"ids": [a, b], "balance": 5})).2, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn gradients_rasterize_and_flat_areas_stay_vector() {
|
|
let _settings = settings_lock();
|
|
for clip in [true, false] {
|
|
let mut s = session();
|
|
let g = rect(&mut s, 10.0, 10.0, 100.0, 100.0, "#000000");
|
|
run(
|
|
&mut s,
|
|
"paint.setFill",
|
|
json!({"ids": [g], "gradient": {"kind": "linear", "stops": [{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}]}}),
|
|
);
|
|
let t = rect(&mut s, 80.0, 80.0, 100.0, 100.0, "#20c040");
|
|
run(&mut s, "transparency.set", json!({"ids": [t], "opacity": 50}));
|
|
let before = picture(&s);
|
|
let (ids, vector, rasterized) = flatten(&mut s, json!({"ids": [g, t], "preset": "high", "clipComplexRegions": clip}));
|
|
assert_eq!(rasterized, 1, "clip {clip}");
|
|
assert!(vector >= 1, "the green alone stays vector (clip {clip})");
|
|
assert_regions_match(&s, &before, ids[0], 1, "gradient");
|
|
assert!(changed(&before, &picture(&s)) < 0.02, "clip {clip}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn flatten_is_one_undo_step() {
|
|
let _settings = settings_lock();
|
|
let mut s = session();
|
|
let a = rect(&mut s, 10.0, 10.0, 100.0, 100.0, "#ff0000");
|
|
let b = rect(&mut s, 60.0, 60.0, 100.0, 100.0, "#0000ff");
|
|
run(&mut s, "transparency.set", json!({"ids": [b], "opacity": 50}));
|
|
let steps = s.doc().unwrap().history.undo.len();
|
|
let doc = s.doc().unwrap().doc.clone();
|
|
flatten(&mut s, json!({"ids": [a, b]}));
|
|
assert_eq!(s.doc().unwrap().history.undo.len(), steps + 1);
|
|
assert_eq!(s.doc().unwrap().history.undo.last().unwrap().label, "Flatten Transparency");
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert_eq!(s.doc().unwrap().doc.layers, doc.layers);
|
|
run(&mut s, "edit.redo", json!({}));
|
|
assert!(!exists(&s, a));
|
|
}
|
|
|
|
#[test]
|
|
fn objects_without_transparency_stay_unless_outlined() {
|
|
let _settings = settings_lock();
|
|
let mut s = session();
|
|
let a = rect(&mut s, 10.0, 10.0, 50.0, 50.0, "#ff0000");
|
|
run(&mut s, "transparency.set", json!({"ids": [a], "opacity": 50}));
|
|
let far = rect(&mut s, 120.0, 120.0, 50.0, 50.0, "#00ff00");
|
|
run(&mut s, "paint.setStroke", json!({"color": "#000000", "ids": [far]}));
|
|
let text = run(&mut s, "text.create", json!({"x": 10, "y": 150, "text": "Hi", "size": 24}))["id"].as_u64().unwrap();
|
|
// Nothing transparent: nothing happens.
|
|
let steps = s.doc().unwrap().history.undo.len();
|
|
let (ids, vector, rasterized) = flatten(&mut s, json!({"ids": [far, text]}));
|
|
assert_eq!((ids, vector, rasterized, s.doc().unwrap().history.undo.len()), (vec![far, text], 0, 0, steps));
|
|
// The transparent rectangle is flattened; the others keep their ids.
|
|
let (ids, vector, _) = flatten(&mut s, json!({"ids": [a, far, text]}));
|
|
assert_eq!(vector, 1);
|
|
assert!(exists(&s, far) && exists(&s, text) && !exists(&s, a) && ids.contains(&far) && ids.contains(&text));
|
|
// On request, type and strokes are outlined too.
|
|
let (ids, ..) = flatten(&mut s, json!({"ids": [far, text], "textToOutlines": true, "strokesToOutlines": true}));
|
|
assert!(!exists(&s, far) && !exists(&s, text) && ids.len() == 2);
|
|
for id in ids {
|
|
node(&s, id).walk(&mut |c| {
|
|
assert!(!matches!(c.kind, NodeKind::Text(_)), "type is outlined");
|
|
assert!(c.appearance.stroke().is_none_or(|st| st.paint.is_none()), "strokes are outlined");
|
|
});
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn preserve_alpha_composites_over_nothing() {
|
|
let _settings = settings_lock();
|
|
let mut s = session();
|
|
let a = rect(&mut s, 10.0, 10.0, 100.0, 100.0, "#ff0000");
|
|
run(&mut s, "transparency.set", json!({"ids": [a], "opacity": 50}));
|
|
let before = picture(&s);
|
|
let (ids, ..) = flatten(&mut s, json!({"ids": [a], "preserveAlpha": true}));
|
|
let r = ®ions(&s, ids[0])[0];
|
|
assert_eq!((r.opacity, r.appearance.fill_paint()), (0.5, Paint::solid(Color::rgb(1.0, 0.0, 0.0))));
|
|
assert_regions_match(&s, &before, ids[0], 1, "preserve alpha");
|
|
// Over white by default: opaque pink.
|
|
run(&mut s, "edit.undo", json!({}));
|
|
let (ids, ..) = flatten(&mut s, json!({"ids": [a]}));
|
|
let r = ®ions(&s, ids[0])[0];
|
|
assert_eq!((r.opacity, shown(r)), (1.0, [255, 128, 128]));
|
|
}
|
|
|
|
#[test]
|
|
fn single_paints_keep_their_colour_and_overprint() {
|
|
let _settings = settings_lock();
|
|
let mut s = session();
|
|
let a = rect(&mut s, 10.0, 10.0, 100.0, 100.0, "#000000");
|
|
run(&mut s, "paint.setFill", json!({"ids": [a], "color": {"c": 0, "m": 100, "y": 50, "k": 0}}));
|
|
run(&mut s, "object.setOverprint", json!({"ids": [a], "fill": true}));
|
|
let b = rect(&mut s, 60.0, 60.0, 100.0, 100.0, "#0000ff");
|
|
run(&mut s, "transparency.set", json!({"ids": [b], "opacity": 50}));
|
|
let cmyk_region = |s: &Session, ids: &[u64]| regions(s, ids[0]).into_iter().find(|r| outline(r).winding(Point::new(30.0, 30.0)) != 0).unwrap();
|
|
let (ids, ..) = flatten(&mut s, json!({"ids": [a, b]}));
|
|
let r = cmyk_region(&s, &ids);
|
|
let AppearanceItem::Fill(f) = &r.appearance.items[0] else { panic!() };
|
|
assert!(matches!(f.paint.color(), Some(Color::Cmyk { .. })) && f.overprint, "{:?}", f);
|
|
run(&mut s, "edit.undo", json!({}));
|
|
let (ids, ..) = flatten(&mut s, json!({"ids": [a, b], "preserveOverprints": false}));
|
|
let r = cmyk_region(&s, &ids);
|
|
let AppearanceItem::Fill(f) = &r.appearance.items[0] else { panic!() };
|
|
assert!(matches!(f.paint.color(), Some(Color::Rgb { .. })) && !f.overprint);
|
|
}
|
|
|
|
#[test]
|
|
fn options_come_from_presets_and_params() {
|
|
let _settings = settings_lock();
|
|
use crate::cmd::FlattenOptions;
|
|
let high = FlattenOptions::preset("High Resolution").unwrap();
|
|
assert_eq!((high.balance, high.line_art_ppi, high.clip_complex_regions), (100.0, 1200.0, true));
|
|
assert_eq!(FlattenOptions::default(), FlattenOptions::preset("medium").unwrap());
|
|
assert_ne!(FlattenOptions::preset("low"), FlattenOptions::preset("medium"));
|
|
let o = FlattenOptions::from_params(&json!({"preset": "low", "balance": 40, "options": {"antiAlias": false}})).unwrap();
|
|
assert_eq!((o.balance, o.gradient_ppi, o.anti_alias), (40.0, 150.0, false));
|
|
for bad in [json!({"preset": "ultra"}), json!({"balance": 120}), json!({"lineArtPpi": 0}), json!({"balance": "lots"})] {
|
|
assert!(FlattenOptions::from_params(&bad).is_err(), "{bad}");
|
|
}
|
|
let mut s = session();
|
|
let a = rect(&mut s, 10.0, 10.0, 50.0, 50.0, "#ff0000");
|
|
run(&mut s, "transparency.set", json!({"ids": [a], "opacity": 50}));
|
|
assert!(s.execute("object.flattenTransparency", &json!({"ids": [a], "balance": -1})).is_err());
|
|
run(&mut s, "select.none", json!({}));
|
|
assert!(s.execute("object.flattenTransparency", &json!({})).is_err(), "nothing to flatten");
|
|
let r = run(&mut s, "object.flattenTransparency", json!({"ids": [a], "preset": "high"}));
|
|
assert_eq!(r["options"]["lineArtPpi"], 1200.0);
|
|
}
|
|
|
|
#[test]
|
|
fn a_mixed_scene_looks_the_same_flattened() {
|
|
let _settings = settings_lock();
|
|
for preset in FlattenPresets::ALL {
|
|
let mut s = session();
|
|
rect(&mut s, 0.0, 0.0, 200.0, 200.0, "#ffcc00");
|
|
let r = rect(&mut s, 30.0, 30.0, 80.0, 80.0, "#3355cc");
|
|
run(&mut s, "paint.setStroke", json!({"color": "#000000", "ids": [r]}));
|
|
run(&mut s, "stroke.set", json!({"weight": 8, "ids": [r]}));
|
|
run(&mut s, "transparency.set", json!({"ids": [r], "opacity": 60}));
|
|
let t = run(&mut s, "text.create", json!({"x": 40, "y": 170, "text": "Hi", "size": 48, "color": "#e01010"}))["id"].clone();
|
|
run(&mut s, "transparency.set", json!({"ids": [t], "opacity": 70, "blend": "Multiply"}));
|
|
let e = run(&mut s, "shape.ellipse", json!({"x": 120, "y": 40, "width": 60, "height": 60}))["id"].clone();
|
|
run(&mut s, "paint.setFill", json!({"color": "#20a040", "ids": [e]}));
|
|
run(&mut s, "effect.apply", json!({"effect": "stylize.dropShadow", "ids": [e]}));
|
|
let m = rect(&mut s, 130.0, 130.0, 40.0, 40.0, "#8040c0");
|
|
run(&mut s, "transparency.set", json!({"ids": [m], "opacity": 50}));
|
|
run(&mut s, "select.set", json!({"ids": [m]}));
|
|
let sym = run(&mut s, "symbol.new", json!({"name": "Half"}))["id"].as_u64().unwrap();
|
|
assert!(matches!(node(&s, sym).kind, NodeKind::SymbolInstance { .. }));
|
|
let before = picture(&s);
|
|
run(&mut s, "select.all", json!({}));
|
|
let (ids, vector, rasterized) = flatten(&mut s, json!({"preset": preset, "lineArtPpi": 216, "gradientPpi": 144}));
|
|
assert!(ids.len() == 1 && rasterized == 1, "{preset}: the shadow is rasterized ({ids:?}, {rasterized})");
|
|
assert!(preset == "low" || vector > 4, "{preset}: {vector} regions");
|
|
let mut kinds = vec![];
|
|
node(&s, ids[0]).walk(&mut |c| kinds.push(c.kind_label()));
|
|
assert!(!kinds.iter().any(|k| ["Type", "Symbol"].contains(k)), "{preset}: {kinds:?}");
|
|
let diff = changed(&before, &picture(&s));
|
|
assert!(diff < 0.03, "{preset}: {:.1}% of the pixels changed", diff * 100.0);
|
|
}
|
|
}
|
|
|
|
struct FlattenPresets;
|
|
impl FlattenPresets {
|
|
const ALL: [&'static str; 3] = crate::cmd::FlattenOptions::PRESETS;
|
|
}
|
|
|
|
#[test]
|
|
fn opacity_masks_are_rasterized_with_their_look() {
|
|
let _settings = settings_lock();
|
|
let mut s = session();
|
|
rect(&mut s, 0.0, 0.0, 200.0, 200.0, "#ffffff");
|
|
let a = rect(&mut s, 20.0, 20.0, 160.0, 160.0, "#d02060");
|
|
let m = rect(&mut s, 40.0, 40.0, 120.0, 120.0, "#000000");
|
|
run(
|
|
&mut s,
|
|
"paint.setFill",
|
|
json!({"ids": [m], "gradient": {"kind": "linear", "stops": [{"offset": 0, "color": "#ffffff"}, {"offset": 1, "color": "#000000"}]}}),
|
|
);
|
|
run(&mut s, "transparency.makeOpacityMask", json!({"ids": [a, m]}));
|
|
assert!(node(&s, a).mask.is_some());
|
|
let before = picture(&s);
|
|
run(&mut s, "select.all", json!({}));
|
|
let (ids, _, rasterized) = flatten(&mut s, json!({"lineArtPpi": 144}));
|
|
assert_eq!((ids.len(), rasterized), (1, 1));
|
|
let mut masks = 0;
|
|
node(&s, ids[0]).walk(&mut |c| masks += c.mask.is_some() as usize);
|
|
assert_eq!(masks, 0, "no transparency is left");
|
|
assert!(changed(&before, &picture(&s)) < 0.02);
|
|
}
|