Add-ons: Color Effects — Duotone effect and Print Ink Fixes

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.
This commit is contained in:
bflatastic
2026-10-31 00:26:18 +07:00
parent 79fe17934a
commit ebc5b7038c
46 changed files with 1709 additions and 65 deletions
+3
View File
@@ -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;
+10 -4
View File
@@ -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<EffectInfo> {
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),
}
}
}
+178
View File
@@ -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<bool> + 'a;
/// [`SwatchKinds`] of `d`'s swatch `name`.
pub fn swatch_kind(d: &Document, name: &str) -> Option<bool> {
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<String, bool> {
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<String>,
/// 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<Tone>,
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<Self> {
let tones: Vec<Tone> = 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<String>, 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;
+128
View File
@@ -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<String>, 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]));
}
+83 -26
View File
@@ -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<Adjust>);
@@ -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<String>, 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<ColorMap> {
let v: Vec<Adjust> =
@@ -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<String> + '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<Node> {
/// 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<Node> {
let mut m: Option<Node> = 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<Node> {
}
_ => n,
};
let kinds = crate::addons::duotone::swatch_kinds(d);
let mut hook = |key: &str, map: &ColorMap| -> Option<String> {
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<String> = g.freeform.as_ref().unwrap().points.iter().map(|p| p.color.to_hex()).collect();
+21 -8
View File
@@ -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<CommandSpec> {
"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<Value> {
o.insert("ids".into(), json!(ids.iter().map(|i| i.0).collect::<Vec<_>>()));
}
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))
}
@@ -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<f32> {
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<Self> {
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<Value> {
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<String>, _: &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;
@@ -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<NodeId>) {
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<bool> = 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::<Vec<_>>(), [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}");
}
@@ -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);
}
+6
View File
@@ -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<Value> {
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<Value> {
let ids = match ids_param(p, "ids") {
+16
View File
@@ -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})),
+8 -5
View File
@@ -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);
+33 -3
View File
@@ -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<Item> {
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)]
+55 -2
View File
@@ -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::<Vec<_>>(), [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 %");
}
+1
View File
@@ -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;
+1
View File
@@ -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"],
_ => &[],
}
}
+123
View File
@@ -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<Value> = 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<String, Value>, 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<String, Value>) {
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));
}
}
});
}
+21 -12
View File
@@ -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<egui::Pos2> = (0..=64).map(|k| k as f32 / 64.0).map(|x| screen((x, vectorcraft_effects::curve_at(&pts, x)))).collect();
let curve: Vec<egui::Pos2> = (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<String> = 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::<Vec<_>>().join(" ")
}
/// What OK runs: `effect.setParams` on the edited effect (`__index`), else `effect.apply` of the
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 164 and column 5.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2169 and column 5.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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} にしてください
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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}
Can't render this file because it is too large.
+23
View File
@@ -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}이어야 합니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 6 and column 47.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 5 and column 25.
+23
View File
@@ -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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 178 and column 5.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 200 and column 5.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+23
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 6 and column 47.
+57
View File
@@ -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}}}
```
+12 -3
View File
@@ -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. |
+3 -2
View File
@@ -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 |