Brush: CMYK foreground paints the picked inks into a CMYK document (#4344) (#4347)

* Color Picker: CMYK foreground fills a CMYK document with the picked inks (#4196)

The tool colours are stored as sRGB, so a colour picked as CMYK 100/0/0/0
(outside sRGB) came back as 100/28/0/17 when a CMYK document was filled.
tools.setColors now takes foregroundCmyk/backgroundCmyk, the picker sends
the entered inks, and Edit > Fill (and the fill keys) write them as they
are into a CMYK target. Fill also enters the document's CMYK profile.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Brush and Pencil paint a CMYK foreground with the picked inks in a CMYK document (#4344)

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Valentin KleinandClaude Opus 5.5 authored and GitHub committed 2026-10-12 19:47:14 -04:00
1 parent f48a72cbe3
commit 46740ad4bc
4 files changed
+33 -1

No files matched your search

+2
View File
@@ -204,6 +204,7 @@ pub fn resolve_brush(s: &Session, p: &Value, cmd: &str) -> Result<BrushSettings>
b.smoothing.amount = v.clamp(0.0, 1.0);
}
b.color = color(p.get("color"), s.tools.foreground);
b.color_cmyk = s.tools.picked_cmyk([b.color[0], b.color[1], b.color[2]]).map(|k| ([b.color[0], b.color[1], b.color[2]], k));
b.background = s.tools.background;
b.erase = flag(p, "erase", false);
b.seed = match p.get("seed").and_then(Value::as_u64) {
@@ -367,6 +368,7 @@ fn block_brush(b: BrushSettings, p: &Value) -> Result<BrushSettings> {
pressure_opacity: false,
spacing: 0.25,
color: b.color,
color_cmyk: b.color_cmyk,
background: b.background,
erase: b.erase,
smoothing: b.smoothing,
+18
View File
@@ -883,3 +883,21 @@ fn clear_mode_erases_with_the_brush_or_pencil() {
assert_eq!(s.active().unwrap().history.undo_label(), Some(label), "{cmd}");
}
}
/// #4344: a foreground picked as CMYK 100/0/0/0 is painted into a CMYK document as those inks by
/// the Brush and the Pencil, not as whatever its sRGB stand-in converts back to (100/28/0/17).
#[test]
fn brush_and_pencil_paint_a_cmyk_pick_with_the_picked_inks() {
let cyan = [1.0f32, 0.0, 0.0, 0.0];
for cmd in ["paint.stroke", "paint.pencil"] {
let mut s = session(16, 16);
s.execute("image.mode.cmyk", json!({})).unwrap();
let rgb = photocraft_color::convert::cmyk_to_rgb(cyan).map(|c| c.clamp(0.0, 1.0));
s.execute("tools.setColors", json!({"foreground": rgb, "foregroundCmyk": cyan})).unwrap();
s.execute(cmd, json!({"points": [[8.0, 8.0]], "size": 6.0, "hardness": 1.0})).unwrap();
let st = s.active().unwrap();
let surf = st.doc.layer(st.active_layer.unwrap()).unwrap().surface().unwrap();
let got: Vec<f32> = surf.read_region(Rect::new(8, 8, 9, 9)).into_iter().take(4).collect();
assert!(got.iter().zip(cyan).all(|(a, b)| (a - b).abs() < 0.02), "{cmd}: {got:?}");
}
}
+6
View File
@@ -486,6 +486,11 @@ pub struct BrushSettings {
pub pressure_opacity: bool,
/// Straight RGBA colour (display RGB). Commands fill it from the foreground colour.
pub color: [f32; 4],
/// The inks `color` was picked as (its sRGB colour, the CMYK values), for a CMYK target: the
/// stroke paints exactly these, since an sRGB round trip does not give them back (#4344).
/// Only valid while `color` is that sRGB colour; ignored with Color Dynamics.
#[serde(skip)]
pub color_cmyk: Option<([f32; 3], [f32; 4])>,
/// Background colour for Color Dynamics' foreground/background jitter.
pub background: [f32; 4],
/// Erase instead of paint.
@@ -545,6 +550,7 @@ impl Default for BrushSettings {
pressure_size: true,
pressure_opacity: false,
color: [0.0, 0.0, 0.0, 1.0],
color_cmyk: None,
background: [1.0, 1.0, 1.0, 1.0],
erase: false,
mode: BlendMode::Normal,
+7 -1
View File
@@ -10,7 +10,7 @@
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use photocraft_color::{BlendMode, PixelFormat, SampleType};
use photocraft_color::{BlendMode, ColorMode, PixelFormat, SampleType};
use photocraft_geom::Rect;
use photocraft_raster::{Surface, from_rgba_into, to_rgba};
@@ -966,6 +966,12 @@ impl StrokeRenderer {
let opacity = b.opacity.clamp(0.0, 1.0);
let mut src = [0.0f32; 8];
from_rgba_into(&fmt, b.color, &mut src);
// A colour picked as CMYK paints its inks as they are in a CMYK target (#4344).
if let (ColorMode::Cmyk, Some((rgb, inks))) = (fmt.mode, b.color_cmyk)
&& rgb == [b.color[0], b.color[1], b.color[2]]
{
src[..4].copy_from_slice(&inks);
}
if let Some(a) = a_idx {
src[a] = b.color[3];
}