mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 20:58:00 -06:00
Fix #3232: a CMYK gradient mesh keeps its K when lightened
Create Gradient Mesh's To Center / To Edge highlight lightened every point towards RGB white, so a CMYK (or grey) point became display RGB. A patch with any non-CMYK corner then mixes in RGB, and the PDF writer separated those colours back to CMYK with K = 0, moving ink off the K plate. Points now lighten towards white in their own colour model (no ink for CMYK), so the mesh stays CMYK and K shrinks with the other inks. Flat meshes (highlight 0) already kept their inks.
This commit is contained in:
@@ -1153,3 +1153,88 @@ fn mesh_create_on_an_image_keeps_alpha_and_refuses_linked_and_big_grids() {
|
||||
assert!(e.contains("linked"), "{e}");
|
||||
assert_eq!(node(&s, id).kind_label(), "Image", "left as it was");
|
||||
}
|
||||
|
||||
/// A CMYK document with the #3232 rectangle filled C0 M40 Y60 K10, made a 4×4 mesh with `create`.
|
||||
fn cmyk_mesh(create: Value) -> (Session, NodeId) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 226.77, "height": 170.08, "colorMode": "cmyk"})).unwrap();
|
||||
let a = rect(&mut s, 28.35, 28.35, 170.08, 113.39);
|
||||
s.execute("paint.setStroke", &json!({"ids": [a.0], "none": true})).unwrap();
|
||||
s.execute("paint.setFill", &json!({"ids": [a.0], "color": {"c": 0, "m": 0.4, "y": 0.6, "k": 0.1}})).unwrap();
|
||||
let mut p = json!({"ids": [a.0], "rows": 4, "cols": 4});
|
||||
p.as_object_mut().unwrap().extend(create.as_object().unwrap().clone());
|
||||
s.execute("object.mesh.create", &p).unwrap();
|
||||
(s, a)
|
||||
}
|
||||
|
||||
/// The fill colours `document.exportPdf {standard: pdfX4}` writes (`… scn` operands), and its warnings.
|
||||
fn pdf_fill_colours(s: &mut Session) -> (Vec<Vec<f32>>, Vec<String>) {
|
||||
let v = s.execute("document.exportPdf", &json!({"standard": "pdfX4", "compression": {"compressText": false}})).unwrap();
|
||||
let bytes = vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap();
|
||||
let text = String::from_utf8_lossy(&bytes).into_owned();
|
||||
let mut out = vec![];
|
||||
for line in text.lines() {
|
||||
let words: Vec<&str> = line.split_whitespace().collect();
|
||||
for (i, w) in words.iter().enumerate() {
|
||||
if *w == "scn" {
|
||||
let nums: Vec<f32> = words[..i].iter().rev().map_while(|x| x.parse().ok()).collect();
|
||||
out.push(nums.into_iter().rev().collect());
|
||||
}
|
||||
}
|
||||
}
|
||||
(out, serde_json::from_value(v["warnings"].clone()).unwrap())
|
||||
}
|
||||
|
||||
/// #3232: a mesh made from a CMYK fill in a CMYK document keeps its points in CMYK, and the PDF
|
||||
/// writes them with their K, not separated again from RGB (C/M/Y with K = 0).
|
||||
#[test]
|
||||
fn cmyk_mesh_keeps_its_inks_in_pdf() {
|
||||
for app in ["flat", "center", "edge"] {
|
||||
let (mut s, a) = cmyk_mesh(json!({"appearance": app, "highlight": 0}));
|
||||
assert!(mesh_of(&s, a).points.iter().all(|p| p.color == Color::cmyk(0.0, 0.4, 0.6, 0.1)), "{app}");
|
||||
let (fills, warnings) = pdf_fill_colours(&mut s);
|
||||
assert!(!fills.is_empty() && warnings.is_empty(), "{app}: {warnings:?}");
|
||||
for f in &fills {
|
||||
let [c, m, y, k] = f[..] else { panic!("{app}: not CMYK {f:?}") };
|
||||
// The writer keeps 8 bits per ink: within one step of C0 M40 Y60 K10.
|
||||
let near = |a: f32, b: f32| (a - b).abs() <= 1.0 / 255.0 + 1e-6;
|
||||
assert!(near(c, 0.0) && near(m, 0.4) && near(y, 0.6) && near(k, 0.1), "{app}: {f:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #3232: lightening to the centre or the edges lightens CMYK points towards paper white in CMYK,
|
||||
/// so K shrinks with the other inks instead of being dropped.
|
||||
#[test]
|
||||
fn cmyk_mesh_highlight_lightens_in_cmyk() {
|
||||
for app in ["center", "edge"] {
|
||||
let (mut s, a) = cmyk_mesh(json!({"appearance": app, "highlight": 50}));
|
||||
let m = mesh_of(&s, a);
|
||||
let (corner, centre) = (m.points[0].color, m.points[m.idx(2, 2)].color);
|
||||
let (lit, plain) = if app == "center" { (centre, corner) } else { (corner, centre) };
|
||||
assert_eq!(plain, Color::cmyk(0.0, 0.4, 0.6, 0.1), "{app}");
|
||||
let Color::Cmyk { c, m: mg, y, k } = lit else { panic!("{app}: {lit:?}") };
|
||||
assert!(c.abs() < 1e-6 && (mg - 0.2).abs() < 1e-5 && (y - 0.3).abs() < 1e-5 && (k - 0.05).abs() < 1e-5, "{app}: {lit:?}");
|
||||
assert!(m.points.iter().all(|p| matches!(p.color, Color::Cmyk { .. })), "{app}");
|
||||
let (fills, warnings) = pdf_fill_colours(&mut s);
|
||||
assert!(warnings.is_empty(), "{app}: no RGB to separate {warnings:?}");
|
||||
for f in &fills {
|
||||
let [c, m, y, k] = f[..] else { panic!("{app}: not CMYK {f:?}") };
|
||||
// Every ink in proportion to C0 M40 Y60 K10, lightened by at most half.
|
||||
assert!(c < 1.5 / 255.0 && (0.05 - 1.5 / 255.0..=0.1 + 1.5 / 255.0).contains(&k), "{app}: {f:?}");
|
||||
assert!((m / 0.4 - k / 0.1).abs() < 0.06 && (y / 0.6 - k / 0.1).abs() < 0.06, "{app}: {f:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #3232: a CMYK colour given to a mesh point stays CMYK, and the PDF keeps its K.
|
||||
#[test]
|
||||
fn cmyk_mesh_point_colour_keeps_k() {
|
||||
let (mut s, a) = cmyk_mesh(json!({"appearance": "flat"}));
|
||||
s.execute("object.mesh.setPointColor", &json!({"id": a.0, "index": 0, "color": {"c": 0, "m": 0, "y": 0, "k": 1}})).unwrap();
|
||||
assert_eq!(mesh_of(&s, a).points[0].color, Color::cmyk(0.0, 0.0, 0.0, 1.0));
|
||||
let (fills, warnings) = pdf_fill_colours(&mut s);
|
||||
assert!(warnings.is_empty(), "{warnings:?}");
|
||||
assert!(fills.iter().all(|f| f.len() == 4 && f[0] < 1.5 / 255.0), "no cyan from a CMY black: {fills:?}");
|
||||
assert!(fills.iter().any(|f| f[3] > 0.8), "the black corner prints on K: {fills:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user