From ebc5b7038c58c7579da4326bcab2b4b2bd76c567 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 30 Oct 2026 23:51:56 +0700 Subject: [PATCH] =?UTF-8?q?Add-ons:=20Color=20Effects=20=E2=80=94=20Duoton?= =?UTF-8?q?e=20effect=20and=20Print=20Ink=20Fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Effect › Color Effects › Duotone… (addon.color.duotone, also in Object › Plug-ins › Color Effects) prints the grey-equivalent tone of each colour with one to four inks, each with its own tone curve. It is a colour adjustment like the add-on's others, so it stacks with them and works as a filter through addon.color.filter. - One tone of a swatch the document has gives a tint of that swatch, linked, so a spot tone separates onto its plate; several tones give the CMYK mix of their tints (the inks overprint, so they multiply). - Colour adjustments now map paints with their swatch links (ColorMap::apply_link); the existing adjustments behave as before. - Skip Objects with Spot Colors leaves spot-linked colours as they are. - The dialog has a row per tone and the picked tone's curve (the Curves graph, reused), its points and Reset to Linear / Inverse. Print Ink Fixes (addon.color.printFixes, Object › Plug-ins › Color Effects › Print Ink Fixes…) scales CMYK colours over a total ink limit down to it, removes white overprint and sets black overprint (Overprint Black's logic) in a CMYK document, as one undo step, and reports what changed. RGB documents are refused. The panel opens both under Inks. Deferred: an opaque undercoat, tones for type only, extra gradient stops for smoother tints, curve flip and all-100 % buttons. --- crates/effects/src/addons.rs | 3 + crates/effects/src/addons/color.rs | 14 +- crates/effects/src/addons/duotone.rs | 178 ++++++++++++++++++ crates/effects/src/addons/duotone/tests.rs | 128 +++++++++++++ crates/effects/src/adjust.rs | 109 ++++++++--- crates/engine/src/cmd/addons/color.rs | 29 ++- .../src/cmd/addons/color/print_fixes.rs | 170 +++++++++++++++++ .../src/cmd/addons/color/print_fixes/tests.rs | 129 +++++++++++++ crates/engine/src/cmd/addons/color/tests.rs | 76 ++++++++ crates/engine/src/cmd/colorcmds.rs | 6 + crates/engine/tests/command_sweep.rs | 16 ++ crates/render/src/fx/renderer.rs | 13 +- crates/ui-egui/src/addons/color.rs | 36 +++- crates/ui-egui/src/addons/color/tests.rs | 57 +++++- crates/ui-egui/src/dialogs.rs | 1 + crates/ui-egui/src/dialogs/command.rs | 1 + crates/ui-egui/src/dialogs/duotone.rs | 123 ++++++++++++ crates/ui-egui/src/dialogs/effect.rs | 33 ++-- crates/ui-egui/src/i18n/bg.tsv | 23 +++ crates/ui-egui/src/i18n/ca.tsv | 23 +++ crates/ui-egui/src/i18n/cs.tsv | 23 +++ crates/ui-egui/src/i18n/da.tsv | 23 +++ crates/ui-egui/src/i18n/de.tsv | 23 +++ crates/ui-egui/src/i18n/el.tsv | 23 +++ crates/ui-egui/src/i18n/es.tsv | 23 +++ crates/ui-egui/src/i18n/fi.tsv | 23 +++ crates/ui-egui/src/i18n/fr.tsv | 23 +++ crates/ui-egui/src/i18n/hu.tsv | 23 +++ crates/ui-egui/src/i18n/id.tsv | 23 +++ crates/ui-egui/src/i18n/it.tsv | 23 +++ crates/ui-egui/src/i18n/ja.tsv | 23 +++ crates/ui-egui/src/i18n/ka.tsv | 23 +++ crates/ui-egui/src/i18n/ko.tsv | 23 +++ crates/ui-egui/src/i18n/nl.tsv | 23 +++ crates/ui-egui/src/i18n/pl.tsv | 23 +++ crates/ui-egui/src/i18n/pt-br.tsv | 23 +++ crates/ui-egui/src/i18n/ru.tsv | 23 +++ crates/ui-egui/src/i18n/sv.tsv | 23 +++ crates/ui-egui/src/i18n/tr.tsv | 23 +++ crates/ui-egui/src/i18n/uk.tsv | 23 +++ crates/ui-egui/src/i18n/vi.tsv | 23 +++ crates/ui-egui/src/i18n/zh-hans.tsv | 23 +++ crates/ui-egui/src/i18n/zh-hant.tsv | 23 +++ docs/mcp.md | 57 ++++++ docs/target-app-parity.md | 15 +- docs/ui-parity.md | 5 +- 46 files changed, 1709 insertions(+), 65 deletions(-) create mode 100644 crates/effects/src/addons/duotone.rs create mode 100644 crates/effects/src/addons/duotone/tests.rs create mode 100644 crates/engine/src/cmd/addons/color/print_fixes.rs create mode 100644 crates/engine/src/cmd/addons/color/print_fixes/tests.rs create mode 100644 crates/ui-egui/src/dialogs/duotone.rs diff --git a/crates/effects/src/addons.rs b/crates/effects/src/addons.rs index d1ba0783d..e02c4b22c 100644 --- a/crates/effects/src/addons.rs +++ b/crates/effects/src/addons.rs @@ -2,8 +2,11 @@ //! //! - [`color`]: Color Effects' live Exposure, Desaturate and Invert Colors, colour adjustments //! evaluated with the others ([`crate::adjust`]). +//! - [`duotone`]: Color Effects' live Duotone, a colour adjustment that prints each colour's tone +//! with one to four inks. //! - [`halftone`]: Color Effects' live Vector Halftone, and the screens Object › Vector Halftone //! builds its dots with. pub mod color; +pub mod duotone; pub mod halftone; diff --git a/crates/effects/src/addons/color.rs b/crates/effects/src/addons/color.rs index 2d5ebea91..07627dd8d 100644 --- a/crates/effects/src/addons/color.rs +++ b/crates/effects/src/addons/color.rs @@ -1,6 +1,6 @@ -//! Color Effects (a built-in add-on): live Exposure, Desaturate and Invert Colors, in Effect › Color -//! Effects. They are colour adjustments ([`crate::is_adjustment`]): they recolour what an object -//! paints as the others do, in stack order with them. +//! Color Effects (a built-in add-on): live Exposure, Desaturate, Duotone and Invert Colors, in +//! Effect › Color Effects. They are colour adjustments ([`crate::is_adjustment`]): they recolour what +//! an object paints as the others do, in stack order with them. use serde_json::{Value, json}; @@ -13,9 +13,10 @@ pub const EXPOSURE: &str = "addon.color.exposure"; pub const DESATURATE: &str = "addon.color.desaturate"; /// Invert Colors: every colour to its RGB inverse. pub const INVERT: &str = "addon.color.invert"; +pub use super::duotone::DUOTONE; /// The add-on's effects, in menu order. -pub const EFFECTS: [&str; 3] = [DESATURATE, EXPOSURE, INVERT]; +pub const EFFECTS: [&str; 4] = [DESATURATE, DUOTONE, EXPOSURE, INVERT]; /// Is `id` one of the add-on's effects? pub fn is_color_effect(id: &str) -> bool { @@ -28,6 +29,7 @@ pub(crate) fn catalog() -> Vec { let e = |id, label, params, defaults| EffectInfo { id, label, menu: MENU, params, defaults, raster: false, lengths: Lengths::default() }; vec![ e(DESATURATE, "Desaturate", "{} turns every colour the object paints to the grey of its lightness (each colour keeps its model)", json!({})), + e(DUOTONE, "Duotone…", super::duotone::PARAMS, super::duotone::defaults()), e( EXPOSURE, "Exposure…", @@ -49,6 +51,8 @@ pub(crate) enum Adjust { }, Desaturate, Invert, + /// Duotone ([`super::duotone`]). + Duotone(super::duotone::Duotone), } impl Adjust { @@ -61,6 +65,7 @@ impl Adjust { }, DESATURATE => Adjust::Desaturate, INVERT => Adjust::Invert, + DUOTONE => Adjust::Duotone(super::duotone::Duotone::parse(p)?), _ => return None, }) } @@ -73,6 +78,7 @@ impl Adjust { [(r.max(g).max(b) + r.min(g).min(b)) / 2.0; 3] } Adjust::Invert => rgb.map(|v| 1.0 - v), + Adjust::Duotone(d) => d.apply_rgb(rgb), } } } diff --git a/crates/effects/src/addons/duotone.rs b/crates/effects/src/addons/duotone.rs new file mode 100644 index 000000000..c4febd865 --- /dev/null +++ b/crates/effects/src/addons/duotone.rs @@ -0,0 +1,178 @@ +//! Color Effects' Duotone (Effect › Color Effects › Duotone…), a colour adjustment: the +//! grey-equivalent tone of each colour an object paints is printed with one to four inks (a +//! monotone, duotone, tritone or quadtone). Each ink, a tone, is a colour, typically a spot swatch, +//! with its own curve from the tone's tint to the ink's. The inks overprint, so their tints +//! multiply. +//! +//! With one tone of a swatch, a colour becomes a tint of that swatch, linked to it, so a spot tone +//! separates onto its plate. With several tones it becomes the CMYK mix of their tints: a colour +//! links to one swatch at one tint, and a mix of several spot inks has no swatch to link to. + +use std::collections::HashMap; +use std::sync::Arc; + +use serde_json::{Value, json}; +use vectorcraft_doc::Document; +use vectorcraft_doc::color::{Color, cms::Model}; + +use crate::adjust::{LUT, curve_at, curve_points_on, lookup, sample}; +use crate::raster::color_param; +use crate::util::flag; + +/// Duotone (Effect › Color Effects › Duotone…). +pub const DUOTONE: &str = "addon.color.duotone"; + +/// Most tones Duotone reads (a quadtone): any past them are left out. +pub const MAX_TONES: usize = 4; + +/// Most points a tone's curve reads: any past them are left out. +pub const MAX_CURVE_POINTS: usize = 64; + +/// The parameters' description (`effect.apply`). +pub(crate) const PARAMS: &str = "{tones: [{color: \"#rrggbb\" or a colour object, swatch?: a global or spot swatch's name (with the swatch's colour as `color`), curve?: \"tint,ink tint,ink …\" or [[tint, ink], …] (0..100, at most 64 points; \"0,0 100,100\"), active?: true}] (up to four inks, overprinting; any past four are left out), skipSpot?: false (colours linked to a spot swatch keep their colour)} prints the grey-equivalent tone of each colour the object paints with the inks, each at the tint its curve gives for the tone's: with one tone of a swatch, as a tint of that swatch (a spot tone prints on its own plate); with several, as the CMYK mix of their tints"; + +/// The dialog's defaults: one black tone, its curve linear. +pub(crate) fn defaults() -> Value { + json!({"tones": [{"color": "#000000", "curve": "0,0 100,100", "active": true}], "skipSpot": false}) +} + +/// Whether swatch `name` of the document is a spot colour: `Some(false)` for a global process +/// swatch, `None` when the document has no global swatch of that name. +pub type SwatchKinds<'a> = dyn Fn(&str) -> Option + 'a; + +/// [`SwatchKinds`] of `d`'s swatch `name`. +pub fn swatch_kind(d: &Document, name: &str) -> Option { + d.swatches_iter().find(|w| w.global && w.name == name).map(|w| w.spot) +} + +/// [`swatch_kind`] of each of `d`'s global swatches. +pub fn swatch_kinds(d: &Document) -> HashMap { + d.swatches_iter().filter(|w| w.global).map(|w| (w.name.clone(), w.spot)).collect() +} + +/// A tone's curve points (0..1 each) from its `curve` (0..100). +pub fn tone_curve_points(v: Option<&Value>) -> Vec<(f32, f32)> { + curve_points_on(v, 100.0, MAX_CURVE_POINTS) +} + +/// One ink. +#[derive(Clone, Debug, PartialEq)] +struct Tone { + color: Color, + /// `color` as CMYK inks, which several tones mix in. + cmyk: [f32; 4], + /// The swatch the colour is of, if any. + swatch: Option, + /// Its curve, sampled: the tone's tint → the ink's. + curve: Arc<[f32]>, +} + +/// Duotone's tones and options, its parameters read. +#[derive(Clone, Debug, PartialEq)] +pub struct Duotone { + tones: Vec, + skip_spot: bool, + /// The display colour at each sampled tint, for pixels. + rgb: Arc<[[f32; 3]]>, +} + +impl Duotone { + /// `None` without an active tone (the effect then leaves the colours as they are). + pub(crate) fn parse(p: &Value) -> Option { + let tones: Vec = p + .get("tones") + .and_then(Value::as_array) + .into_iter() + .flatten() + .take(MAX_TONES) + .filter(|t| t.is_object() && flag(t, "active", true)) + .map(|t| { + let color = finite(color_param(t, Color::BLACK)); + let cmyk = match color.in_model(Model::Cmyk) { + Color::Cmyk { c, m, y, k } => [c, m, y, k].map(|v| if v.is_finite() { v.clamp(0.0, 1.0) } else { 0.0 }), + _ => [0.0, 0.0, 0.0, 1.0], + }; + let pts = tone_curve_points(t.get("curve")); + let swatch = t.get("swatch").and_then(Value::as_str).filter(|s| !s.is_empty()).map(str::to_string); + Tone { color, cmyk, swatch, curve: sample(|x| curve_at(&pts, x)) } + }) + .collect(); + if tones.is_empty() { + return None; + } + let mut d = Duotone { tones, skip_spot: flag(p, "skipSpot", false), rgb: Arc::new([]) }; + d.rgb = (0..LUT).map(|i| d.mix(i as f32 / (LUT - 1) as f32).to_rgb()).collect(); + Some(d) + } + + /// The inks' colour for tone `t` (0 = white, 1 = black): one tone's colour at its tint, or + /// several tones' CMYK tints overprinted (each ink lets through what the others leave). + fn mix(&self, t: f32) -> Color { + if let [one] = self.tones.as_slice() { + return one.color.tinted(lookup(&one.curve, t)); + } + let mut paper = [1.0f32; 4]; + for tone in &self.tones { + let k = lookup(&tone.curve, t); + for (p, ink) in paper.iter_mut().zip(tone.cmyk) { + *p *= 1.0 - ink * k; + } + } + let [c, m, y, k] = paper.map(|p| 1.0 - p); + Color::cmyk(c, m, y, k) + } + + /// Display components 0..1 through the effect. + pub(crate) fn apply_rgb(&self, rgb: [f32; 3]) -> [f32; 3] { + let x = tone(rgb) * (LUT - 1) as f32; + let i = (x.floor() as usize).min(LUT - 2); + let f = x - i as f32; + match (self.rgb.get(i), self.rgb.get(i + 1)) { + (Some(a), Some(b)) => std::array::from_fn(|k| a[k] + (b[k] - a[k]) * f), + _ => rgb, + } + } + + /// A colour with its swatch link and tint through the effect ([`crate::ColorMap::apply_link`]): + /// true when it changed. With `skipSpot`, a colour linked to a spot swatch stays. + pub(crate) fn map_link(&self, c: &mut Color, link: &mut Option, tint: &mut f32, swatches: &SwatchKinds) -> bool { + if self.skip_spot && link.as_deref().and_then(swatches) == Some(true) { + return false; + } + let t = tone(c.to_rgb()); + let new = self.mix(t); + let (new_link, new_tint) = match self.tones.as_slice() { + [one] => match one.swatch.as_ref().filter(|n| swatches(n).is_some()) { + Some(name) => (Some(name.clone()), lookup(&one.curve, t)), + None => (None, 1.0), + }, + _ => (None, 1.0), + }; + if new == *c && new_link == *link && new_tint == *tint { + return false; + } + (*c, *link, *tint) = (new, new_link, new_tint); + true + } +} + +/// The grey-equivalent tone of display colour `rgb`: 0 for white, 1 for black (its luma, as +/// Convert to Grayscale reads it, so a grey's tone is its ink). +fn tone([r, g, b]: [f32; 3]) -> f32 { + let t = 1.0 - (0.299 * r + 0.587 * g + 0.114 * b); + if t.is_finite() { t.clamp(0.0, 1.0) } else { 0.0 } +} + +/// `c` with its components finite and in range. +fn finite(c: Color) -> Color { + let v = |x: f32, lo: f32, hi: f32| if x.is_finite() { x.clamp(lo, hi) } else { lo }; + match c { + Color::Rgb { r, g, b } => Color::rgb(v(r, 0.0, 1.0), v(g, 0.0, 1.0), v(b, 0.0, 1.0)), + Color::Cmyk { c, m, y, k } => Color::cmyk(v(c, 0.0, 1.0), v(m, 0.0, 1.0), v(y, 0.0, 1.0), v(k, 0.0, 1.0)), + Color::Gray { k } => Color::gray(v(k, 0.0, 1.0)), + Color::Lab { l, a, b } => Color::lab(v(l, 0.0, 100.0), v(a, -128.0, 127.0), v(b, -128.0, 127.0)), + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/effects/src/addons/duotone/tests.rs b/crates/effects/src/addons/duotone/tests.rs new file mode 100644 index 000000000..8a3255b05 --- /dev/null +++ b/crates/effects/src/addons/duotone/tests.rs @@ -0,0 +1,128 @@ +use serde_json::{Value, json}; +use vectorcraft_doc::Effect; +use vectorcraft_doc::color::Color; + +use super::*; +use crate::{ColorMap, color_map}; + +fn map(params: Value) -> ColorMap { + color_map(&[Effect { id: DUOTONE.into(), params, visible: true }]).unwrap() +} + +/// `c` with link `link` through `m`, the document having spot swatch "Ink" and process swatch +/// "Blue": (colour, link, tint, changed). +fn linked(m: &ColorMap, c: Color, link: Option<&str>) -> (Color, Option, f32, bool) { + let (mut c, mut link, mut tint) = (c, link.map(str::to_string), 1.0); + let kinds = |n: &str| match n { + "Ink" => Some(true), + "Blue" => Some(false), + _ => None, + }; + let changed = m.apply_link(&mut c, &mut link, &mut tint, &kinds); + (c, link, tint, changed) +} + +fn close(a: f32, b: f32) -> bool { + (a - b).abs() < 0.01 +} + +fn looks(c: Color, rgb: [f32; 3]) -> bool { + c.to_rgb().iter().zip(rgb).all(|(a, b)| close(*a, b)) +} + +const INK: Color = Color::Cmyk { c: 0.0, m: 0.5, y: 1.0, k: 0.0 }; + +fn ink_tone(curve: &str) -> Value { + json!({"tones": [{"color": INK, "swatch": "Ink", "curve": curve}]}) +} + +#[test] +fn a_spot_tone_turns_greys_into_tints_of_its_swatch() { + let m = map(ink_tone("0,0 100,100")); + for k in [0.0, 0.25, 0.6, 1.0] { + let (c, link, tint, _) = linked(&m, Color::gray(k), None); + assert_eq!(link.as_deref(), Some("Ink"), "linked at {k}"); + assert!(close(tint, k), "the tint is the grey's ink: {k} → {tint}"); + assert_eq!(c, INK.tinted(tint), "its colour is the swatch's at that tint"); + } + // A colour reads as its grey equivalent: white is no ink. + let (_, _, tint, _) = linked(&m, Color::rgb(1.0, 1.0, 1.0), None); + assert!(close(tint, 0.0)); + // A swatch the document hasn't: an unlinked tint of the tone's colour. + let (c, link, ..) = linked(&map(json!({"tones": [{"color": INK, "swatch": "Gone"}]})), Color::gray(0.5), None); + assert!(link.is_none() && looks(c, INK.tinted(0.5).to_rgb())); + // Without the links, colours (mesh points) take the tone's colour too. + assert!(looks(m.apply(Color::gray(0.5)), INK.tinted(0.5).to_rgb())); +} + +#[test] +fn the_curve_changes_the_mapping() { + // Inverse: dark greys get little ink. + let (_, _, tint, _) = linked(&map(ink_tone("0,100 100,0")), Color::gray(0.8), None); + assert!(close(tint, 0.2), "{tint}"); + // Through its points exactly, flat past its ends. + let lifted = map(ink_tone("0,0 50,80 100,100")); + assert!(close(linked(&lifted, Color::gray(0.5), None).2, 0.8)); + assert!(close(linked(&map(ink_tone("20,0 80,100")), Color::gray(0.1), None).2, 0.0)); + // Unreadable or missing curves are linear; points past the cap are left out. + for curve in [json!("junk"), json!(null), json!([[0, 0]])] { + let p = json!({"tones": [{"color": INK, "swatch": "Ink", "curve": curve}]}); + assert!(close(linked(&map(p), Color::gray(0.3), None).2, 0.3), "{curve}"); + } + let many: Vec<[f64; 2]> = (0..200).map(|i| [f64::from(i) / 2.0, 100.0 - f64::from(i) / 2.0]).collect(); + assert_eq!(tone_curve_points(Some(&json!(many))).len(), MAX_CURVE_POINTS); +} + +#[test] +fn several_tones_multiply_as_cmyk_inks() { + let cyan = json!({"color": {"model": "cmyk", "c": 1.0, "m": 0.0, "y": 0.0, "k": 0.0}, "swatch": "Ink"}); + let half_black = json!({"color": {"model": "cmyk", "c": 0.6, "m": 0.0, "y": 0.0, "k": 1.0}, "curve": "0,0 100,50"}); + let m = map(json!({"tones": [cyan, half_black]})); + let (c, link, tint, changed) = linked(&m, Color::gray(1.0), Some("Ink")); + assert!(changed && link.is_none() && tint == 1.0, "a process mix, unlinked"); + // Cyan 100% over the second ink at 50% (60% cyan, 100% black at half): cyan 1-(1-1)(1-0.3). + let Color::Cmyk { c, m: mg, y, k } = c else { panic!("CMYK: {c:?}") }; + assert!(close(c, 1.0) && close(mg, 0.0) && close(y, 0.0) && close(k, 0.5), "{c} {mg} {y} {k}"); + let (mid, ..) = linked(&m, Color::gray(0.5), None); + let Color::Cmyk { c, k, .. } = mid else { panic!("CMYK") }; + // Cyan 50% over the second ink at 25% (15% cyan, 25% black): 1 - 0.5 * 0.85 = 0.575. + assert!(close(c, 0.575) && close(k, 0.25), "{c} {k}"); + // Inactive tones and tones past the fourth don't count; none active leaves the colours. + let one = json!({"color": "#000000"}); + let off = json!({"color": "#ff0000", "active": false}); + let only_black = map(json!({"tones": [one, off, off, off, {"color": "#ff0000"}]})); + assert!(looks(only_black.apply(Color::gray(0.4)), [0.6; 3])); + assert!(color_map(&[Effect { id: DUOTONE.into(), params: json!({"tones": [off]}), visible: true }]).is_none()); + assert!(color_map(&[Effect { id: DUOTONE.into(), params: json!({"tones": "x"}), visible: true }]).is_none()); +} + +#[test] +fn skip_spot_keeps_spot_colours() { + let mut p = json!({"tones": [{"color": "#000000"}], "skipSpot": true}); + let m = map(p.clone()); + let spot = Color::cmyk(0.0, 1.0, 0.0, 0.0); + assert_eq!(linked(&m, spot, Some("Ink")), (spot, Some("Ink".to_string()), 1.0, false)); + // A process swatch's colour, and an unlinked one, still change. + assert!(linked(&m, spot, Some("Blue")).3); + assert!(linked(&m, spot, None).3); + p["skipSpot"] = json!(false); + assert!(linked(&map(p), spot, Some("Ink")).3); +} + +#[test] +fn pixels_and_other_adjustments_compose() { + let m = map(json!({"tones": [{"color": "#ff0000"}]})); + // A grey pixel half way: half way to red from white. + let px = m.apply_rgb([0.5, 0.5, 0.5]); + assert!(close(px[0], 1.0) && close(px[1], 0.5) && close(px[2], 0.5), "{px:?}"); + // Invert, then Duotone: white becomes full red. + let both = color_map(&[ + Effect { id: crate::addons::color::INVERT.into(), params: json!({}), visible: true }, + Effect { id: DUOTONE.into(), params: json!({"tones": [{"color": "#ff0000"}]}), visible: true }, + ]) + .unwrap(); + assert!(looks(both.apply(Color::WHITE), [1.0, 0.0, 0.0])); + // Out of range colours are brought into range. + let wild = map(json!({"tones": [{"color": [1e30, -5, 0.5]}]})); + assert!(looks(wild.apply(Color::gray(1.0)), [1.0, 0.0, 0.5])); +} diff --git a/crates/effects/src/adjust.rs b/crates/effects/src/adjust.rs index 7350958cc..93601680f 100644 --- a/crates/effects/src/adjust.rs +++ b/crates/effects/src/adjust.rs @@ -14,9 +14,10 @@ use std::sync::Arc; use serde_json::Value; use vectorcraft_doc::color::{Color, Paint, keep_model}; -use vectorcraft_doc::recolor::{ColorVisitor, Reach, map_paint, recolor_node}; +use vectorcraft_doc::recolor::{ColorVisitor, Reach, map_links, recolor_node}; use vectorcraft_doc::{Document, Effect, Node, NodeKind}; +use crate::addons::duotone::{Duotone, SwatchKinds}; use crate::merged_params; use crate::raster::color_param; use crate::util::{flag, num, text}; @@ -72,7 +73,7 @@ pub fn is_channel(s: &str) -> bool { } /// Entries of a sampled tone curve. -const LUT: usize = 256; +pub(crate) const LUT: usize = 256; /// One adjustment, its parameters normalised. #[derive(Clone, Debug, PartialEq)] @@ -92,6 +93,16 @@ enum Adjust { Addon(crate::addons::color::Adjust), } +impl Adjust { + /// Duotone, which maps a colour with its swatch link ([`ColorMap::apply_link`]). + fn duotone(&self) -> Option<&Duotone> { + match self { + Adjust::Addon(crate::addons::color::Adjust::Duotone(d)) => Some(d), + _ => None, + } + } +} + /// The colour adjustments of an effect list, composed in order. #[derive(Clone, Debug, PartialEq)] pub struct ColorMap(Vec); @@ -104,17 +115,44 @@ impl ColorMap { /// Adjust display (sRGB) components 0..1. pub fn apply_rgb(&self, rgb: [f32; 3]) -> [f32; 3] { - self.0.iter().fold(rgb, |c, a| a.apply(c).map(|v| if v.is_finite() { v.clamp(0.0, 1.0) } else { 0.0 })) + apply_rgb(&self.0, rgb) } /// Adjust a colour, keeping its colour model (unchanged when the adjustments leave it as is). + /// Duotone gives its inks' colour instead ([`Duotone`]). pub fn apply(&self, c: Color) -> Color { - let rgb = c.to_rgb(); - let out = self.apply_rgb(rgb); - if out.iter().zip(rgb).all(|(a, b)| (a - b).abs() < 1e-6) { - return c; + let (mut c, mut link, mut tint) = (c, None, 1.0); + self.apply_link(&mut c, &mut link, &mut tint, &|_| None); + c + } + + /// Adjust a colour with its swatch link and tint (`link`, `tint`), as a paint holds them; + /// true when it changed. A colour that changes loses its link, except that Duotone with one + /// tone of a swatch the document has (`swatches`, [`SwatchKinds`]) links it to that swatch at + /// the tone's tint. The adjustments between Duotones work in display RGB, each run keeping + /// the colour's model. + pub fn apply_link(&self, c: &mut Color, link: &mut Option, tint: &mut f32, swatches: &SwatchKinds) -> bool { + let mut changed = false; + let mut rest: &[Adjust] = &self.0; + while !rest.is_empty() { + let (run, after) = rest.split_at(rest.iter().position(|a| a.duotone().is_some()).unwrap_or(rest.len())); + if !run.is_empty() { + let rgb = c.to_rgb(); + let out = apply_rgb(run, rgb); + if !out.iter().zip(rgb).all(|(a, b)| (a - b).abs() < 1e-6) { + *c = keep_model(*c, Color::rgb(out[0], out[1], out[2])); + *link = None; + *tint = 1.0; + changed = true; + } + } + let Some((duotone, tail)) = after.split_first() else { break }; + if let Some(d) = duotone.duotone() { + changed |= d.map_link(c, link, tint, swatches); + } + rest = tail; } - keep_model(c, Color::rgb(out[0], out[1], out[2])) + changed } /// Adjust an 8-bit sRGB pixel. @@ -124,6 +162,11 @@ impl ColorMap { } } +/// Display (sRGB) components 0..1 through `adjustments`, in order. +fn apply_rgb(adjustments: &[Adjust], rgb: [f32; 3]) -> [f32; 3] { + adjustments.iter().fold(rgb, |c, a| a.apply(c).map(|v| if v.is_finite() { v.clamp(0.0, 1.0) } else { 0.0 })) +} + /// The visible colour adjustments of `effects` as one map, `None` when there are none. pub fn color_map(effects: &[Effect]) -> Option { let v: Vec = @@ -233,12 +276,12 @@ impl Adjust { } /// `f` sampled at [`LUT`] inputs 0..1. -fn sample(f: impl Fn(f32) -> f32) -> Arc<[f32]> { +pub(crate) fn sample(f: impl Fn(f32) -> f32) -> Arc<[f32]> { (0..LUT).map(|i| f(i as f32 / (LUT - 1) as f32).clamp(0.0, 1.0)).collect() } /// The tone curve `lut` at `v` (linear between samples). -fn lookup(lut: &[f32], v: f32) -> f32 { +pub(crate) fn lookup(lut: &[f32], v: f32) -> f32 { let x = v.clamp(0.0, 1.0) * (LUT - 1) as f32; let i = (x.floor() as usize).min(LUT - 2); let (a, b) = (lut.get(i).copied().unwrap_or(v), lut.get(i + 1).copied().unwrap_or(v)); @@ -248,6 +291,12 @@ fn lookup(lut: &[f32], v: f32) -> f32 { /// Curves' points (0..1 each, sorted by input, one per input) from its `points` parameter: /// `"x,y x,y …"` or `[[x, y], …]` in 0..255. Missing or unreadable points give the identity line. pub fn curve_points(v: Option<&Value>) -> Vec<(f32, f32)> { + // At most a point per level is meaningful. + curve_points_on(v, 255.0, 256) +} + +/// [`curve_points`] on a scale of 0..`max`, reading at most `cap` points. +pub fn curve_points_on(v: Option<&Value>, max: f64, cap: usize) -> Vec<(f32, f32)> { let mut pts: Vec<(f64, f64)> = match v { Some(Value::String(s)) => s .split(|c: char| c.is_whitespace() || c == ';') @@ -260,10 +309,9 @@ pub fn curve_points(v: Option<&Value>) -> Vec<(f32, f32)> { _ => vec![], }; pts.retain(|(x, y)| x.is_finite() && y.is_finite()); - // At most a point per level is meaningful. - pts.truncate(256); - let mut pts: Vec<(f32, f32)> = - pts.into_iter().map(|(x, y)| ((x.clamp(0.0, 255.0) / 255.0) as f32, (y.clamp(0.0, 255.0) / 255.0) as f32)).collect(); + pts.truncate(cap); + let unit = |v: f64| (v.clamp(0.0, max) / max) as f32; + let mut pts: Vec<(f32, f32)> = pts.into_iter().map(|(x, y)| (unit(x), unit(y))).collect(); pts.sort_by(|a, b| a.0.total_cmp(&b.0)); pts.dedup_by(|a, b| (a.0 - b.0).abs() < 1e-6); if pts.len() < 2 { @@ -357,15 +405,22 @@ fn from_hsl([h, s, l]: [f32; 3]) -> [f32; 3] { /// Gives the key of an embedded image recoloured by a map (`None`: leave it as it is). pub type ImageHook<'a> = dyn FnMut(&str, &ColorMap) -> Option + 'a; -/// The live visitor: every colour through `map`, embedded images through the hook. +/// The live visitor: every colour through `map` (paints with their swatch links, the document's +/// swatches being `swatches`), embedded images through the hook. struct Live<'a, 'b> { map: &'a ColorMap, + swatches: &'a SwatchKinds<'b>, images: &'a mut ImageHook<'b>, } +/// `p`'s colours through `map` with their swatch links ([`ColorMap::apply_link`]). +fn map_paint(p: &mut Paint, map: &ColorMap, swatches: &SwatchKinds) -> bool { + map_links(p, &|c, link, tint| map.apply_link(c, link, tint, swatches)) +} + impl ColorVisitor for Live<'_, '_> { fn paint(&mut self, p: &mut Paint) -> bool { - map_paint(p, &|c| self.map.apply(c)) + map_paint(p, self.map, self.swatches) } fn color(&mut self, c: Color) -> Color { self.map.apply(c) @@ -378,12 +433,13 @@ impl ColorVisitor for Live<'_, '_> { /// `n` with the colour adjustments in it applied, `None` when it has none: its members' first, /// then those of its fills and strokes on their paints, then its own followed by `outer` (the /// adjustments of an object `n` is the art of) on everything it paints. The adjustment effects -/// are gone from the result; embedded images take the keys `images` gives. -pub fn adjust(n: &Node, outer: Option<&ColorMap>, images: &mut ImageHook<'_>) -> Option { +/// are gone from the result; embedded images take the keys `images` gives. `swatches` tells the +/// document's global and spot swatches apart ([`crate::addons::duotone::swatch_kind`]). +pub fn adjust(n: &Node, outer: Option<&ColorMap>, swatches: &SwatchKinds, images: &mut ImageHook<'_>) -> Option { let mut m: Option = None; if let Some(ch) = n.children() { for (i, c) in ch.iter().enumerate() { - if let Some(new) = adjust(c, None, images) + if let Some(new) = adjust(c, None, swatches, images) && let Some(slot) = m.get_or_insert_with(|| n.clone()).children_mut().and_then(|ch| ch.get_mut(i)) { *slot = Arc::new(new); @@ -394,7 +450,7 @@ pub fn adjust(n: &Node, outer: Option<&ColorMap>, images: &mut ImageHook<'_>) -> if let Some(map) = color_map(item.effects()) && let Some(it) = m.get_or_insert_with(|| n.clone()).appearance.items.get_mut(i) { - map_paint(it.paint_mut(), &|c| map.apply(c)); + map_paint(it.paint_mut(), &map, swatches); it.effects_mut().retain(|e| !is_adjustment(&e.id)); } } @@ -406,7 +462,7 @@ pub fn adjust(n: &Node, outer: Option<&ColorMap>, images: &mut ImageHook<'_>) -> if let Some(map) = map { let mm = m.get_or_insert_with(|| n.clone()); mm.appearance.effects.retain(|e| !is_adjustment(&e.id)); - recolor_node(mm, Reach::ALL, &mut Live { map: &map, images }); + recolor_node(mm, Reach::ALL, &mut Live { map: &map, swatches, images }); } m } @@ -423,13 +479,14 @@ pub fn adjust_in_document(d: &mut Document, n: &Node) -> Option { } _ => n, }; + let kinds = crate::addons::duotone::swatch_kinds(d); let mut hook = |key: &str, map: &ColorMap| -> Option { let blob = d.images.get(key)?.map_rgb(|rgb| map.apply_rgb8(rgb))?; let k = blob.content_key(); d.images.entry(k.clone()).or_insert(blob); Some(k) }; - adjust(src, None, &mut hook) + adjust(src, None, &|name| kinds.get(name).copied(), &mut hook) } #[cfg(test)] @@ -492,17 +549,17 @@ mod tests { let mut g = Node::group(NodeId(1), vec![leaf(2), leaf(3)]); g.appearance.effects.push(Effect { id: "adjust.hueSaturation".into(), params: json!({"hue": 120}), visible: true }); assert!(has_adjustment(&g)); - let out = adjust(&g, None, &mut |_, _| None).unwrap(); + let out = adjust(&g, None, &|_| None, &mut |_, _| None).unwrap(); assert!(out.appearance.effects.is_empty()); for c in out.children().unwrap() { assert_eq!(c.appearance.fill_paint().color().unwrap().to_hex(), "#00ff00"); } // A hidden effect does nothing; a stroke's own effect recolours just that stroke. g.appearance.effects[0].visible = false; - assert!(!has_adjustment(&g) && adjust(&g, None, &mut |_, _| None).is_none()); + assert!(!has_adjustment(&g) && adjust(&g, None, &|_| None, &mut |_, _| None).is_none()); let mut p = Node::path(NodeId(5), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art()); p.appearance.items[1].effects_mut().push(Effect { id: "adjust.levels".into(), params: json!({"outputBlack": 255}), visible: true }); - let out = adjust(&p, None, &mut |_, _| None).unwrap(); + let out = adjust(&p, None, &|_| None, &mut |_, _| None).unwrap(); assert_eq!(out.appearance.items[1].paint().color().unwrap().to_hex(), "#ffffff"); assert_eq!(out.appearance.items[0].paint(), p.appearance.items[0].paint()); assert!(out.appearance.items[1].effects().is_empty()); @@ -517,7 +574,7 @@ mod tests { let fill = Appearance::basic(Paint::Gradient(Box::new(gp)), Paint::None, 0.0); let mut p = Node::path(NodeId(1), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), fill); p.appearance.effects.push(Effect { id: "adjust.hueSaturation".into(), params: json!({"hue": 120}), visible: true }); - let out = adjust(&p, None, &mut |_, _| None).unwrap(); + let out = adjust(&p, None, &|_| None, &mut |_, _| None).unwrap(); let Paint::Gradient(g) = out.appearance.fill_paint() else { panic!("a gradient fill") }; // The points are what a freeform gradient draws from, not its stops. let hexes: Vec = g.freeform.as_ref().unwrap().points.iter().map(|p| p.color.to_hex()).collect(); diff --git a/crates/engine/src/cmd/addons/color.rs b/crates/engine/src/cmd/addons/color.rs index 3ddbd8c44..4426624cc 100644 --- a/crates/engine/src/cmd/addons/color.rs +++ b/crates/engine/src/cmd/addons/color.rs @@ -1,9 +1,11 @@ -//! Color Effects (`addon.color.*`), a built-in add-on: its live effects (Exposure, Desaturate and -//! Invert Colors, in Effect › Color Effects) are the effects crate's; here, Apply as Filter writes -//! any colour adjustment into the colours themselves, and Select by Color Kind selects the objects -//! that use spot colours, carry a live colour adjustment, or are images of a colour model. Its panel -//! and its submenu, Object › Plug-ins › Color Effects, are the UI's. +//! Color Effects (`addon.color.*`), a built-in add-on: its live effects (Exposure, Desaturate, +//! Duotone and Invert Colors, in Effect › Color Effects) are the effects crate's; here, Apply as +//! Filter writes any colour adjustment into the colours themselves, Select by Color Kind selects the +//! objects that use spot colours, carry a live colour adjustment, or are images of a colour model, +//! and Print Ink Fixes ([`print_fixes`]) prepares a CMYK document's inks for print. Its panel and +//! its submenu, Object › Plug-ins › Color Effects, are the UI's. +mod print_fixes; #[cfg(test)] mod tests; @@ -11,7 +13,7 @@ use serde_json::{Value, json}; use vectorcraft_doc::{Document, Node, NodeId, NodeKind}; use vectorcraft_render::effects; -use super::super::colorcmds::recolor; +use super::super::colorcmds::recolor_links; use super::super::select::walk_selectable; use super::super::{CommandSpec, bad, checked_ids_or_selection, cmd, has_doc, has_selection, text_param}; use crate::{Result, Session}; @@ -35,10 +37,19 @@ pub fn specs() -> Vec { "Apply as Filter", ["Object", "Plug-ins", "Color Effects"], None, - "{effect: a colour adjustment's effect id (adjust.brightnessContrast, adjust.curves, adjust.hueSaturation, adjust.levels, adjust.shiftToColor, adjust.temperatureTint, addon.color.exposure, addon.color.desaturate or addon.color.invert), params?: {…} (as effect.apply; missing keys take the dialog defaults), fill?: true, stroke?: true, includeImages?: true, includePatterns?: true, ids?: [id…] (default: the selection)} recolour the objects and everything inside them as the live effect would draw them, but in their colours, as edit.colors.invert does (fills, strokes, type, gradient meshes, embedded images and pattern tiles), as one undo step; their effects stay → {changed}", + "{effect: a colour adjustment's effect id (adjust.brightnessContrast, adjust.curves, adjust.hueSaturation, adjust.levels, adjust.shiftToColor, adjust.temperatureTint, addon.color.exposure, addon.color.desaturate, addon.color.duotone or addon.color.invert), params?: {…} (as effect.apply; missing keys take the dialog defaults), fill?: true, stroke?: true, includeImages?: true, includePatterns?: true, ids?: [id…] (default: the selection)} recolour the objects and everything inside them as the live effect would draw them, but in their colours, as edit.colors.invert does (fills, strokes, type, gradient meshes, embedded images and pattern tiles), as one undo step; their effects stay. Duotone links a colour to its one tone's swatch as the live effect does → {changed}", has_selection, filter ), + cmd!( + "addon.color.printFixes", + "Print Ink Fixes", + ["Object", "Plug-ins", "Color Effects"], + None, + "{scope?: \"selection\" (the selection, or `ids`; by default, the document while nothing is selected) | \"document\" (every layer), inkLimit?: total ink % 1..400 (0 or left out: off), excludePureBlack?: true (black-only and grey colours keep their ink), removeWhiteOverprint?: false (white fills and strokes stop overprinting), overprintBlack?: black % 1..100 (black-only and grey fills and strokes this dark or darker overprint, as edit.colors.overprintBlack; 0 or left out: off), textOnly?: false (Overprint Black on type only), fill?: true, stroke?: true, ids?: [id…]} prepare a CMYK document's inks for print as one undo step: CMYK colours (solid, gradient stops, gradient meshes) with more total ink than inkLimit are scaled down to it (colours of spot swatches and Registration stay), white overprint is removed and black overprint set. Refused in RGB documents (File › Document Color Mode › CMYK Color first) → {changed, inkLimited: paints and meshes scaled, whiteOverprintRemoved: objects, blackOverprinted: objects}", + has_doc, + print_fixes::run + ), ] } @@ -116,5 +127,7 @@ fn filter(s: &mut Session, p: &Value) -> Result { o.insert("ids".into(), json!(ids.iter().map(|i| i.0).collect::>())); } let label = effects::effect_info(id).map_or("Apply as Filter", |e| e.label.trim_end_matches('…')); - recolor(s, &given, label, &|c| map.apply(c)) + let kinds = effects::addons::duotone::swatch_kinds(&s.doc()?.doc); + let swatches = |name: &str| kinds.get(name).copied(); + recolor_links(s, &given, label, &|c| map.apply(c), &|c, link, tint| map.apply_link(c, link, tint, &swatches)) } diff --git a/crates/engine/src/cmd/addons/color/print_fixes.rs b/crates/engine/src/cmd/addons/color/print_fixes.rs new file mode 100644 index 000000000..7a4cfee9c --- /dev/null +++ b/crates/engine/src/cmd/addons/color/print_fixes.rs @@ -0,0 +1,170 @@ +//! Print Ink Fixes (`addon.color.printFixes`, Object › Plug-ins › Color Effects › Print Ink +//! Fixes…): a CMYK document's inks made ready for print in one undo step. Colours with more total +//! ink than the limit are scaled down to it, white fills and strokes stop overprinting, and black +//! ones start to, as Edit › Edit Colors › Overprint Black sets it. + +use std::sync::Arc; + +use serde_json::{Value, json}; +use vectorcraft_color::Color; +use vectorcraft_color::swatch::REGISTRATION; +use vectorcraft_doc::overprint::{OverprintBlack, is_white}; +use vectorcraft_doc::{ColorMode, Node, NodeKind}; + +use super::super::super::colorcmds::{Recolor, Scope}; +use super::super::super::overprint::edit_counted; +use super::super::super::{bad, checked_ids_or_selection, number_or, switch_or, text_param}; +use crate::{Result, Session}; + +const C: &str = "addon.color.printFixes"; + +/// What `p` asks for. +struct Fixes { + /// Most total ink, 0..4 (0: off). + ink_limit: f32, + exclude_pure_black: bool, + remove_white_overprint: bool, + /// Overprint Black's least black, 0..1 (0: off). + overprint_black: f32, + text_only: bool, + fill: bool, + stroke: bool, +} + +/// The percentage `p` gives for `key` in 0..=`max`, as a fraction (0 when left out). +fn percent(p: &Value, key: &str, max: f64) -> Result { + let v = number_or(p, key, 0.0, C)?; + if !(0.0..=max).contains(&v) { + return Err(bad(C, format!("`{key}` must be 0..{max}"))); + } + Ok((v / 100.0) as f32) +} + +impl Fixes { + fn of(p: &Value) -> Result { + let f = Fixes { + ink_limit: percent(p, "inkLimit", 400.0)?, + exclude_pure_black: switch_or(p, "excludePureBlack", true, C)?, + remove_white_overprint: switch_or(p, "removeWhiteOverprint", false, C)?, + overprint_black: percent(p, "overprintBlack", 100.0)?, + text_only: switch_or(p, "textOnly", false, C)?, + fill: switch_or(p, "fill", true, C)?, + stroke: switch_or(p, "stroke", true, C)?, + }; + if f.ink_limit <= 0.0 && f.overprint_black <= 0.0 && !f.remove_white_overprint { + return Err(bad(C, "give `inkLimit`, `removeWhiteOverprint` or `overprintBlack`")); + } + Ok(f) + } + + /// `c` with its total ink scaled down to the limit: CMYK, and grey (black ink) unless + /// pure blacks are excluded, which CMYK with black ink only is too. + fn limit(&self, c: Color) -> Color { + const E: f32 = 0.005; + let max = self.ink_limit; + match c { + Color::Cmyk { c: cy, m, y, k } => { + let total = cy + m + y + k; + let pure_black = cy <= E && m <= E && y <= E; + if total <= max || (self.exclude_pure_black && pure_black) || !total.is_finite() { + return c; + } + let s = max / total; + Color::cmyk(cy * s, m * s, y * s, k * s) + } + Color::Gray { k } if k > max && !self.exclude_pure_black => Color::gray(max), + _ => c, + } + } + + /// Remove white overprint from `n`'s own fills and strokes and its characters': true when + /// that changed anything. + fn unset_white(&self, n: &mut Node) -> bool { + let mut changed = false; + let mut off = |aimed: bool, white: bool, flag: &mut bool| { + if aimed && white && *flag { + *flag = false; + changed = true; + } + }; + for it in &mut n.appearance.items { + let aimed = if it.is_fill() { self.fill } else { self.stroke }; + let white = is_white(it.paint()); + off(aimed, white, it.overprint_mut()); + } + if let NodeKind::Text(t) = &mut n.kind { + for r in &mut t.runs { + off(self.fill, is_white(&r.style.fill), &mut r.style.overprint_fill); + off(self.stroke, is_white(&r.style.stroke), &mut r.style.overprint_stroke); + } + } + changed + } + + /// The overprint fixes on `n` and everything inside it: (objects whose white overprint was + /// removed, objects set to overprint black). + fn overprint(&self, n: &mut Node, black: &OverprintBlack, spots: &[String]) -> (usize, usize) { + let white = usize::from(self.remove_white_overprint && self.unset_white(n)); + let blacks = self.overprint_black > 0.0 && (!self.text_only || matches!(n.kind, NodeKind::Text(_))); + let mut counts = (white, usize::from(blacks && black.apply(n, spots))); + for c in n.children_mut().into_iter().flatten() { + let (w, b) = self.overprint(Arc::make_mut(c), black, spots); + counts = (counts.0 + w, counts.1 + b); + } + counts + } +} + +pub(super) fn run(s: &mut Session, p: &Value) -> Result { + let fixes = Fixes::of(p)?; + let st = s.doc()?; + if st.doc.color_mode != ColorMode::Cmyk { + return Err(bad(C, "Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first")); + } + let layers = st.doc.layers.iter().map(|l| l.id).collect(); + let roots = checked_ids_or_selection(s, p, C)?; + // As Artwork Cleanup's: the selection, or the document while nothing is selected. + let ids = match text_param(p, "scope", C)? { + Some("document") => layers, + None if roots.is_empty() => layers, + Some("selection") if roots.is_empty() => return Err(bad(C, "nothing selected")), + Some("selection") | None => roots, + Some(other) => return Err(bad(C, format!("`scope` must be \"selection\" or \"document\", not {other:?}"))), + }; + let black = OverprintBlack { min_k: fixes.overprint_black, fill: fixes.fill, stroke: fixes.stroke, ..OverprintBlack::default() }; + let (mut ink, mut white, mut blacks) = (0, 0, 0); + let changed = edit_counted(s, "Print Ink Fixes", |d| { + if fixes.ink_limit > 0.0 { + let spots = d.spot_names(); + // Spot inks and Registration aren't process ink: they keep their colour and link. + let links = |c: &mut Color, link: &mut Option, _: &mut f32| { + if link.as_ref().is_some_and(|n| n == REGISTRATION || spots.contains(n)) { + return false; + } + let new = fixes.limit(*c); + if new == *c { + return false; + } + (*c, *link) = (new, None); + true + }; + let scope = Scope { fill: fixes.fill, stroke: fixes.stroke, images: false, patterns: false }; + ink = Recolor::with_links(scope, &|c| fixes.limit(c), &links).run(d, &ids); + } + if !fixes.remove_white_overprint && fixes.overprint_black <= 0.0 { + return Ok(ink); + } + let spots = d.spot_names(); + for id in &ids { + if let Some(n) = d.node_mut(*id) { + let (w, b) = fixes.overprint(n, &black, &spots); + (white, blacks) = (white + w, blacks + b); + } + } + Ok(ink + white + blacks) + })?; + Ok(json!({ "changed": changed, "inkLimited": ink, "whiteOverprintRemoved": white, "blackOverprinted": blacks })) +} + +#[cfg(test)] +mod tests; diff --git a/crates/engine/src/cmd/addons/color/print_fixes/tests.rs b/crates/engine/src/cmd/addons/color/print_fixes/tests.rs new file mode 100644 index 000000000..e4c191587 --- /dev/null +++ b/crates/engine/src/cmd/addons/color/print_fixes/tests.rs @@ -0,0 +1,129 @@ +//! Print Ink Fixes. + +use serde_json::{Value, json}; +use vectorcraft_color::Color; +use vectorcraft_doc::NodeId; + +use crate::Session; + +const C: &str = "addon.color.printFixes"; + +/// A CMYK document with a rectangle per fill, each with overprint as given; nothing selected. +fn doc(fills: &[(Value, bool)]) -> (Session, Vec) { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 200, "colorMode": "cmyk"})).unwrap(); + let ids = fills + .iter() + .enumerate() + .map(|(i, (fill, overprint))| { + let id = s.execute("shape.rectangle", &json!({"x": 10 + 30 * i, "y": 10, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap(); + s.execute("paint.setFill", &json!({ "color": fill })).unwrap(); + s.execute("paint.setStroke", &json!({"none": true})).unwrap(); + s.execute("object.setOverprint", &json!({ "fill": overprint })).unwrap(); + NodeId(id) + }) + .collect(); + s.execute("select.none", &json!({})).unwrap(); + (s, ids) +} + +fn fill(s: &Session, id: NodeId) -> (Color, bool) { + let n = s.doc().unwrap().doc.node(id).unwrap().clone(); + let item = n.appearance.items.iter().find(|i| i.is_fill()).unwrap().clone(); + (item.paint().color().unwrap(), item.overprint()) +} + +fn cmyk(c: f64, m: f64, y: f64, k: f64) -> Value { + json!({"c": c, "m": m, "y": y, "k": k}) +} + +fn close(a: Color, b: [f32; 4]) -> bool { + let Color::Cmyk { c, m, y, k } = a else { return false }; + [c, m, y, k].iter().zip(b).all(|(x, y)| (x - y).abs() < 0.001) +} + +#[test] +fn the_ink_limit_scales_heavy_colours_down_to_it() { + let (mut s, ids) = doc(&[(cmyk(100.0, 100.0, 100.0, 100.0), false), (cmyk(40.0, 30.0, 20.0, 10.0), false), (cmyk(0.0, 0.0, 0.0, 80.0), false)]); + let undo = s.doc().unwrap().history.undo.len(); + // Nothing selected: the whole document. + let v = s.execute(C, &json!({"inkLimit": 300})).unwrap(); + assert_eq!((v["inkLimited"].clone(), v["changed"].clone()), (json!(1), json!(1))); + assert!(close(fill(&s, ids[0]).0, [0.75; 4]), "{:?}", fill(&s, ids[0])); + assert!(close(fill(&s, ids[1]).0, [0.4, 0.3, 0.2, 0.1]), "under the limit: as it was"); + assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step"); + // The selection only. + s.execute("select.set", &json!({"ids": [ids[1].0]})).unwrap(); + s.execute(C, &json!({"inkLimit": 50})).unwrap(); + assert!(close(fill(&s, ids[1]).0, [0.2, 0.15, 0.1, 0.05])); + assert!(close(fill(&s, ids[0]).0, [0.75; 4]), "left out"); + // Pure black keeps its ink unless it is included. + s.execute(C, &json!({"inkLimit": 50, "scope": "document"})).unwrap(); + assert!(close(fill(&s, ids[0]).0, [0.125; 4])); + assert!(close(fill(&s, ids[2]).0, [0.0, 0.0, 0.0, 0.8])); + s.execute(C, &json!({"inkLimit": 50, "scope": "document", "excludePureBlack": false})).unwrap(); + assert!(close(fill(&s, ids[2]).0, [0.0, 0.0, 0.0, 0.5])); +} + +#[test] +fn spot_and_registration_colours_keep_their_ink() { + let (mut s, ids) = doc(&[(cmyk(0.0, 0.0, 0.0, 0.0), false), (cmyk(0.0, 0.0, 0.0, 0.0), false)]); + s.execute("swatch.new", &json!({"name": "Heavy", "color": cmyk(100.0, 100.0, 100.0, 50.0), "spot": true})).unwrap(); + s.execute("select.set", &json!({"ids": [ids[0].0]})).unwrap(); + s.execute("paint.setFill", &json!({"swatch": "Heavy"})).unwrap(); + s.execute("select.set", &json!({"ids": [ids[1].0]})).unwrap(); + s.execute("paint.setFill", &json!({"swatch": "[Registration]"})).unwrap(); + s.execute("select.none", &json!({})).unwrap(); + let before = [fill(&s, ids[0]), fill(&s, ids[1])]; + assert_eq!(s.execute(C, &json!({"inkLimit": 200})).unwrap()["changed"], 0); + assert_eq!([fill(&s, ids[0]), fill(&s, ids[1])], before); +} + +#[test] +fn white_overprint_goes_and_black_overprint_comes_at_the_threshold() { + let white = cmyk(0.0, 0.0, 0.0, 0.0); + let (mut s, ids) = + doc(&[(white.clone(), true), (cmyk(0.0, 0.0, 0.0, 95.0), false), (cmyk(0.0, 0.0, 0.0, 80.0), false), (cmyk(30.0, 0.0, 0.0, 100.0), false)]); + let undo = s.doc().unwrap().history.undo.len(); + let v = s.execute(C, &json!({"removeWhiteOverprint": true, "overprintBlack": 90})).unwrap(); + assert_eq!((v["whiteOverprintRemoved"].clone(), v["blackOverprinted"].clone()), (json!(1), json!(1)), "{v}"); + let overprints: Vec = ids.iter().map(|id| fill(&s, *id).1).collect(); + // White stops; 95 % black starts; 80 % is under the threshold; a rich black isn't pure black. + assert_eq!(overprints, [false, true, false, false]); + assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step"); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(ids.iter().map(|id| fill(&s, *id).1).collect::>(), [true, false, false, false]); + // On type only: the rectangles stay. + let v = s.execute(C, &json!({"overprintBlack": 90, "textOnly": true})).unwrap(); + assert_eq!(v["changed"], 0); + assert_eq!(s.doc().unwrap().history.undo.len(), undo, "nothing changed, no undo step"); + // Strokes only: the fills stay. + assert_eq!(s.execute(C, &json!({"overprintBlack": 90, "fill": false})).unwrap()["changed"], 0); +} + +#[test] +fn bad_requests_and_rgb_documents_are_refused() { + let (mut s, ids) = doc(&[(cmyk(100.0, 100.0, 100.0, 100.0), true)]); + for p in [ + json!({}), + json!({"inkLimit": 0}), + json!({"inkLimit": "300"}), + json!({"inkLimit": 401}), + json!({"inkLimit": -1}), + json!({"overprintBlack": 101}), + json!({"overprintBlack": true}), + json!({"removeWhiteOverprint": "yes"}), + json!({"inkLimit": 300, "fill": 1}), + json!({"inkLimit": 300, "scope": "page"}), + json!({"inkLimit": 300, "scope": "selection"}), + json!({"inkLimit": 300, "ids": [999_999]}), + ] { + assert!(s.execute(C, &p).is_err(), "{p}"); + } + assert!(close(fill(&s, ids[0]).0, [1.0; 4])); + let mut rgb = Session::new(); + rgb.execute("file.new", &json!({})).unwrap(); + rgb.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap(); + let e = rgb.execute(C, &json!({"inkLimit": 300})).unwrap_err().to_string(); + assert!(e.contains("CMYK"), "{e}"); +} diff --git a/crates/engine/src/cmd/addons/color/tests.rs b/crates/engine/src/cmd/addons/color/tests.rs index 5094c2019..ed8137d6a 100644 --- a/crates/engine/src/cmd/addons/color/tests.rs +++ b/crates/engine/src/cmd/addons/color/tests.rs @@ -238,3 +238,79 @@ fn a_halftone_past_the_caps_leaves_the_object_as_it_is() { let n = s.doc().unwrap().doc.node(id).cloned().unwrap(); assert!(n.children().is_none_or(|c| c.len() < 5), "no dots"); } + +/// Duotone, the live effect. +const DUOTONE: &str = "addon.color.duotone"; + +/// A document with spot swatch "Ink" and a selected grey (50 %) rectangle, unstroked. +fn grey_rect() -> (Session, NodeId) { + let (mut s, id) = red_rect(); + s.execute("swatch.new", &json!({"name": "Ink", "color": {"model": "cmyk", "c": 0.0, "m": 0.6, "y": 1.0, "k": 0.0}, "spot": true})).unwrap(); + s.execute("select.set", &json!({"ids": [id.0]})).unwrap(); + s.execute("paint.setFill", &json!({"color": {"gray": 0.5}})).unwrap(); + (s, id) +} + +fn ink_tone(curve: &str) -> serde_json::Value { + json!({"tones": [{"color": {"model": "cmyk", "c": 0.0, "m": 0.6, "y": 1.0, "k": 0.0}, "swatch": "Ink", "curve": curve}]}) +} + +fn fill_paint(s: &Session, id: NodeId) -> vectorcraft_color::Paint { + s.doc().unwrap().doc.node(id).unwrap().appearance.fill_paint().clone() +} + +#[test] +fn duotone_draws_a_tint_of_its_spot_tone_and_expands_to_it() { + let (mut s, id) = grey_rect(); + let grey = pixel(&s); + s.execute("effect.apply", &json!({"effect": DUOTONE, "params": ink_tone("0,0 100,100")})).unwrap(); + assert_eq!(fill_paint(&s, id).color().unwrap(), vectorcraft_color::Color::gray(0.5), "the colours stay"); + let drawn = pixel(&s); + assert!(drawn != grey && drawn[0] > drawn[2], "drawn in the ink: {drawn:?}"); + s.execute("effect.expandAppearance", &json!({})).unwrap(); + let vectorcraft_color::Paint::Solid { swatch, tint, .. } = fill_paint(&s, id) else { panic!("a solid fill") }; + assert_eq!(swatch.as_deref(), Some("Ink"), "linked to the spot swatch, so it separates on its plate"); + assert!((tint - 0.5).abs() < 0.01, "{tint}"); +} + +#[test] +fn duotone_as_a_filter_equals_the_live_effect_expanded() { + for params in [ + ink_tone("0,0 100,100"), + ink_tone("0,0 40,70 100,100"), + json!({"tones": [{"color": "#00ffff", "swatch": "Ink"}, {"color": {"model": "cmyk", "c": 0.0, "m": 0.0, "y": 0.0, "k": 1.0}, "curve": "0,0 100,40"}]}), + json!({"tones": [{"color": "#000000"}], "skipSpot": true}), + ] { + let (mut live, id) = grey_rect(); + live.execute("effect.apply", &json!({"effect": DUOTONE, "params": params})).unwrap(); + live.execute("effect.expandAppearance", &json!({})).unwrap(); + let (mut filtered, _) = grey_rect(); + let undo = filtered.doc().unwrap().history.undo.len(); + assert_eq!(filtered.execute("addon.color.filter", &json!({"effect": DUOTONE, "params": params})).unwrap()["changed"], 1); + assert_eq!(filtered.doc().unwrap().history.undo.len(), undo + 1, "one undo step"); + assert_eq!(fill_paint(&filtered, id), fill_paint(&live, id), "{params}"); + } + // The params round-trip: the effect keeps what it was given, and the filter reads the same. + let (mut s, id) = grey_rect(); + let params = ink_tone("0,0 40,70 100,100"); + s.execute("effect.apply", &json!({"effect": DUOTONE, "params": params})).unwrap(); + let kept = s.doc().unwrap().doc.node(id).unwrap().appearance.effects[0].params.clone(); + assert_eq!(kept["tones"], params["tones"]); + s.execute("addon.color.filter", &json!({"effect": DUOTONE, "params": kept})).unwrap(); + let vectorcraft_color::Paint::Solid { swatch, tint, .. } = fill_paint(&s, id) else { panic!("a solid fill") }; + assert_eq!(swatch.as_deref(), Some("Ink")); + let fx = vectorcraft_render::effects::addons::duotone::tone_curve_points(Some(&json!("0,0 40,70 100,100"))); + let want = vectorcraft_render::effects::curve_at(&fx, 0.5); + assert!(want > 0.7 && (tint - want).abs() < 0.01, "the curve lifts the mid tone: {tint}, {want}"); +} + +#[test] +fn duotone_skips_spot_colours_when_asked() { + let (mut s, id) = grey_rect(); + s.execute("paint.setFill", &json!({"swatch": "Ink"})).unwrap(); + let spot = fill_paint(&s, id); + s.execute("addon.color.filter", &json!({"effect": DUOTONE, "params": {"tones": [{"color": "#000000"}], "skipSpot": true}})).unwrap(); + assert_eq!(fill_paint(&s, id), spot); + s.execute("addon.color.filter", &json!({"effect": DUOTONE, "params": {"tones": [{"color": "#000000"}]}})).unwrap(); + assert_ne!(fill_paint(&s, id), spot); +} diff --git a/crates/engine/src/cmd/colorcmds.rs b/crates/engine/src/cmd/colorcmds.rs index 2fadc11fd..4ab859740 100644 --- a/crates/engine/src/cmd/colorcmds.rs +++ b/crates/engine/src/cmd/colorcmds.rs @@ -335,6 +335,12 @@ pub(crate) fn recolor(s: &mut Session, p: &Value, label: &str, f: &dyn Fn(Color) run_pass(s, p, label, Recolor::new(Scope::of(p), f)) } +/// [`recolor`] with solid colours and gradient stops mapped through `links`, with their swatch +/// link and tint. +pub(crate) fn recolor_links(s: &mut Session, p: &Value, label: &str, f: &dyn Fn(Color) -> Color, links: &LinkMap) -> Result { + run_pass(s, p, label, Recolor::with_links(Scope::of(p), f, links)) +} + /// Run `pass` on the `ids` param's objects (default: the selection) as one undo step. fn run_pass(s: &mut Session, p: &Value, label: &str, pass: Recolor) -> Result { let ids = match ids_param(p, "ids") { diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index bf28fa057..c5f9fc9de 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -406,6 +406,22 @@ fn structured_junk() { ("effect.apply", json!({"effect": "addon.color.halftone", "params": {"frequency": 1e308, "angle": -1e308, "shape": "star", "mode": "rgb"}})), ("effect.apply", json!({"effect": "addon.color.halftone", "params": {"frequency": 300, "invert": true, "clip": false, "fromArt": true}})), ("effect.apply", json!({"effect": "addon.color.halftone", "params": {"frequency": -1e308, "mode": "cmyk", "color": "#123456", "target": 3}})), + ("effect.apply", json!({"effect": "addon.color.duotone", "params": {"tones": "x", "skipSpot": "yes"}})), + ( + "effect.apply", + json!({"effect": "addon.color.duotone", "params": {"tones": [1, null, {"color": [1e308, -1e308, "x"], "curve": [[1e308, -1e308], ["a"]]}]}}), + ), + ( + "effect.apply", + json!({"effect": "addon.color.duotone", "params": {"tones": [{"swatch": 3, "curve": "1e999,5 x,y", "active": "no"}, {"color": {"model": "lab", "l": 1e30, "a": -1e30, "b": 0}}, {}, {}, {}, {}]}}), + ), + ( + "addon.color.filter", + json!({"effect": "addon.color.duotone", "params": {"tones": [{"color": "#ff0000", "swatch": "[Registration]"}], "skipSpot": true}}), + ), + ("addon.color.printFixes", json!({"inkLimit": 1e308, "overprintBlack": -1e308})), + ("addon.color.printFixes", json!({"inkLimit": 1, "overprintBlack": 1, "removeWhiteOverprint": true, "scope": "document", "textOnly": true})), + ("addon.color.printFixes", json!({"inkLimit": [300], "scope": 3, "ids": "x"})), ("effect.apply", json!({"effect": "no.such.effect"})), ("effect.remove", json!({"index": 99})), ("effect.setParams", json!({"index": 0, "params": null})), diff --git a/crates/render/src/fx/renderer.rs b/crates/render/src/fx/renderer.rs index b053f3120..949732c52 100644 --- a/crates/render/src/fx/renderer.rs +++ b/crates/render/src/fx/renderer.rs @@ -444,7 +444,7 @@ impl Renderer { let art = if effects::has_revolve(a) || effects::has_solid(a) { effects::evaluate_container(Some(doc), &art).unwrap_or(art) } else { art }; // Vector Halftone samples the object with its colour adjustments: its dots keep their inks. let outer = if effects::addons::halftone::has_halftone(a) { None } else { effects::color_map(&a.appearance.effects) }; - let mut art = self.adjusted_art(art, outer.as_ref()); + let mut art = self.adjusted_art(doc, art, outer.as_ref()); // A path's geometry effects still apply when it is drawn. let path = matches!(art.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }); art.appearance.effects.retain(|e| path && effects::is_geometry(&e.id)); @@ -477,12 +477,15 @@ impl Renderer { } /// `art` with the colour adjustments inside it applied, then `outer` (its object's own) on - /// everything ([`effects::adjust`]). Embedded images become recoloured copies, cached with the - /// decoded images under a key naming the adjustment. - fn adjusted_art(&mut self, mut art: Node, outer: Option<&effects::ColorMap>) -> Node { + /// everything ([`effects::adjust`]), with `doc`'s swatches. Embedded images become recoloured + /// copies, cached with the decoded images under a key naming the adjustment. + fn adjusted_art(&mut self, doc: &vectorcraft_doc::Document, mut art: Node, outer: Option<&effects::ColorMap>) -> Node { art.appearance.effects.retain(|e| !effects::is_adjustment(&e.id)); let mut wanted: Vec<(String, String, u64, effects::ColorMap)> = vec![]; - let adjusted = effects::adjust(&art, outer, &mut |key, map| { + // shortcut: art kept by `fx_arts` isn't made again when only a swatch's kind (spot or + // process) changes, which Duotone reads; editing the object makes it again. + let swatches = |name: &str| effects::addons::duotone::swatch_kind(doc, name); + let adjusted = effects::adjust(&art, outer, &swatches, &mut |key, map| { let mut h = std::hash::DefaultHasher::new(); std::hash::Hash::hash(&format!("{map:?}"), &mut h); let hash = std::hash::Hasher::finish(&h); diff --git a/crates/ui-egui/src/addons/color.rs b/crates/ui-egui/src/addons/color.rs index e338137ce..c3944cbb8 100644 --- a/crates/ui-egui/src/addons/color.rs +++ b/crates/ui-egui/src/addons/color.rs @@ -1,8 +1,9 @@ //! Color Effects' panel and submenu (Object › Plug-ins › Color Effects). The panel picks a colour //! adjustment and its values, then adds it to the selection as a live effect or applies it as a -//! filter, into the colours themselves (`addon.color.filter`). The submenu applies the add-on's -//! live effects, Vector Halftone among them (Effect › Color Effects has them too), and opens the -//! panel. +//! filter, into the colours themselves (`addon.color.filter`); below, under Inks, it opens Duotone's +//! dialog and Print Ink Fixes' (`addon.color.printFixes`). The submenu applies the add-on's live +//! effects, Duotone and Vector Halftone among them (Effect › Color Effects has them too), opens +//! Print Ink Fixes and opens the panel. use egui::Ui; use serde_json::{Map, Value, json}; @@ -63,6 +64,21 @@ const ADJUSTMENTS: [(&str, &str, &[Field]); 7] = [ ("addon.color.invert", "Invert Colors", &[]), ]; +/// Print Ink Fixes' menu item: its label, command and the options its dialog opens with (the +/// selection, or the document while nothing is selected). +fn print_fixes() -> (&'static str, &'static str, Value) { + let options = json!({ + "inkLimit": 300, "excludePureBlack": true, "removeWhiteOverprint": true, "overprintBlack": 100, + "textOnly": false, "fill": true, "stroke": true, + }); + ("Print Ink Fixes…", "addon.color.printFixes", options) +} + +/// Duotone's menu item: its label, and its effect's dialog. +fn duotone() -> (&'static str, &'static str, Value) { + ("Duotone…", "effect.dialog", json!({ "effect": vectorcraft_effects::addons::duotone::DUOTONE })) +} + /// Adjustment `i` of [`ADJUSTMENTS`] (the first past the end). fn adjustment(i: usize) -> (&'static str, &'static str, &'static [Field]) { ADJUSTMENTS.get(i).copied().unwrap_or(ADJUSTMENTS[0]) @@ -70,14 +86,18 @@ fn adjustment(i: usize) -> (&'static str, &'static str, &'static [Field]) { /// Object › Plug-ins › Color Effects. pub fn menu() -> Vec { + let cmd = |(label, id, p): (&'static str, &'static str, Value)| Item::Cmd(label, id, p); vec![ Item::Cmd("Color Effects Panel", "window.panel", json!({ "panel": ID })), Item::Sep, Item::Cmd("Exposure…", "effect.dialog", json!({ "effect": "addon.color.exposure" })), Item::Cmd("Desaturate", "effect.apply", json!({ "effect": "addon.color.desaturate" })), + cmd(duotone()), Item::Cmd("Invert Colors", "effect.apply", json!({ "effect": "addon.color.invert" })), Item::Cmd("Vector Halftone…", "effect.dialog", json!({ "effect": "addon.color.halftone" })), Item::Sep, + cmd(print_fixes()), + Item::Sep, Item::Sub( "Select by Color Kind", [ @@ -159,6 +179,16 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } }); set_pstate(ui.ctx(), STATE, st); + // Inks: Duotone's and Print Ink Fixes' dialogs, as their menu items open them. + ui.add_space(8.0); + widgets::subheader(ui, tl!("Inks")); + let width = ui.available_width(); + for (label, id, p) in [duotone(), print_fixes()] { + if widgets::flat_button(ui, tl!(label), width).clicked() { + let (id, p) = crate::menus::click_target(label, id, &p); + crate::menus::invoke(app, &id, p); + } + } } #[cfg(test)] diff --git a/crates/ui-egui/src/addons/color/tests.rs b/crates/ui-egui/src/addons/color/tests.rs index a44f1d985..7ba046c72 100644 --- a/crates/ui-egui/src/addons/color/tests.rs +++ b/crates/ui-egui/src/addons/color/tests.rs @@ -22,10 +22,11 @@ fn the_menus_list_the_effects_and_open_the_panel() { let at = |path: &[&str], label: &str| entries.iter().find(|e| e.path == path && e.label == label); let plugins = ["Object", "Plug-ins", "Color Effects"]; let item = at(&plugins, "Color Effects Panel").expect("listed"); - for label in ["Exposure…", "Desaturate", "Invert Colors", "Vector Halftone…"] { + for label in ["Exposure…", "Desaturate", "Duotone…", "Invert Colors", "Vector Halftone…"] { assert!(at(&plugins, label).is_some(), "Object › Plug-ins › Color Effects › {label}"); assert!(at(&["Effect", "Color Effects"], label).is_some(), "Effect › Color Effects › {label}"); } + assert!(at(&plugins, "Print Ink Fixes…").is_some(), "Object › Plug-ins › Color Effects › Print Ink Fixes…"); // The existing submenus are as they were. assert!(at(&["Effect", "Color Adjustments"], "Exposure…").is_none()); menus::invoke(&mut app, "window.panel", item.params.clone()); @@ -53,7 +54,7 @@ fn the_panel_shows_the_values_and_its_buttons() { set_pstate(ui.ctx(), STATE, Picked { index: exposure, ..Picked::default() }); show(app, ui); }); - for s in ["Exposure", "Offset", "Gamma", "Add as Effect", "Apply as Filter"] { + for s in ["Exposure", "Offset", "Gamma", "Add as Effect", "Apply as Filter", "Inks", "Duotone…", "Print Ink Fixes…"] { assert!(text.contains(s), "{s} in {text}"); } app.run("select.none", json!({})).unwrap(); @@ -82,3 +83,55 @@ fn the_halftone_effect_dialog_has_the_command_fields_and_edits_the_effect() { app.run("effect.dialog", json!({"effect": "addon.color.halftone", "index": 0})).unwrap(); assert_eq!(app.ui.dialog.as_ref().unwrap().fields["fromArt"], json!(true)); } + +#[test] +fn the_duotone_dialog_has_a_row_per_tone_and_the_curve_of_one() { + let mut app = app(); + let text = |app: &mut VectorcraftApp| painted_text(app, |app, ui| crate::dialogs::show(app, ui.ctx())); + app.run("swatch.new", json!({"name": "Ink", "color": "#e07020", "spot": true})).unwrap(); + app.run("effect.dialog", json!({"effect": "addon.color.duotone"})).unwrap(); + let shown = text(&mut app); + for s in ["Duotone", "Tone 1", "Tone 2", "Tone 3", "Tone 4", "Process Color", "Curve", "Points", "Reset to Linear", "Reset to Inverse"] { + assert!(shown.contains(s), "{s} in {shown}"); + } + assert!(shown.contains("Skip Objects with Spot Colors") && shown.contains("Preview"), "{shown}"); + assert!(app.session.in_interaction(), "the preview runs"); + // Four tones while the dialog is open, the ones after the first off. + let tones = app.ui.dialog.as_ref().unwrap().fields["tones"].as_array().unwrap().clone(); + assert_eq!(tones.iter().map(|t| t["active"].as_bool().unwrap_or(true)).collect::>(), [true, false, false, false]); + // A spot tone with an inverse curve; the second tone's curve shows when picked. + { + let d = app.ui.dialog.as_mut().unwrap(); + d.fields["tones"][0] = json!({"color": "#e07020", "swatch": "Ink", "curve": "0,100 100,0", "active": true}); + d.fields.insert("__tone".into(), json!(1)); + } + assert!(text(&mut app).contains("Ink"), "the swatch is named"); + crate::dialogs::confirm(&mut app).unwrap(); + let st = app.session.active().unwrap(); + let fx = st.doc.node(st.selection.objects[0]).unwrap().appearance.effects.clone(); + assert_eq!((fx.len(), fx[0].id.as_str()), (1, "addon.color.duotone")); + assert_eq!((fx[0].params["tones"][0]["swatch"].clone(), fx[0].params["tones"].as_array().unwrap().len()), (json!("Ink"), 4)); + assert!(!fx[0].params.as_object().unwrap().contains_key("__tone"), "dialog-only"); + // The Appearance panel reopens it with its values. + app.run("effect.dialog", json!({"effect": "addon.color.duotone", "index": 0})).unwrap(); + assert_eq!(app.ui.dialog.as_ref().unwrap().fields["tones"][0]["curve"], json!("0,100 100,0")); +} + +#[test] +fn print_ink_fixes_opens_its_options_and_runs_on_ok() { + let mut app = app(); + app.run("file.documentColorMode", json!({"mode": "cmyk"})).unwrap(); + app.run("paint.setFill", json!({"color": {"c": 100, "m": 100, "y": 100, "k": 100}})).unwrap(); + let entries = menus::menu_entries(&app); + let item = entries.iter().find(|e| e.path == ["Object", "Plug-ins", "Color Effects"] && e.label == "Print Ink Fixes…").unwrap(); + let (id, p) = menus::click_target(&item.label, item.command.as_deref().unwrap(), &item.params); + menus::invoke(&mut app, &id, p); + let shown = painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx())); + for s in ["Print Ink Fixes", "Ink Limit", "Exclude Pure Black", "Remove White Overprint", "Overprint Black", "Text Only"] { + assert!(shown.contains(s), "{s} in {shown}"); + } + crate::dialogs::confirm(&mut app).unwrap(); + let st = app.session.active().unwrap(); + let fill = st.doc.node(st.selection.objects[0]).unwrap().appearance.fill_paint().color().unwrap(); + assert_eq!(fill, vectorcraft_color::Color::cmyk(0.75, 0.75, 0.75, 0.75), "scaled to 300 %"); +} diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index eb78e4392..ba85dc4a2 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -21,6 +21,7 @@ pub mod confirm; pub mod corners; pub mod dimension; mod document_setup; +mod duotone; pub mod dxf_import; pub mod dxf_options; pub mod edit_selection; diff --git a/crates/ui-egui/src/dialogs/command.rs b/crates/ui-egui/src/dialogs/command.rs index 8e8196ade..3ebefdc21 100644 --- a/crates/ui-egui/src/dialogs/command.rs +++ b/crates/ui-egui/src/dialogs/command.rs @@ -49,6 +49,7 @@ fn order(command: &str) -> &'static [&'static str] { match command { // The public Trap options' order. "object.pathfinder.trap" => &["thickness", "heightWidth", "tintReduction", "trapsWithProcessColor", "reverseTraps"], + "addon.color.printFixes" => &["inkLimit", "excludePureBlack", "removeWhiteOverprint", "overprintBlack", "textOnly", "fill", "stroke"], _ => &[], } } diff --git a/crates/ui-egui/src/dialogs/duotone.rs b/crates/ui-egui/src/dialogs/duotone.rs new file mode 100644 index 000000000..69ce931a8 --- /dev/null +++ b/crates/ui-egui/src/dialogs/duotone.rs @@ -0,0 +1,123 @@ +//! Duotone's dialog (Effect › Color Effects › Duotone…): a row per tone (on or off, its swatch or +//! process colour), the curve of the tone picked under Curve (the graph, its points as text and +//! the Reset buttons) and Skip Objects with Spot Colors. +//! +//! Fields: `tones` (the effect's, always [`MAX_TONES`] while the dialog is open, the ones after +//! the first off), `skipSpot`, and `__tone`, the tone whose curve shows. + +use serde_json::{Map, Value, json}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_effects::addons::duotone::{MAX_TONES, tone_curve_points}; + +use super::effect::{curve_graph, points_text}; +use super::form; +use crate::state::Dialog; +use crate::{VectorcraftApp, widgets}; + +/// A curve's text: linear, inverse. +const LINEAR: &str = "0,0 100,100"; +const INVERSE: &str = "0,100 100,0"; + +/// Width of the label column. +const LABEL_W: f32 = 84.0; + +/// A tone's colour (black when it has none that reads). +fn tone_color(tone: &Value) -> Color { + match tone.get("color") { + Some(Value::String(s)) => Color::from_hex(s), + Some(v @ Value::Object(_)) => serde_json::from_value(v.clone()).ok(), + _ => None, + } + .unwrap_or(Color::BLACK) +} + +/// The dialog's fields; returns nothing, the caller compares the values with those previewed. +pub(super) fn fields(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { + let mut tones: Vec = d.fields.get("tones").and_then(Value::as_array).cloned().unwrap_or_default(); + tones.truncate(MAX_TONES); + while tones.len() < MAX_TONES { + tones.push(json!({"color": "#000000", "curve": LINEAR, "active": false})); + } + // The global and spot swatches a tone can be: (name, colour). + let swatches: Vec<(String, Color)> = app + .session + .active() + .map(|st| st.doc.swatches_iter().filter_map(|w| Some((w.name.clone(), st.doc.global_color(&w.name)?))).collect()) + .unwrap_or_default(); + let labels = [tl!("Tone 1"), tl!("Tone 2"), tl!("Tone 3"), tl!("Tone 4")]; + let process = tl!("Process Color"); + let names: Vec<&str> = std::iter::once(process).chain(swatches.iter().map(|(n, _)| n.as_str())).collect(); + for (i, tone) in tones.iter_mut().enumerate() { + let Some(t) = tone.as_object_mut() else { continue }; + ui.horizontal(|ui| { + let on = t.get("active").and_then(Value::as_bool).unwrap_or(true); + if widgets::check(ui, labels.get(i).copied().unwrap_or_default(), on, true) { + t.insert("active".into(), json!(!on)); + } + tone_row(ui, (i, labels.get(i).copied().unwrap_or_default()), t, &swatches, &names); + }); + } + ui.add_space(6.0); + widgets::subheader(ui, tl!("Curve")); + let picked = (d.f64("__tone", 0.0).max(0.0) as usize).min(MAX_TONES - 1); + ui.horizontal(|ui| { + let current = labels.get(picked).copied().unwrap_or_default(); + if let Some(i) = widgets::dropdown_named(ui, "duotone-curve-tone", Some(tl!("Curve")), current, &labels, 120.0) { + d.fields.insert("__tone".into(), json!(i)); + } + }); + if let Some(t) = tones.get_mut(picked).and_then(Value::as_object_mut) { + curve_fields(ui, t); + } + d.fields.insert("tones".into(), Value::Array(tones)); + ui.add_space(4.0); + form::check(ui, d, "skipSpot", tl!("Skip Objects with Spot Colors")); +} + +/// Tone `i`'s (named `name`) colour chip, its swatch (or Process Color) and, as a process colour, +/// its hex value. +fn tone_row(ui: &mut egui::Ui, (i, name): (usize, &str), t: &mut Map, swatches: &[(String, Color)], names: &[&str]) { + let color = tone_color(&Value::Object(t.clone())); + let (chip, _) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), egui::Sense::hover()); + widgets::paint_chip(ui, chip, &Paint::solid(color)); + let swatch = t.get("swatch").and_then(Value::as_str).filter(|s| !s.is_empty()).map(str::to_string); + let current = swatch.as_deref().unwrap_or(names.first().copied().unwrap_or_default()); + if let Some(k) = widgets::dropdown_names_named(ui, ("duotone-swatch", i), Some(name), current, names, 130.0) { + match k.checked_sub(1).and_then(|k| swatches.get(k)) { + Some((name, c)) => { + t.insert("swatch".into(), json!(name)); + t.insert("color".into(), serde_json::to_value(c).unwrap_or(Value::Null)); + } + None => { + t.remove("swatch"); + } + } + } + if swatch.is_none() + && let Some(hex) = widgets::hex_field_named(ui, ("duotone-hex", i), Some(name), color.to_hex().trim_start_matches('#')) + && let Some(c) = Color::from_hex(&format!("#{hex}")) + { + t.insert("color".into(), json!(c.to_hex())); + } +} + +/// The picked tone's curve: the graph, its points as text, and Reset to Linear or to Inverse. +fn curve_fields(ui: &mut egui::Ui, t: &mut Map) { + let mut pts = tone_curve_points(t.get("curve")); + if curve_graph(ui, &mut pts, 100.0) { + t.insert("curve".into(), json!(points_text(&pts, 100.0))); + } + widgets::label_row(ui, tl!("Points"), LABEL_W, |ui| { + let mut text = t.get("curve").and_then(Value::as_str).unwrap_or(LINEAR).to_string(); + if ui.add(egui::TextEdit::singleline(&mut text).desired_width(200.0)).changed() { + t.insert("curve".into(), json!(text)); + } + }); + ui.horizontal(|ui| { + for (label, curve) in [(tl!("Reset to Linear"), LINEAR), (tl!("Reset to Inverse"), INVERSE)] { + if widgets::flat_button(ui, label, 120.0).clicked() { + t.insert("curve".into(), json!(curve)); + } + } + }); +} diff --git a/crates/ui-egui/src/dialogs/effect.rs b/crates/ui-egui/src/dialogs/effect.rs index d54fe8296..6fc6203de 100644 --- a/crates/ui-egui/src/dialogs/effect.rs +++ b/crates/ui-egui/src/dialogs/effect.rs @@ -167,6 +167,10 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { super::halftone::screen_fields(ui, d); values_changed(d) } + None if id == vectorcraft_effects::addons::duotone::DUOTONE => { + super::duotone::fields(app, ui, d); + values_changed(d) + } None if id == GLASS => glass_fields(app, ui, d, GLASS), None if id == TEXTURIZER => glass_fields(app, ui, d, TEXTURIZER), None if id == TRANSFORM => transform_fields(ui, d, app.session.general_unit()), @@ -454,7 +458,10 @@ fn adjust_fields(ui: &mut egui::Ui, d: &mut Dialog) -> bool { if d.fields.contains_key("points") { ui.horizontal(|ui| { ui.add_space(LABEL_W + ui.spacing().item_spacing.x); - curve_graph(ui, d); + let mut pts = vectorcraft_effects::curve_points(d.fields.get("points")); + if curve_graph(ui, &mut pts, 255.0) { + d.fields.insert("points".into(), json!(points_text(&pts, 255.0))); + } }); } for (key, label, width) in [("points", tl!("Points"), 220.0), ("color", tl!("Color"), 120.0)] { @@ -714,15 +721,15 @@ fn drop_shadow_fields(ui: &mut egui::Ui, d: &mut Dialog, unit: vectorcraft_doc:: values_changed(d) } -/// Curves' graph of its `points` (input across, output up, 0..255): drag a point to move it -/// between its neighbours, press on the graph away from the points to add one there, drag one -/// off the graph to remove it (two always stay). -fn curve_graph(ui: &mut egui::Ui, d: &mut Dialog) { +/// A curve's graph of its points `pts` (0..1 each; input across, output up), as Curves and +/// Duotone's tones edit them on a scale of 0..`max`: drag a point to move it between its +/// neighbours, press on the graph away from the points to add one there, drag one off the graph +/// to remove it (two always stay). Returns whether the points changed. +pub(super) fn curve_graph(ui: &mut egui::Ui, pts: &mut Vec<(f32, f32)>, max: f32) -> bool { const SIZE: f32 = 200.0; /// How near (screen points) a press must be to take a point. const GRAB: f32 = 8.0; let t = Tokens::get(ui.ctx()); - let mut pts = vectorcraft_effects::curve_points(d.fields.get("points")); let (rect, resp) = ui.allocate_exact_size(egui::vec2(SIZE, SIZE), egui::Sense::click_and_drag()); let screen = |(x, y): (f32, f32)| egui::pos2(rect.left() + x * SIZE, rect.bottom() - y * SIZE); let unit = |p: egui::Pos2| (((p.x - rect.left()) / SIZE).clamp(0.0, 1.0), ((rect.bottom() - p.y) / SIZE).clamp(0.0, 1.0)); @@ -752,7 +759,7 @@ fn curve_graph(ui: &mut egui::Ui, d: &mut Dialog) { pts.remove(i.min(pts.len() - 1)); grabbed = None; } else { - let step = 1.0 / 255.0; + let step = 1.0 / max; let lo = if i == 0 { 0.0 } else { pts.get(i - 1).map_or(0.0, |a| a.0 + step) }; let hi = pts.get(i + 1).map_or(1.0, |a| a.0 - step); let (x, y) = unit(p); @@ -776,17 +783,19 @@ fn curve_graph(ui: &mut egui::Ui, d: &mut Dialog) { painter.line_segment([screen((0.0, f)), screen((1.0, f))], faint); } painter.line_segment([screen((0.0, 0.0)), screen((1.0, 1.0))], faint); - let curve: Vec = (0..=64).map(|k| k as f32 / 64.0).map(|x| screen((x, vectorcraft_effects::curve_at(&pts, x)))).collect(); + let curve: Vec = (0..=64).map(|k| k as f32 / 64.0).map(|x| screen((x, vectorcraft_effects::curve_at(pts.as_slice(), x)))).collect(); painter.add(egui::Shape::line(curve, egui::Stroke::new(1.5, t.text))); for (i, q) in pts.iter().enumerate() { let fill = if Some(i) == grabbed { t.accent } else { t.panel }; painter.circle(screen(*q), 4.0, fill, egui::Stroke::new(1.5, t.text)); } painter.rect_stroke(rect, 2.0, egui::Stroke::new(1.0, t.input_border), egui::StrokeKind::Inside); - if changed { - let text: Vec = pts.iter().map(|(x, y)| format!("{},{}", (x * 255.0).round(), (y * 255.0).round())).collect(); - d.fields.insert("points".into(), json!(text.join(" "))); - } + changed +} + +/// Curve points `pts` (0..1) as the `"x,y x,y …"` text of a curve on a scale of 0..`max`. +pub(super) fn points_text(pts: &[(f32, f32)], max: f32) -> String { + pts.iter().map(|(x, y)| format!("{},{}", (x * max).round(), (y * max).round())).collect::>().join(" ") } /// What OK runs: `effect.setParams` on the edited effect (`__index`), else `effect.apply` of the diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 530dc7b5d..2a64dfdb7 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5749,3 +5749,26 @@ anchor Points: Точки: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Цвят на точките от илюстрацията + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Двутон… + Duotone Двутон + Print Ink Fixes… Корекции на мастилата за печат… + Print Ink Fixes Корекции на мастилата за печат + Inks Мастила + Tone 1 Тон 1 + Tone 2 Тон 2 + Tone 3 Тон 3 + Tone 4 Тон 4 + Curve Крива + Reset to Linear Връщане към линейна + Reset to Inverse Връщане към обърната + Skip Objects with Spot Colors Пропускане на обекти с плакатни цветове + Ink Limit Лимит на мастилото + Exclude Pure Black Без чисто черно + Remove White Overprint Премахване на напечатването на бялото + Overprint Black Напечатване на черното + Text Only Само текст +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` задайте `inkLimit`, `removeWhiteOverprint` или `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Корекциите на мастилата за печат работят върху CMYK документи: първо изберете Файл › Цветен режим на документа › CMYK цвят +@msg `{key}` must be 0..{max} `{key}` трябва да е 0..{max} diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 08e8c524d..473c7b8bf 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5850,3 +5850,26 @@ anchor Points: Punts: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Color dels punts de la il·lustració + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Bitò… + Duotone Bitò + Print Ink Fixes… Correccions de tinta per a impressió… + Print Ink Fixes Correccions de tinta per a impressió + Inks Tintes + Tone 1 To 1 + Tone 2 To 2 + Tone 3 To 3 + Tone 4 To 4 + Curve Corba + Reset to Linear Restableix a lineal + Reset to Inverse Restableix a invertida + Skip Objects with Spot Colors Omet els objectes amb colors directes + Ink Limit Límit de tinta + Exclude Pure Black Exclou el negre pur + Remove White Overprint Elimina la sobreimpressió del blanc + Overprint Black Sobreimpressió del negre + Text Only Només text +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` indiqueu `inkLimit`, `removeWhiteOverprint` o `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Les correccions de tinta per a impressió funcionen en documents CMYK: trieu primer Fitxer › Mode de color del document › Color CMYK +@msg `{key}` must be 0..{max} `{key}` ha de ser 0..{max} diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 30ebe1e7f..91be88b94 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -5859,3 +5859,26 @@ anchor Points: Body: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Barva bodů podle kresby + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Dvoutón… + Duotone Dvoutón + Print Ink Fixes… Opravy tiskových barev… + Print Ink Fixes Opravy tiskových barev + Inks Barvy + Tone 1 Tón 1 + Tone 2 Tón 2 + Tone 3 Tón 3 + Tone 4 Tón 4 + Curve Křivka + Reset to Linear Obnovit lineární + Reset to Inverse Obnovit obrácenou + Skip Objects with Spot Colors Vynechat objekty s přímými barvami + Ink Limit Limit barvy + Exclude Pure Black Vyloučit čistou černou + Remove White Overprint Odebrat přetisk bílé + Overprint Black Přetisk černé + Text Only Pouze text +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` zadejte `inkLimit`, `removeWhiteOverprint` nebo `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Opravy tiskových barev fungují v dokumentech CMYK: nejprve zvolte Soubor › Barevný režim dokumentu › Barvy CMYK +@msg `{key}` must be 0..{max} `{key}` musí být 0..{max} diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 1c67176a2..bc671274c 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5764,3 +5764,26 @@ anchor Points: Punkter: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Prikfarve fra grafik + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Duotone… + Duotone Duotone + Print Ink Fixes… Trykfarverettelser… + Print Ink Fixes Trykfarverettelser + Inks Trykfarver + Tone 1 Tone 1 + Tone 2 Tone 2 + Tone 3 Tone 3 + Tone 4 Tone 4 + Curve Kurve + Reset to Linear Nulstil til lineær + Reset to Inverse Nulstil til omvendt + Skip Objects with Spot Colors Spring objekter med spotfarver over + Ink Limit Farvegrænse + Exclude Pure Black Udelad ren sort + Remove White Overprint Fjern overprint af hvid + Overprint Black Overprint sort + Text Only Kun tekst +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` angiv `inkLimit`, `removeWhiteOverprint` eller `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Trykfarverettelser virker på CMYK-dokumenter: vælg først Filer › Dokumentets farvetilstand › CMYK-farve +@msg `{key}` must be 0..{max} `{key}` skal være 0..{max} diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index cf6c8aec5..a87dcbc6a 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5444,3 +5444,26 @@ anchor Points: Punkte: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Punktfarbe aus Grafik + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Duplex… + Duotone Duplex + Print Ink Fixes… Druckfarbkorrekturen… + Print Ink Fixes Druckfarbkorrekturen + Inks Druckfarben + Tone 1 Druckfarbe 1 + Tone 2 Druckfarbe 2 + Tone 3 Druckfarbe 3 + Tone 4 Druckfarbe 4 + Curve Kurve + Reset to Linear Auf linear zurücksetzen + Reset to Inverse Auf umgekehrt zurücksetzen + Skip Objects with Spot Colors Objekte mit Volltonfarben auslassen + Ink Limit Farbauftragsgrenze + Exclude Pure Black Reines Schwarz ausnehmen + Remove White Overprint Überdrucken von Weiß entfernen + Overprint Black Schwarz überdrucken + Text Only Nur Text +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` geben Sie `inkLimit`, `removeWhiteOverprint` oder `overprintBlack` an +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Druckfarbkorrekturen wirken in CMYK-Dokumenten: Wählen Sie zuerst Datei › Dokumentfarbmodus › CMYK-Farbe +@msg `{key}` must be 0..{max} `{key}` muss 0..{max} sein diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 91c001e7a..8b571c11f 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -5811,3 +5811,26 @@ anchor Points: Σημεία: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Χρώμα κουκκίδων από το γραφικό + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Δίτονο… + Duotone Δίτονο + Print Ink Fixes… Διορθώσεις μελανιών εκτύπωσης… + Print Ink Fixes Διορθώσεις μελανιών εκτύπωσης + Inks Μελάνια + Tone 1 Τόνος 1 + Tone 2 Τόνος 2 + Tone 3 Τόνος 3 + Tone 4 Τόνος 4 + Curve Καμπύλη + Reset to Linear Επαναφορά σε γραμμική + Reset to Inverse Επαναφορά σε αντεστραμμένη + Skip Objects with Spot Colors Παράλειψη αντικειμένων με χρώματα spot + Ink Limit Όριο μελανιού + Exclude Pure Black Εξαίρεση καθαρού μαύρου + Remove White Overprint Αφαίρεση υπερεκτύπωσης λευκού + Overprint Black Υπερεκτύπωση μαύρου + Text Only Μόνο κείμενο +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` δώστε `inkLimit`, `removeWhiteOverprint` ή `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Οι διορθώσεις μελανιών εκτύπωσης ισχύουν σε έγγραφα CMYK: επιλέξτε πρώτα Αρχείο › Λειτουργία χρώματος εγγράφου › Χρώμα CMYK +@msg `{key}` must be 0..{max} το `{key}` πρέπει να είναι 0..{max} diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 8eaac3bb7..b84ce995a 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -5802,3 +5802,26 @@ anchor Points: Puntos: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Color de puntos de la ilustración + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Duotono… + Duotone Duotono + Print Ink Fixes… Correcciones de tinta para impresión… + Print Ink Fixes Correcciones de tinta para impresión + Inks Tintas + Tone 1 Tono 1 + Tone 2 Tono 2 + Tone 3 Tono 3 + Tone 4 Tono 4 + Curve Curva + Reset to Linear Restablecer a lineal + Reset to Inverse Restablecer a invertida + Skip Objects with Spot Colors Omitir objetos con tintas planas + Ink Limit Límite de tinta + Exclude Pure Black Excluir negro puro + Remove White Overprint Quitar sobreimpresión del blanco + Overprint Black Sobreimprimir negro + Text Only Solo texto +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` indique `inkLimit`, `removeWhiteOverprint` u `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Las correcciones de tinta para impresión funcionan en documentos CMYK: elija primero Archivo › Modo de color del documento › Color CMYK +@msg `{key}` must be 0..{max} `{key}` debe ser 0..{max} diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 3b0bb739e..47aab9377 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -5890,3 +5890,26 @@ anchor Points: Pisteet: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Pisteiden väri kuvituksesta + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Duotone… + Duotone Duotone + Print Ink Fixes… Painovärien korjaukset… + Print Ink Fixes Painovärien korjaukset + Inks Painovärit + Tone 1 Sävy 1 + Tone 2 Sävy 2 + Tone 3 Sävy 3 + Tone 4 Sävy 4 + Curve Käyrä + Reset to Linear Palauta lineaariseksi + Reset to Inverse Palauta käänteiseksi + Skip Objects with Spot Colors Ohita objektit, joissa on spottivärejä + Ink Limit Värirajoitus + Exclude Pure Black Jätä puhdas musta pois + Remove White Overprint Poista valkoisen päällepainatus + Overprint Black Päällepainata musta + Text Only Vain teksti +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` anna `inkLimit`, `removeWhiteOverprint` tai `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Painovärien korjaukset toimivat CMYK-asiakirjoissa: valitse ensin Tiedosto › Asiakirjan väritila › CMYK-väri +@msg `{key}` must be 0..{max} `{key}` on oltava 0..{max} diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index f90d02f87..8e1fb1a78 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -5858,3 +5858,26 @@ anchor Points: Points : # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Couleur des points d’après l’illustration + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Bichromie… + Duotone Bichromie + Print Ink Fixes… Corrections d’encres pour l’impression… + Print Ink Fixes Corrections d’encres pour l’impression + Inks Encres + Tone 1 Ton 1 + Tone 2 Ton 2 + Tone 3 Ton 3 + Tone 4 Ton 4 + Curve Courbe + Reset to Linear Réinitialiser en linéaire + Reset to Inverse Réinitialiser en inversée + Skip Objects with Spot Colors Ignorer les objets avec tons directs + Ink Limit Limite d’encrage + Exclude Pure Black Exclure le noir pur + Remove White Overprint Supprimer la surimpression du blanc + Overprint Black Surimpression du noir + Text Only Texte uniquement +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` indiquez `inkLimit`, `removeWhiteOverprint` ou `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Les corrections d’encres pour l’impression s’appliquent aux documents CMJN : choisissez d’abord Fichier › Mode colorimétrique du document › Couleur CMJN +@msg `{key}` must be 0..{max} `{key}` doit être compris entre 0 et {max} diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index b8035f4c2..fa4728ec4 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5846,3 +5846,26 @@ anchor Points: Pontok: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Pontszín a grafikából + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Duplex… + Duotone Duplex + Print Ink Fixes… Nyomdai festékjavítások… + Print Ink Fixes Nyomdai festékjavítások + Inks Festékek + Tone 1 1. tónus + Tone 2 2. tónus + Tone 3 3. tónus + Tone 4 4. tónus + Curve Görbe + Reset to Linear Visszaállítás lineárisra + Reset to Inverse Visszaállítás fordítottra + Skip Objects with Spot Colors Direkt színeket használó objektumok kihagyása + Ink Limit Festékkorlát + Exclude Pure Black Tiszta fekete kizárása + Remove White Overprint Fehér felülnyomásának eltávolítása + Overprint Black Fekete felülnyomása + Text Only Csak szöveg +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` adja meg: `inkLimit`, `removeWhiteOverprint` vagy `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first A nyomdai festékjavítások CMYK-dokumentumokon működnek: előbb válassza a Fájl › Dokumentum színmódja › CMYK-szín parancsot +@msg `{key}` must be 0..{max} `{key}` értéke 0..{max} lehet diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 995e57458..0907afd03 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5764,3 +5764,26 @@ anchor Points: Titik: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Warna titik dari karya seni + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Duotone… + Duotone Duotone + Print Ink Fixes… Perbaikan Tinta Cetak… + Print Ink Fixes Perbaikan Tinta Cetak + Inks Tinta + Tone 1 Nada 1 + Tone 2 Nada 2 + Tone 3 Nada 3 + Tone 4 Nada 4 + Curve Kurva + Reset to Linear Atur ulang ke linear + Reset to Inverse Atur ulang ke terbalik + Skip Objects with Spot Colors Lewati objek dengan warna spot + Ink Limit Batas tinta + Exclude Pure Black Kecualikan hitam murni + Remove White Overprint Hapus overprint putih + Overprint Black Overprint hitam + Text Only Hanya teks +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` berikan `inkLimit`, `removeWhiteOverprint`, atau `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Perbaikan Tinta Cetak bekerja pada dokumen CMYK: pilih dulu Berkas › Mode warna dokumen › Warna CMYK +@msg `{key}` must be 0..{max} `{key}` harus 0..{max} diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index ffcb2a32a..13b70da3c 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5478,3 +5478,26 @@ anchor Points: Punti: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Colore punti dalla grafica + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Bicromia… + Duotone Bicromia + Print Ink Fixes… Correzioni inchiostri di stampa… + Print Ink Fixes Correzioni inchiostri di stampa + Inks Inchiostri + Tone 1 Tono 1 + Tone 2 Tono 2 + Tone 3 Tono 3 + Tone 4 Tono 4 + Curve Curva + Reset to Linear Ripristina lineare + Reset to Inverse Ripristina invertita + Skip Objects with Spot Colors Salta oggetti con colori spot + Ink Limit Limite inchiostro + Exclude Pure Black Escludi nero puro + Remove White Overprint Rimuovi sovrastampa del bianco + Overprint Black Sovrastampa nero + Text Only Solo testo +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` indica `inkLimit`, `removeWhiteOverprint` o `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Le correzioni inchiostri di stampa funzionano nei documenti CMYK: scegli prima File › Metodo colore documento › Colore CMYK +@msg `{key}` must be 0..{max} `{key}` deve essere 0..{max} diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 5a8d91242..274d1eff8 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5547,3 +5547,26 @@ anchor Points: ポイント: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art ドットの色をアートから取得 + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… ダブルトーン… + Duotone ダブルトーン + Print Ink Fixes… 印刷インキの修正… + Print Ink Fixes 印刷インキの修正 + Inks インキ + Tone 1 トーン 1 + Tone 2 トーン 2 + Tone 3 トーン 3 + Tone 4 トーン 4 + Curve カーブ + Reset to Linear 直線にリセット + Reset to Inverse 反転にリセット + Skip Objects with Spot Colors 特色を使用したオブジェクトをスキップ + Ink Limit インキ総量の上限 + Exclude Pure Black 純粋なブラックを除外 + Remove White Overprint 白のオーバープリントを削除 + Overprint Black ブラックをオーバープリント + Text Only テキストのみ +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` `inkLimit`、`removeWhiteOverprint` または `overprintBlack` を指定してください +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first 印刷インキの修正は CMYK ドキュメントで使用できます。先にファイル › ドキュメントのカラーモード › CMYK カラーを選択してください +@msg `{key}` must be 0..{max} `{key}` は 0..{max} にしてください diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index d150719a2..236a14bef 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -5849,3 +5849,26 @@ anchor Points: წერტილები: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art წერტილების ფერი ნახატიდან + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… დუოტონი… + Duotone დუოტონი + Print Ink Fixes… საბეჭდი მელნის შესწორებები… + Print Ink Fixes საბეჭდი მელნის შესწორებები + Inks მელნები + Tone 1 ტონი 1 + Tone 2 ტონი 2 + Tone 3 ტონი 3 + Tone 4 ტონი 4 + Curve მრუდი + Reset to Linear წრფივზე დაბრუნება + Reset to Inverse შებრუნებულზე დაბრუნება + Skip Objects with Spot Colors სპოტ-ფერების მქონე ობიექტების გამოტოვება + Ink Limit მელნის ზღვარი + Exclude Pure Black სუფთა შავის გამორიცხვა + Remove White Overprint თეთრის ზედდაბეჭდვის მოხსნა + Overprint Black შავის ზედდაბეჭდვა + Text Only მხოლოდ ტექსტი +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` მიუთითეთ `inkLimit`, `removeWhiteOverprint` ან `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first საბეჭდი მელნის შესწორებები მუშაობს CMYK დოკუმენტებზე: ჯერ აირჩიეთ ფაილი › დოკუმენტის ფერის რეჟიმი › CMYK ფერი +@msg `{key}` must be 0..{max} `{key}` უნდა იყოს 0..{max} diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index e956dc4a3..2bc410a42 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5441,3 +5441,26 @@ anchor Points: 포인트: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art 아트워크에서 점 색상 가져오기 + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… 이중톤… + Duotone 이중톤 + Print Ink Fixes… 인쇄 잉크 수정… + Print Ink Fixes 인쇄 잉크 수정 + Inks 잉크 + Tone 1 톤 1 + Tone 2 톤 2 + Tone 3 톤 3 + Tone 4 톤 4 + Curve 곡선 + Reset to Linear 선형으로 재설정 + Reset to Inverse 반전으로 재설정 + Skip Objects with Spot Colors 별색을 사용한 개체 건너뛰기 + Ink Limit 잉크 한계 + Exclude Pure Black 순수 검정 제외 + Remove White Overprint 흰색 겹쳐찍기 제거 + Overprint Black 검정 겹쳐찍기 + Text Only 문자만 +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` `inkLimit`, `removeWhiteOverprint` 또는 `overprintBlack`을 지정하십시오 +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first 인쇄 잉크 수정은 CMYK 문서에서 작동합니다. 먼저 파일 › 문서 색상 모드 › CMYK 색상을 선택하십시오 +@msg `{key}` must be 0..{max} `{key}`은(는) 0..{max}이어야 합니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index fbe5d843c..d5c08f815 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -5808,3 +5808,26 @@ anchor Points: Punten: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Puntkleur uit artwork + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Duotoon… + Duotone Duotoon + Print Ink Fixes… Drukinktcorrecties… + Print Ink Fixes Drukinktcorrecties + Inks Inkten + Tone 1 Toon 1 + Tone 2 Toon 2 + Tone 3 Toon 3 + Tone 4 Toon 4 + Curve Curve + Reset to Linear Herstellen naar lineair + Reset to Inverse Herstellen naar omgekeerd + Skip Objects with Spot Colors Objecten met steunkleuren overslaan + Ink Limit Inktlimiet + Exclude Pure Black Puur zwart uitsluiten + Remove White Overprint Overprint van wit verwijderen + Overprint Black Zwart overprinten + Text Only Alleen tekst +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` geef `inkLimit`, `removeWhiteOverprint` of `overprintBlack` op +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Drukinktcorrecties werken in CMYK-documenten: kies eerst Bestand › Documentkleurmodus › CMYK-kleur +@msg `{key}` must be 0..{max} `{key}` moet 0..{max} zijn diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 618cf8553..c456c6df8 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5714,3 +5714,26 @@ anchor Points: Punkty: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Kolor kropek z kompozycji + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Dwutonowy… + Duotone Dwutonowy + Print Ink Fixes… Poprawki farb drukarskich… + Print Ink Fixes Poprawki farb drukarskich + Inks Farby + Tone 1 Ton 1 + Tone 2 Ton 2 + Tone 3 Ton 3 + Tone 4 Ton 4 + Curve Krzywa + Reset to Linear Resetuj do liniowej + Reset to Inverse Resetuj do odwróconej + Skip Objects with Spot Colors Pomiń obiekty z kolorami dodatkowymi + Ink Limit Limit farby + Exclude Pure Black Wyklucz czystą czerń + Remove White Overprint Usuń nadruk bieli + Overprint Black Nadruk czerni + Text Only Tylko tekst +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` podaj `inkLimit`, `removeWhiteOverprint` lub `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Poprawki farb drukarskich działają w dokumentach CMYK: najpierw wybierz Plik › Tryb koloru dokumentu › Kolor CMYK +@msg `{key}` must be 0..{max} `{key}` musi wynosić 0..{max} diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index a38135298..b95ea47bd 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5777,3 +5777,26 @@ anchor Points: Pontos: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Cor dos Pontos da Arte + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Duotom… + Duotone Duotom + Print Ink Fixes… Correções de Tinta para Impressão… + Print Ink Fixes Correções de Tinta para Impressão + Inks Tintas + Tone 1 Tom 1 + Tone 2 Tom 2 + Tone 3 Tom 3 + Tone 4 Tom 4 + Curve Curva + Reset to Linear Redefinir para linear + Reset to Inverse Redefinir para invertida + Skip Objects with Spot Colors Ignorar objetos com cores spot + Ink Limit Limite de tinta + Exclude Pure Black Excluir preto puro + Remove White Overprint Remover sobreimpressão do branco + Overprint Black Sobreimpressão de Preto + Text Only Somente texto +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` informe `inkLimit`, `removeWhiteOverprint` ou `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first As Correções de Tinta para Impressão funcionam em documentos CMYK: escolha antes Arquivo › Modo de Cor do Documento › Cor CMYK +@msg `{key}` must be 0..{max} `{key}` deve ser 0..{max} diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index b1c4c01be..bbeca6300 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5837,3 +5837,26 @@ anchor Points: Точки: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Цвет точек из рисунка + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Дуплекс… + Duotone Дуплекс + Print Ink Fixes… Исправления печатных красок… + Print Ink Fixes Исправления печатных красок + Inks Краски + Tone 1 Тон 1 + Tone 2 Тон 2 + Tone 3 Тон 3 + Tone 4 Тон 4 + Curve Кривая + Reset to Linear Сбросить на линейную + Reset to Inverse Сбросить на обратную + Skip Objects with Spot Colors Пропускать объекты с плашечными цветами + Ink Limit Предел краски + Exclude Pure Black Исключить чистый чёрный + Remove White Overprint Убрать наложение белого + Overprint Black Наложение чёрного + Text Only Только текст +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` задайте `inkLimit`, `removeWhiteOverprint` или `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Исправления печатных красок работают в документах CMYK: сначала выберите Файл › Цветовой режим документа › Цвет CMYK +@msg `{key}` must be 0..{max} `{key}` должен быть 0..{max} diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 3593d08d9..7ba6023b4 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -5915,3 +5915,26 @@ shape Open Öppen # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Punktfärg från objekt + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Duoton… + Duotone Duoton + Print Ink Fixes… Tryckfärgskorrigeringar… + Print Ink Fixes Tryckfärgskorrigeringar + Inks Tryckfärger + Tone 1 Ton 1 + Tone 2 Ton 2 + Tone 3 Ton 3 + Tone 4 Ton 4 + Curve Kurva + Reset to Linear Återställ till linjär + Reset to Inverse Återställ till omvänd + Skip Objects with Spot Colors Hoppa över objekt med dekorfärger + Ink Limit Färggräns + Exclude Pure Black Uteslut ren svart + Remove White Overprint Ta bort övertryck av vitt + Overprint Black Övertryck svart + Text Only Endast text +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` ange `inkLimit`, `removeWhiteOverprint` eller `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Tryckfärgskorrigeringar fungerar i CMYK-dokument: välj först Fil › Dokumentets färgläge › CMYK-färg +@msg `{key}` must be 0..{max} `{key}` måste vara 0..{max} diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index eb2a277f0..f445da8ad 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -5817,3 +5817,26 @@ anchor Points: Noktalar: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Nokta Rengini Sanattan Al + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Çift Ton… + Duotone Çift Ton + Print Ink Fixes… Baskı Mürekkebi Düzeltmeleri… + Print Ink Fixes Baskı Mürekkebi Düzeltmeleri + Inks Mürekkepler + Tone 1 Ton 1 + Tone 2 Ton 2 + Tone 3 Ton 3 + Tone 4 Ton 4 + Curve Eğri + Reset to Linear Doğrusala sıfırla + Reset to Inverse Terse sıfırla + Skip Objects with Spot Colors Spot renkli nesneleri atla + Ink Limit Mürekkep sınırı + Exclude Pure Black Saf siyahı hariç tut + Remove White Overprint Beyazın üst baskısını kaldır + Overprint Black Üst Baskı Siyah + Text Only Yalnızca metin +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` `inkLimit`, `removeWhiteOverprint` veya `overprintBlack` verin +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Baskı Mürekkebi Düzeltmeleri CMYK belgelerinde çalışır: önce Dosya › Belge Renk Modu › CMYK Rengi'ni seçin +@msg `{key}` must be 0..{max} `{key}` 0..{max} olmalıdır diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 535cbeacc..26da71aac 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5458,3 +5458,26 @@ anchor Points: Точки: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Колір точок із малюнка + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Дуплекс… + Duotone Дуплекс + Print Ink Fixes… Виправлення друкарських фарб… + Print Ink Fixes Виправлення друкарських фарб + Inks Фарби + Tone 1 Тон 1 + Tone 2 Тон 2 + Tone 3 Тон 3 + Tone 4 Тон 4 + Curve Крива + Reset to Linear Скинути до лінійної + Reset to Inverse Скинути до оберненої + Skip Objects with Spot Colors Пропускати об’єкти з плашковими кольорами + Ink Limit Межа фарби + Exclude Pure Black Виключити чистий чорний + Remove White Overprint Прибрати накладення білого + Overprint Black Накладення чорного + Text Only Лише текст +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` задайте `inkLimit`, `removeWhiteOverprint` або `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Виправлення друкарських фарб працюють у документах CMYK: спершу виберіть Файл › Колірний режим документа › Колір CMYK +@msg `{key}` must be 0..{max} `{key}` має бути 0..{max} diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index baf4a60d9..f26f07db7 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5460,3 +5460,26 @@ anchor Points: Điểm: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Màu chấm theo tác phẩm + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… Hai tông… + Duotone Hai tông + Print Ink Fixes… Sửa mực in… + Print Ink Fixes Sửa mực in + Inks Mực + Tone 1 Tông 1 + Tone 2 Tông 2 + Tone 3 Tông 3 + Tone 4 Tông 4 + Curve Đường cong + Reset to Linear Đặt lại thành tuyến tính + Reset to Inverse Đặt lại thành đảo ngược + Skip Objects with Spot Colors Bỏ qua đối tượng có màu pha + Ink Limit Giới hạn mực + Exclude Pure Black Loại trừ đen thuần + Remove White Overprint Bỏ in chồng màu trắng + Overprint Black In chồng đen + Text Only Chỉ văn bản +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` hãy cho `inkLimit`, `removeWhiteOverprint` hoặc `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first Sửa mực in hoạt động trên tài liệu CMYK: trước hết chọn Tệp › Chế độ màu tài liệu › Màu CMYK +@msg `{key}` must be 0..{max} `{key}` phải là 0..{max} diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index d49842dbc..76378e4e8 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5701,3 +5701,26 @@ anchor Points: 点: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art 网点颜色取自图稿 + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… 双色调… + Duotone 双色调 + Print Ink Fixes… 印刷油墨修正… + Print Ink Fixes 印刷油墨修正 + Inks 油墨 + Tone 1 色调 1 + Tone 2 色调 2 + Tone 3 色调 3 + Tone 4 色调 4 + Curve 曲线 + Reset to Linear 重置为线性 + Reset to Inverse 重置为反相 + Skip Objects with Spot Colors 跳过使用专色的对象 + Ink Limit 油墨限制 + Exclude Pure Black 排除纯黑 + Remove White Overprint 移除白色叠印 + Overprint Black 黑色叠印 + Text Only 仅文字 +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` 请指定 `inkLimit`、`removeWhiteOverprint` 或 `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first 印刷油墨修正适用于 CMYK 文档:请先选择 文件 › 文档颜色模式 › CMYK 颜色 +@msg `{key}` must be 0..{max} `{key}` 必须为 0..{max} diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index e305bc445..34597f4aa 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5497,3 +5497,26 @@ anchor Points: 點: # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art 網點顏色取自圖稿 + +# Color Effects add-on: Duotone (Effect › Color Effects) and Print Ink Fixes (Object › Plug-ins › Color Effects). + Duotone… 雙色調… + Duotone 雙色調 + Print Ink Fixes… 印刷油墨修正… + Print Ink Fixes 印刷油墨修正 + Inks 油墨 + Tone 1 色調 1 + Tone 2 色調 2 + Tone 3 色調 3 + Tone 4 色調 4 + Curve 曲線 + Reset to Linear 重設為線性 + Reset to Inverse 重設為反轉 + Skip Objects with Spot Colors 略過使用特別色的物件 + Ink Limit 油墨限制 + Exclude Pure Black 排除純黑 + Remove White Overprint 移除白色疊印 + Overprint Black 疊印黑色 + Text Only 僅文字 +@msg give `inkLimit`, `removeWhiteOverprint` or `overprintBlack` 請指定 `inkLimit`、`removeWhiteOverprint` 或 `overprintBlack` +@msg Print Ink Fixes works on CMYK documents: choose File › Document Color Mode › CMYK Color first 印刷油墨修正適用於 CMYK 文件:請先選擇 檔案 › 文件色彩模式 › CMYK 色彩 +@msg `{key}` must be 0..{max} `{key}` 必須為 0..{max} diff --git a/docs/mcp.md b/docs/mcp.md index 17627ddea..d0568d5cd 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5898,6 +5898,7 @@ the other colour adjustments (Effect › Color Adjustments); each colour keeps i |---|---|---| | `addon.color.exposure` (Exposure…) | `exposure` stops −20…20 (0), `offset` −0.5…0.5 (0), `gamma` 0.1…10 (1) | Scales the light in linear light (each stop doubles or halves it, mostly in the highlights), adds the offset (shadows and midtones) and applies the gamma | | `addon.color.desaturate` (Desaturate) | none | Turns every colour to the grey of its lightness | +| `addon.color.duotone` (Duotone…) | `tones`, `skipSpot` (below) | Prints each colour's tone with one to four inks | | `addon.color.invert` (Invert Colors) | none | Turns every colour to its RGB inverse | `addon.color.filter {effect, params?, fill?, stroke?, includeImages?, includePatterns?, ids?}` applies any colour @@ -5951,3 +5952,59 @@ those paths. Not yet: stochastic (FM) and radial patterns, a dot-gain curve, per-screen separation settings, and type or symbols as dots. + +### Duotone (live) + +`addon.color.duotone` (Effect › Color Effects › Duotone…, also in Object › Plug-ins › Color Effects and the panel's Inks) +prints the grey-equivalent tone of each colour the object paints with one to four inks: a monotone, duotone, tritone or +quadtone. It is a colour adjustment like the add-on's others, so it goes on through `effect.apply`, stacks with the +other adjustments and works as a filter through `addon.color.filter {effect: "addon.color.duotone", params}`; Expand +Appearance gives the same colours as the filter. + +| Parameter | Values (default) | Does | +|---|---|---| +| `tones` | up to four `{color, swatch?, curve?, active?}` (one black tone) | The inks; tones past the fourth are left out, and one not `active` (true) is skipped | +| `tones[].color` | `#rrggbb`, `[r, g, b]` or a colour object (`#000000`) | The ink's colour; with a `swatch`, that swatch's colour (the dialog fills it in) | +| `tones[].swatch` | a global or spot swatch's name (none) | The swatch the ink is | +| `tones[].curve` | `"tint,ink tint,ink …"` or `[[tint, ink], …]`, 0…100, at most 64 points (`"0,0 100,100"`) | The tone's tint to the ink's tint, a smooth curve through the points, flat past its ends | +| `skipSpot` | bool (false) | Colours linked to a spot swatch keep their colour | + +The tone of a colour is its grey equivalent (a grey's ink; for other colours one minus their luma). With one tone of a +swatch the document has, the result is a tint of that swatch, linked to it, so a spot tone separates onto its own plate. +With several tones the inks overprint: the result is the CMYK mix of their tints, each ink multiplying what the others +leave of the paper. A colour links to one swatch at one tint, so a mix of several spot inks becomes process colour. +Gradient stops, type, gradient meshes and embedded images are recoloured as by the other adjustments. + +```json +{"name":"run_command","arguments":{"command":"effect.apply","params":{"effect":"addon.color.duotone","params":{"tones":[{"color":"#000000"},{"color":{"model":"cmyk","c":0,"m":0.5,"y":1,"k":0},"swatch":"Orange","curve":"0,0 50,70 100,100"}]}}}} +``` + +The dialog has a row per tone (on or off, a colour chip, the swatch or Process Color with its hex value), the curve of the +tone picked under Curve (the graph, its points and Reset to Linear or Reset to Inverse) and Skip Objects with Spot Colors. + +Not yet: an opaque undercoat, tones for type only, extra gradient stops for smoother tints, and buttons that flip a +curve or set it to 100 %. + +### Print Ink Fixes + +`addon.color.printFixes {scope?, inkLimit?, excludePureBlack?, removeWhiteOverprint?, overprintBlack?, textOnly?, fill?, +stroke?, ids?}` (Object › Plug-ins › Color Effects › Print Ink Fixes…, also under the panel's Inks) prepares a CMYK +document's inks for print, as one undo step: + +| Parameter | Values (default) | Does | +|---|---|---| +| `scope` | `selection`, `document` (the selection, or the document while nothing is selected) | What it fixes; `ids` stand for the selection | +| `inkLimit` | total ink % 1…400, 0 off (off) | CMYK colours (solid, gradient stops, gradient meshes) with more ink than this are scaled down to it; colours of spot swatches and Registration stay | +| `excludePureBlack` | bool (true) | Black-only CMYK and grey colours keep their ink | +| `removeWhiteOverprint` | bool (false) | White fills and strokes, and characters', stop overprinting | +| `overprintBlack` | black % 1…100, 0 off (off) | Black-only and grey fills and strokes this dark or darker overprint, as `edit.colors.overprintBlack` sets it (not spot-linked or rich blacks) | +| `textOnly` | bool (false) | Overprint Black on type only | +| `fill`, `stroke` | bool (true) | Which paints the fixes reach | + +It returns `changed`, `inkLimited` (paints and meshes scaled), `whiteOverprintRemoved` and `blackOverprinted` (objects); +nothing changed is no undo step. It needs a CMYK document (File › Document Color Mode › CMYK Color) and refuses an RGB +one, out of range values and a request with no fix on. + +```json +{"name":"run_command","arguments":{"command":"addon.color.printFixes","params":{"scope":"document","inkLimit":300,"removeWhiteOverprint":true,"overprintBlack":100}}} +``` diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 428b38c75..e480a7525 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (3D Extrude & Bevel, Inflate and Rotate apply to type, which stays editable (#4715); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Color Effects add-on: Duotone and Print Ink Fixes; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -1282,12 +1282,21 @@ Tests: `addons::halftone` in `vectorcraft-effects` (settings, tone, caps), `addo `vectorcraft-render` (drawn as dots, settings change them, unchanged art not sampled again, too many cells draw the object as it is), `expand_appearance_gives_the_dots_object_vector_halftone_makes`, `exports_write_the_dots_as_paths` and `a_halftone_past_the_caps_leaves_the_object_as_it_is` in `vectorcraft-engine`, -and `the_halftone_effect_dialog_has_the_command_fields_and_edits_the_effect` in `vectorcraft-ui-egui`. Commands: -`docs/mcp.md` › Color Effects add-on. +and `the_halftone_effect_dialog_has_the_command_fields_and_edits_the_effect` in `vectorcraft-ui-egui`. Duotone +(`addon.color.duotone`) maps each colour's tone through one to four inks with their curves, a tint of a lone tone's +swatch (linked, so a spot tone separates on its plate) or the multiplied CMYK of several, live and through +`addon.color.filter`; Print Ink Fixes (`addon.color.printFixes`) limits total ink, removes white overprint and sets +black overprint in a CMYK document as one undo step. Tests: `addons::duotone::tests` in `vectorcraft-effects` (spot +tints, curves, multiplied inks, skip spot), `duotone_*` and `cmd::addons::color::print_fixes::tests` in +`vectorcraft-engine` (drawn and expanded, filter equals live then expand, ink limit, overprint, one undo step, bad +params and RGB documents refused), and `the_duotone_dialog_has_a_row_per_tone_and_the_curve_of_one` and +`print_ink_fixes_opens_its_options_and_runs_on_ok` in `vectorcraft-ui-egui`. Commands: `docs/mcp.md` › Color Effects +add-on. ## Revision history | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | Color Effects add-on: live Duotone (one to four inks, spot tones kept) and Print Ink Fixes; scores and estimates unchanged | | 2026-10-30 | minor | 3D Extrude & Bevel, Inflate and Rotate apply to type, which stays editable (#4715); scores and estimates unchanged | | 2026-10-30 | minor | Color Effects add-on: live Vector Halftone; scores and estimates unchanged | | 2026-10-30 | minor | Path Precision Tools add-on: the Parametric Shape Tool; evidence added; scores unchanged. | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 4f3ac445e..e8de7ef08 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Color Effects add-on: live Vector Halftone; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Color Effects add-on: Duotone and Print Ink Fixes; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -330,7 +330,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po | Smart Pen add-on (VectorCraft addition): Object › Plug-ins › Smart Pen selects the Smart Pen Tool, which the All Tools drawer lists under its own heading but neither toolbar shows as shipped (no shortcut), and opens the Smart Pen panel (next segment straight or curved; Remove Points Keep Shape, Make Smooth, Make Corner, Retract Handles, Close or Join). The tool draws (click: straight or curved segment, a curve after a straight one tangent; drag: smooth anchor, Alt breaks the handle, Shift 45°, Space moves it; Tab switches a segment; rubber band; close, join, continue) and edits without switching tools (click selects a point, drag moves it, Alt-click removes it keeping the shape, Shift+Alt-click deletes it, double-click toggles smooth/corner; handle drags with Shift and Alt; segment drags reshape, Alt-click adds a point); an Alt-drag away from paths selects the points inside a marquee | done | `addon.pen.*`, `crates/tools/src/addons/smart_pen.rs`, `crates/ui-egui/src/addons/pen.rs` and their tests; not an Illustrator feature, so it is not scored | | Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools selects its three tools (Anchor Eraser Brush, Extend Path, Measure & Inspect), which the All Tools drawer lists under their own heading but neither toolbar shows as shipped, opens the Anchor Inspector panel (the selected anchor's X/Y and handle lengths and angles as fields; Make Smooth, Make Corner, Equalize Handles, Remove Points Keep Shape, Add Points, the three ways to close) and runs Add Points at / Split at Crossings; a double-click on the brush or Extend Path tool opens its options | done | `addon.pathtools.*`, `crates/tools/src/addons/`, `crates/ui-egui/src/addons/pathtools.rs` and their tests; not an Illustrator feature, so it is not scored | | Artwork Cleanup add-on (VectorCraft addition): Object › Plug-ins › Artwork Cleanup opens its panel, which checks the selection or the document for 16 kinds of issue, lists them with counts, and shows or fixes the picked rows or all of them, each fix one undo step; Simplify Anchors (Keep Shape), Rejoin Path Pieces and Deep Ungroup open their options first | done | `addon.cleanup.*`, `crates/ui-egui/src/addons/cleanup.rs` and its tests; not an Illustrator feature, so it is not scored | -| Color Effects add-on (VectorCraft addition): live Exposure, Desaturate and Invert Colors in Effect › Color Effects and Object › Plug-ins › Color Effects; its panel picks a colour adjustment and its values and adds it as a live effect or applies it as a filter, into the colours, as one undo step; Select by Color Kind selects the objects that use spot colours, carry colour effects, or are RGB, CMYK or grayscale images; Vector Halftone draws an object as halftone dots live (the dots Object › Vector Halftone makes), which Expand Appearance and the exporters write as paths | done | `addon.color.*`, `crates/ui-egui/src/addons/color.rs` and its tests; not an Illustrator feature, so it is not scored | +| Color Effects add-on (VectorCraft addition): live Exposure, Desaturate and Invert Colors in Effect › Color Effects and Object › Plug-ins › Color Effects; its panel picks a colour adjustment and its values and adds it as a live effect or applies it as a filter, into the colours, as one undo step; Select by Color Kind selects the objects that use spot colours, carry colour effects, or are RGB, CMYK or grayscale images; Vector Halftone draws an object as halftone dots live (the dots Object › Vector Halftone makes), which Expand Appearance and the exporters write as paths; Duotone prints each colour's tone with one to four inks (a tint of a spot tone's swatch, or the CMYK mix of several), live or as a filter, its dialog a row per tone and the picked tone's curve; Print Ink Fixes (a CMYK document's ink limit, white overprint removed, black overprint set) opens its options from the submenu and the panel's Inks | done | `addon.color.*`, `crates/ui-egui/src/addons/color.rs` and its tests; not an Illustrator feature, so it is not scored | ## Look and feel @@ -371,6 +371,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | Color Effects add-on: live Duotone (one to four inks, spot tones kept) and Print Ink Fixes; scores and estimates unchanged | | 2026-10-30 | minor | Color Effects add-on: live Vector Halftone; scores and estimates unchanged | | 2026-10-30 | minor | Path Precision Tools add-on: the Parametric Shape Tool (ten re-editable shape kinds), its panel and Convert to Parametric Shape; scores and estimates unchanged | | 2026-10-30 | minor | Smart Pen add-on: an Alt-drag away from paths selects points with a marquee; scores and estimates unchanged |