Merge pull request #6187 from dotCooCoo/fix/generic-cmyk-solver-converges

Color: Generic CMYK separates nearby colors alike when an ink sits at 0 or 100%
This commit is contained in:
BFlat
2026-11-08 14:41:59 +08:00
committed by GitHub
3 changed files with 56 additions and 1 deletions
+15
View File
@@ -231,6 +231,21 @@ impl GenericCmyk {
}
g[p] = -(0..3).map(|i| j[i][p] * r[i]).sum::<f32>();
}
// An ink at a bound whose descent direction points out of the box stays where it is; the step moves the other inks.
for p in 0..3 {
if (x[p] <= 0.0 && g[p] < 0.0) || (x[p] >= 1.0 && g[p] > 0.0) {
a[p] = [0.0; 3];
for row in &mut a {
row[p] = 0.0;
}
a[p][p] = 1.0;
g[p] = 0.0;
}
}
// Every ink is held at a bound, or the gradient is zero: no step can lower the error.
if g == [0.0; 3] {
break;
}
let mut improved = false;
for _ in 0..8 {
let mut al = a;
+40
View File
@@ -259,6 +259,46 @@ fn generic_cmyk_black_round_trips_with_bpc() {
assert!(raw.iter().all(|v| *v > 0.05), "{raw:?}");
}
/// A color whose solve holds cyan at 0 separates to the same inks when its values change by a few
/// float steps: hex ed1e79 with its red stepped one `f32` at a time over 4,001 values (about 0.00024
/// in all) stays within 0.001 on every ink.
#[test]
fn generic_cmyk_separates_nearby_colors_at_the_gamut_edge_alike() {
let c = cms();
let pink = [237.0f32 / 255.0, 30.0 / 255.0, 121.0 / 255.0];
let (mut lo, mut hi) = ([f32::MAX; 4], [f32::MIN; 4]);
for step in -2000i32..=2000 {
let mut rgb = pink;
rgb[0] = f32::from_bits(pink[0].to_bits().wrapping_add_signed(step));
let ink = c.srgb_to_cmyk(rgb, Intent::RelativeColorimetric);
for i in 0..4 {
lo[i] = lo[i].min(ink[i]);
hi[i] = hi[i].max(ink[i]);
}
}
assert!((0..4).all(|i| hi[i] - lo[i] < 1e-3), "{lo:?} to {hi:?}");
assert_eq!(lo[0], 0.0, "{lo:?}");
}
/// A color whose solve holds cyan and magenta at 1 separates to the same inks when its values change
/// by a few float steps: sRGB (0, 0, 0.25) with its blue stepped one `f32` at a time over 121 values
/// stays within 0.001 on every ink.
#[test]
fn generic_cmyk_separates_nearby_colors_with_inks_at_full_alike() {
let c = cms();
let (mut lo, mut hi) = ([f32::MAX; 4], [f32::MIN; 4]);
for step in -60i32..=60 {
let blue = f32::from_bits(0.25f32.to_bits().wrapping_add_signed(step));
let ink = c.srgb_to_cmyk([0.0, 0.0, blue], Intent::RelativeColorimetric);
for i in 0..4 {
lo[i] = lo[i].min(ink[i]);
hi[i] = hi[i].max(ink[i]);
}
}
assert!((0..4).all(|i| hi[i] - lo[i] < 1e-3), "{lo:?} to {hi:?}");
assert_eq!((lo[0], lo[1]), (1.0, 1.0), "{lo:?}");
}
#[test]
fn lab_colours_round_trip_through_the_cms() {
let c = cms_no_bpc();
+1 -1
View File
@@ -503,7 +503,7 @@ fn a_cmyk_document_starts_with_cmyk_swatches_and_stores_new_colours_as_cmyk() {
// A swatch made from a hex colour is CMYK as well; the document round-trips natively.
let name = run(&mut s, "swatch.new", json!({"color": "#00ff00"}))["name"].as_str().unwrap().to_string();
assert_eq!(doc(&s).swatch(&name).and_then(|w| w.paint.color()), Some(managed(Color::rgb(0.0, 1.0, 0.0))));
assert_eq!(name, "C=62 M=0 Y=100 K=0");
assert_eq!(name, "C=61 M=0 Y=100 K=0");
let json = serde_json::to_string(doc(&s)).unwrap();
assert_eq!(serde_json::from_str::<vectorcraft_doc::Document>(&json).unwrap(), *doc(&s));
}