diff --git a/crates/color/src/cms/generic.rs b/crates/color/src/cms/generic.rs index cea213cd6..95abdf2f3 100644 --- a/crates/color/src/cms/generic.rs +++ b/crates/color/src/cms/generic.rs @@ -231,6 +231,21 @@ impl GenericCmyk { } g[p] = -(0..3).map(|i| j[i][p] * r[i]).sum::(); } + // 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; diff --git a/crates/color/src/cms/tests.rs b/crates/color/src/cms/tests.rs index 99c16edf4..ecb4c1c59 100644 --- a/crates/color/src/cms/tests.rs +++ b/crates/color/src/cms/tests.rs @@ -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(); diff --git a/crates/engine/src/tests/swatchcmds.rs b/crates/engine/src/tests/swatchcmds.rs index 53876a218..2a74e194d 100644 --- a/crates/engine/src/tests/swatchcmds.rs +++ b/crates/engine/src/tests/swatchcmds.rs @@ -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::(&json).unwrap(), *doc(&s)); }