mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
* 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]> * Paint Bucket and Edit > Stroke: a CMYK foreground writes the picked inks (#4351) Co-Authored-By: Claude Sonnet 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
46740ad4bc
commit
dc8a3f0b35
4 files changed
+91
-7
No files matched your search
@@ -63,6 +63,7 @@ fn composite_area(
|
||||
coverage: impl Fn(i32, i32) -> f32 + Sync,
|
||||
color: impl Fn(i32, i32) -> [f32; 4] + Sync,
|
||||
lock_transparency: bool,
|
||||
inks: Option<[f32; 4]>,
|
||||
) {
|
||||
// Composite tile by tile (in parallel on native); tiles with zero coverage are left untouched,
|
||||
// so their copy-on-write storage (and GPU residency) survives.
|
||||
@@ -93,6 +94,14 @@ fn composite_area(
|
||||
o[3] = d[3];
|
||||
}
|
||||
from_rgba_into(&fmt, o, &mut enc);
|
||||
// A colour picked as CMYK inks replaces fully covered pixels of a CMYK target as it is.
|
||||
if let Some(i) = inks
|
||||
&& fmt.mode == photocraft_color::ColorMode::Cmyk
|
||||
&& blend_mode == BlendMode::Normal
|
||||
&& k >= 1.0
|
||||
{
|
||||
enc[..4].copy_from_slice(&i);
|
||||
}
|
||||
px.copy_from_slice(&enc[..n]);
|
||||
}
|
||||
}
|
||||
@@ -144,6 +153,7 @@ pub fn paint_gradient(
|
||||
c
|
||||
},
|
||||
lock_transparency,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -179,6 +189,25 @@ pub fn bucket_fill_src(
|
||||
selection: Option<&Surface>,
|
||||
lock_transparency: bool,
|
||||
src: impl Fn(i32, i32) -> [f32; 4] + Sync,
|
||||
) -> bool {
|
||||
bucket_fill_inks(s, area, seed, tolerance, contiguous, anti_alias, opacity, selection, lock_transparency, src, None)
|
||||
}
|
||||
|
||||
/// [`bucket_fill_src`] where `inks` (CMYK, 0..1) are what a fully covered pixel of a CMYK target
|
||||
/// gets instead of `src`'s sRGB stand-in (a colour that was picked as CMYK).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn bucket_fill_inks(
|
||||
s: &mut Surface,
|
||||
area: Rect,
|
||||
seed: (i32, i32),
|
||||
tolerance: f32,
|
||||
contiguous: bool,
|
||||
anti_alias: bool,
|
||||
opacity: f32,
|
||||
selection: Option<&Surface>,
|
||||
lock_transparency: bool,
|
||||
src: impl Fn(i32, i32) -> [f32; 4] + Sync,
|
||||
inks: Option<[f32; 4]>,
|
||||
) -> bool {
|
||||
if !area.contains(seed.0, seed.1) {
|
||||
return false;
|
||||
@@ -186,7 +215,7 @@ pub fn bucket_fill_src(
|
||||
let img = crate::selection::rgba8_image(s, area);
|
||||
let Some(region) = crate::selection::wand_region(&img, area, seed, tolerance, contiguous, anti_alias) else { return false };
|
||||
drop(img);
|
||||
bucket_fill_region_src(s, ®ion, opacity, selection, lock_transparency, src);
|
||||
bucket_fill_region_inks(s, ®ion, opacity, selection, lock_transparency, src, inks);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -199,10 +228,31 @@ pub fn bucket_fill_region_src(
|
||||
selection: Option<&Surface>,
|
||||
lock_transparency: bool,
|
||||
src: impl Fn(i32, i32) -> [f32; 4] + Sync,
|
||||
) {
|
||||
bucket_fill_region_inks(s, region, opacity, selection, lock_transparency, src, None);
|
||||
}
|
||||
|
||||
/// [`bucket_fill_region_src`] with the CMYK `inks` of [`bucket_fill_inks`].
|
||||
pub fn bucket_fill_region_inks(
|
||||
s: &mut Surface,
|
||||
region: &crate::selection::Region,
|
||||
opacity: f32,
|
||||
selection: Option<&Surface>,
|
||||
lock_transparency: bool,
|
||||
src: impl Fn(i32, i32) -> [f32; 4] + Sync,
|
||||
inks: Option<[f32; 4]>,
|
||||
) {
|
||||
// Composite only over the filled region's box.
|
||||
let b = region.bbox;
|
||||
composite_area(s, b, BlendMode::Normal, |x, y| region.at(x, y) * opacity * selection.map_or(1.0, |m| m.sample_channel(x, y, 0)), src, lock_transparency);
|
||||
composite_area(
|
||||
s,
|
||||
b,
|
||||
BlendMode::Normal,
|
||||
|x, y| region.at(x, y) * opacity * selection.map_or(1.0, |m| m.sample_channel(x, y, 0)),
|
||||
src,
|
||||
lock_transparency,
|
||||
inks,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -136,6 +136,8 @@ fn stroke(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// Whole pixels, rounded up, as in Photoshop (0.5 px strokes 1 px).
|
||||
let width = p.get("width").and_then(Value::as_f64).filter(|w| w.is_finite()).unwrap_or(1.0).clamp(1.0, 250.0).ceil() as usize;
|
||||
let color = color_param(p, "color", s.tools.foreground);
|
||||
// A colour picked as CMYK strokes a CMYK document with the picked inks (#4196).
|
||||
let picked = s.tools.picked_cmyk([color[0], color[1], color[2]]);
|
||||
let mode = match p.get("mode") {
|
||||
None | Some(Value::Null) => photocraft_color::BlendMode::Normal,
|
||||
Some(Value::String(m)) => crate::commands::blend_from_str(m)
|
||||
@@ -214,7 +216,7 @@ fn stroke(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let surf = crate::commands::paint_surface(doc, id, &Value::Null)?;
|
||||
let area = band.content_bounds().intersect(&canvas);
|
||||
if !area.is_empty() {
|
||||
crate::fill_cmds::blend_color_mask(surf, area, color, &band, mode, opacity, preserve || lock)?;
|
||||
crate::fill_cmds::blend_color_mask(surf, area, color, picked, &band, mode, opacity, preserve || lock)?;
|
||||
surf.prune();
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -215,16 +215,22 @@ pub(crate) fn fill_color(surf: &mut Surface, area: Rect, color: [f32; 4], sel: O
|
||||
/// Reuse Edit › Fill's compositing for Edit › Stroke. The stroke's rasterized band
|
||||
/// is already a grayscale coverage mask; opacity, blend modes and alpha locking
|
||||
/// must behave identically for both commands at 8/16/32-bit depth.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn blend_color_mask(
|
||||
surf: &mut Surface,
|
||||
area: Rect,
|
||||
color: [f32; 4],
|
||||
picked: Option<[f32; 4]>,
|
||||
coverage: &Surface,
|
||||
mode: BlendMode,
|
||||
opacity: f32,
|
||||
preserve_transparency: bool,
|
||||
) -> Result<()> {
|
||||
blend_into(surf, area, &Source::Color(color), Some(coverage), Blend { mode, opacity, keep_alpha: preserve_transparency, restore: false })
|
||||
let source = match picked {
|
||||
Some(cmyk) if color[3] >= 1.0 => Source::Cmyk { rgba: color, cmyk },
|
||||
_ => Source::Color(color),
|
||||
};
|
||||
blend_into(surf, area, &source, Some(coverage), Blend { mode, opacity, keep_alpha: preserve_transparency, restore: false })
|
||||
}
|
||||
|
||||
/// The selection filled from its surroundings (`photocraft_algo::content_aware`), as straight
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Paint helpers: Paint Bucket and Gradient tool.
|
||||
|
||||
use photocraft_algo::paint::{GradientShape, bucket_fill, bucket_fill_region_src, bucket_fill_src, paint_gradient};
|
||||
use photocraft_algo::paint::{GradientShape, bucket_fill_inks, bucket_fill_region_inks, bucket_fill_region_src, bucket_fill_src, paint_gradient};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::commands::{CommandSpec, blend_from_str};
|
||||
@@ -61,6 +61,8 @@ fn bucket(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// A foreground picked as CMYK fills a CMYK document with the picked inks (#4196).
|
||||
let inks = s.tools.picked_cmyk([c[0], c[1], c[2]]).filter(|_| c[3] >= 1.0 && pattern.is_none());
|
||||
let filled = s.edit("Paint Bucket", |doc, active| {
|
||||
let area = doc.bounds();
|
||||
let sel = doc.selection.clone();
|
||||
@@ -80,10 +82,10 @@ fn bucket(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
bucket_fill_src(surf, area, (x, y), tol, contiguous, aa, opacity, sel.as_ref(), lock, src)
|
||||
}
|
||||
} else if let Some(region) = ®ion {
|
||||
bucket_fill_region_src(surf, region, opacity, sel.as_ref(), lock, |_, _| c);
|
||||
bucket_fill_region_inks(surf, region, opacity, sel.as_ref(), lock, |_, _| c, inks);
|
||||
true
|
||||
} else {
|
||||
bucket_fill(surf, area, (x, y), tol, contiguous, aa, c, opacity, sel.as_ref(), lock)
|
||||
bucket_fill_inks(surf, area, (x, y), tol, contiguous, aa, opacity, sel.as_ref(), lock, |_, _| c, inks)
|
||||
};
|
||||
surf.prune();
|
||||
Ok(ok)
|
||||
@@ -330,6 +332,30 @@ mod tests {
|
||||
assert_eq!(px(&s, 5, 5), vec![0.0, 0.0, 1.0, 1.0]);
|
||||
}
|
||||
|
||||
/// #4196: the Paint Bucket and Edit > Stroke write a CMYK pick as its inks into a CMYK document,
|
||||
/// not as its sRGB stand-in converted back (100/0/0/0 cyan would come out as about 100/28/0/17).
|
||||
#[test]
|
||||
fn bucket_and_stroke_write_a_cmyk_pick_as_its_inks() {
|
||||
let cyan = [1.0f32, 0.0, 0.0, 0.0];
|
||||
let mut s = session();
|
||||
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();
|
||||
let inks_at = |s: &Session, x: i32, y: i32| -> Vec<f32> {
|
||||
let st = s.active().unwrap();
|
||||
let surf = st.doc.layer(st.active_layer.unwrap()).unwrap().surface().unwrap();
|
||||
surf.read_region(Rect::new(x, y, x + 1, y + 1)).into_iter().take(4).collect()
|
||||
};
|
||||
let is_cyan = |got: &[f32]| got.iter().zip(cyan).all(|(a, b)| (a - b).abs() < 0.02);
|
||||
s.execute("paint.bucket", json!({"x": 2, "y": 2, "antiAlias": false})).unwrap();
|
||||
let got = inks_at(&s, 5, 5);
|
||||
assert!(is_cyan(&got), "bucket: {got:?}");
|
||||
s.execute("select.all", json!({})).unwrap();
|
||||
s.execute("edit.stroke", json!({"width": 2, "location": "inside", "color": rgb})).unwrap();
|
||||
let got = inks_at(&s, 0, 5);
|
||||
assert!(is_cyan(&got), "stroke: {got:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_fills_with_a_pattern() {
|
||||
// Paint Bucket with contents="pattern" flood-fills the region with a pattern, not a colour.
|
||||
|
||||
Reference in new issue
Block a user