Files
vectorcraft/crates/engine/src/tests_cmykflatten.rs
T
bflatasticandClaude Opus 5.5 9537eca5f7 M3.90: blend modes and opacity composite inks in CMYK documents
A CMYK document that shows transparency is drawn once per ink plane
(complemented C, M, Y; complemented K as a grey) so every blend mode,
opacity, group, knockout and effect composites inks, PDF-style; the
planes' inks are shown through the working CMYK profile (cached 4-D XYZ
table). Cyan multiplied over magenta now shows C100 M100. Opacity masks
keep screen luminance; RGB colours and placed images are separated
first. RGB documents and opaque CMYK documents keep the screen-colour
path.

PDF export of CMYK documents writes its transparency groups with
/CS /DeviceCMYK (luminosity mask groups stay RGB) and puts top-level
transparency in a non-isolated page-wide group so it blends in CMYK too.
Flatten Transparency composites CMYK documents ink by ink and makes CMYK
regions. Non-separable modes no longer give NaN for greys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:30:06 +07:00

75 lines
3.8 KiB
Rust

//! Flatten Transparency in CMYK documents composites inks, as the canvas shows them (M3.90).
use serde_json::{Value, json};
use vectorcraft_color::{BlendMode, Color};
use vectorcraft_doc::{AppearanceItem, Node, NodeKind};
use vectorcraft_geom::{Point, Rect, Shape};
use vectorcraft_render::Renderer;
use super::*;
fn run(s: &mut Session, id: &str, p: Value) -> Value {
s.execute(id, &p).unwrap()
}
/// A CMYK document with an unstroked rectangle of inks `cmyk` (percentages) at (10, 10) and one
/// at (60, 60), 100 pt square.
fn session(below: [f64; 4], above: [f64; 4]) -> (Session, u64, u64) {
let mut s = Session::new();
run(&mut s, "file.new", json!({"width": 200, "height": 200, "colorMode": "cmyk"}));
let mut rect = |x: f64, [c, m, y, k]: [f64; 4]| {
let id = run(&mut s, "shape.rectangle", json!({"x": x, "y": x, "width": 100, "height": 100}))["id"].as_u64().unwrap();
run(&mut s, "paint.setFill", json!({"color": {"c": c, "m": m, "y": y, "k": k}, "ids": [id]}));
run(&mut s, "paint.setStroke", json!({"none": true, "ids": [id]}));
id
};
let (a, b) = (rect(10.0, below), rect(60.0, above));
(s, a, b)
}
/// The fill colour of the flattened region at `p` in group `group`.
fn region_color(s: &Session, group: u64, p: Point) -> Color {
let doc = &s.doc().unwrap().doc;
let g = doc.node(NodeId(group)).unwrap();
let inside = |n: &Node| matches!(&n.kind, NodeKind::Path { path, .. } if path.to_bezpath().winding(p) != 0);
let r = g.children().unwrap().iter().find(|c| inside(c)).expect("a region there");
let AppearanceItem::Fill(f) = &r.appearance.items[0] else { panic!("a region has one fill") };
f.paint.color().unwrap()
}
fn picture(s: &Session) -> vectorcraft_render::Rendered {
Renderer::new().render_region(&s.doc().unwrap().doc, Rect::new(0.0, 0.0, 200.0, 200.0), 1.0, true)
}
#[test]
fn cmyk_documents_flatten_to_ink_colours() {
let _settings = crate::tests_colormgmt::GLOBAL.lock().unwrap_or_else(|e| e.into_inner());
let (mut s, a, b) = session([0.0, 100.0, 0.0, 0.0], [100.0, 0.0, 0.0, 0.0]);
run(&mut s, "transparency.set", json!({"ids": [b], "blend": "Multiply"}));
let ids = run(&mut s, "object.flattenTransparency", json!({"ids": [a, b]}))["ids"].as_array().unwrap()[0].as_u64().unwrap();
// Multiply prints both inks where they overlap.
let near = |c: Color, want: [f32; 4]| matches!(c, Color::Cmyk { c, m, y, k } if [c, m, y, k].iter().zip(want).all(|(x, w)| (x - w).abs() < 1e-3));
let overlap = region_color(&s, ids, Point::new(85.0, 85.0));
assert!(near(overlap, [1.0, 1.0, 0.0, 0.0]), "{overlap:?}");
assert!(near(region_color(&s, ids, Point::new(130.0, 130.0)), [1.0, 0.0, 0.0, 0.0]));
}
#[test]
fn every_blend_mode_flattens_to_the_canvas_pixels_in_cmyk() {
let _settings = crate::tests_colormgmt::GLOBAL.lock().unwrap_or_else(|e| e.into_inner());
for mode in BlendMode::ALL {
let (mut s, a, b) = session([20.0, 60.0, 10.0, 30.0], [70.0, 10.0, 50.0, 10.0]);
run(&mut s, "transparency.set", json!({"ids": [b], "blend": mode.label(), "opacity": 70}));
let before = picture(&s);
let ids = run(&mut s, "object.flattenTransparency", json!({"ids": [a, b]}))["ids"].as_array().unwrap()[0].as_u64().unwrap();
for p in [Point::new(30.5, 30.5), Point::new(85.5, 85.5), Point::new(140.5, 140.5)] {
let c = region_color(&s, ids, p);
assert!(matches!(c, Color::Cmyk { .. }), "{mode:?}: {c:?}");
let got = c.to_rgba8(1.0);
let want = before.pixel(p.x as u32, p.y as u32);
// The canvas rounds each ink plane to 8 bits and interpolates the profile.
assert!((0..3).all(|i| got[i].abs_diff(want[i]) <= 4), "{mode:?} at {p:?}: region {got:?}, canvas {want:?}");
}
}
}