Merge pull request #4492 from dotCooCoo/fix/recolor-limit-and-scope-params

Recolor Artwork and swatch libraries: parameters of the wrong form are errors
This commit is contained in:
BFlat
2026-10-30 00:44:08 +07:00
committed by GitHub
31 changed files with 274 additions and 49 deletions
+9
View File
@@ -378,6 +378,15 @@ pub(crate) fn check_switches(p: &Value, keys: &[&str], cmd: &str) -> Result<()>
pub(crate) fn str_param<'a>(p: &'a Value, key: &str) -> Option<&'a str> {
p.get(key).and_then(Value::as_str)
}
/// The string `p` gives for `key`, or `None` when `p` leaves it out or gives null. Any other
/// value, such as `5`, is an error: "`{key}` must be a string".
pub(crate) fn text_param<'a>(p: &'a Value, key: &str, cmd: &str) -> Result<Option<&'a str>> {
match p.get(key) {
None | Some(Value::Null) => Ok(None),
Some(Value::String(v)) => Ok(Some(v)),
Some(_) => Err(bad(cmd, format!("`{key}` must be a string"))),
}
}
pub(crate) fn id_param(p: &Value, key: &str) -> Option<NodeId> {
p.get(key).and_then(Value::as_u64).map(NodeId)
}
+1 -1
View File
@@ -183,7 +183,7 @@ fn ids_json(ids: &[NodeId]) -> Value {
/// The whole number `p` gives for `key`, written `4` or `4.0`, or `default` when `p` gives none or null. A value that
/// is not a number gives the [`check_numbers`] error, and a fraction or a negative number gives
/// "`{key}` must be a whole number".
fn whole_or(p: &Value, key: &str, default: u64, cmd: &str) -> Result<u64> {
pub(crate) fn whole_or(p: &Value, key: &str, default: u64, cmd: &str) -> Result<u64> {
check_numbers(p, &[key], cmd)?;
let Some(v) = p.get(key).filter(|v| !v.is_null()) else { return Ok(default) };
match (v.as_u64(), v.as_f64()) {
+40 -19
View File
@@ -13,7 +13,9 @@ use vectorcraft_doc::swatches::{node_colors, node_paints};
use vectorcraft_doc::{Document, Node, NodeId, NodeKind};
use super::colorcmds::{Recolor, Scope};
use super::{CommandSpec, Result, Session, always, bad, bool_or, checked_ids_param, cmd, color_value, has_doc, index, str_param};
use super::{
CommandSpec, Result, Session, always, bad, check_switches, checked_ids_param, cmd, color_value, has_doc, index, str_param, switch_or, text_param,
};
pub fn specs() -> Vec<CommandSpec> {
vec![
@@ -152,15 +154,18 @@ fn colors(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "colors": v }))
}
/// The `method` parameter (`default` without one).
/// The `method` parameter (`default` without one, or for null). A `method` that is not a string,
/// such as `5`, names no method.
fn method_param(p: &Value, cmd: &str, default: Method) -> Result<Method> {
match str_param(p, "method") {
None => Ok(default),
Some(m) => Method::parse(m).ok_or_else(|| {
let ids: Vec<&str> = Method::ALL.iter().map(|m| m.id()).collect();
bad(cmd, format!("unknown method `{m}` ({})", ids.join("|")))
}),
}
let (m, named) = match p.get("method") {
None | Some(Value::Null) => return Ok(default),
Some(Value::String(m)) => (m.clone(), true),
Some(v) => (v.to_string(), false),
};
Method::parse(&m).filter(|_| named).ok_or_else(|| {
let ids: Vec<&str> = Method::ALL.iter().map(|m| m.id()).collect();
bad(cmd, format!("unknown method `{m}` ({})", ids.join("|")))
})
}
fn key_str(c: &Color) -> Value {
@@ -172,8 +177,17 @@ fn reduce(s: &mut Session, p: &Value) -> Result<Value> {
let method = method_param(p, C, Method::ScaleTints)?;
let palette = super::swatchlib::limit_param(s, p, C)?;
let def = Preserve::default();
let flag = |k: &str, d: bool| p.get("preserve").map_or(d, |v| bool_or(v, k, d));
let preserve = Preserve { white: flag("white", def.white), black: flag("black", def.black), grays: flag("grays", def.grays) };
let given = match p.get("preserve") {
None | Some(Value::Null) => &Value::Null,
Some(v @ Value::Object(_)) => v,
Some(_) => return Err(bad(C, "`preserve` must be an object")),
};
let flag = |k: &str, d: bool| switch_or(given, k, d, C);
let preserve = Preserve { white: flag("white", def.white)?, black: flag("black", def.black)?, grays: flag("grays", def.grays)? };
let colors = match p.get("colors") {
None | Some(Value::Null) => None,
Some(_) => Some(index(super::menucmds::whole_or(p, "colors", 0, C)?)),
};
let (ids, only) = scope_ids(s, p, C)?;
let st = s.doc()?;
let (kept, used): (Vec<Used>, Vec<Used>) = used_colors(&st.doc, &ids, only.as_ref()).into_iter().partition(|u| preserve.keeps(&u.color));
@@ -186,7 +200,7 @@ fn reduce(s: &mut Session, p: &Value) -> Result<Value> {
}
}
let unit_colors = |g: &[&Used]| g.iter().map(|u| u.color).collect::<Vec<_>>();
let rows: Vec<Vec<&Used>> = match p.get("colors").and_then(Value::as_u64).map(index) {
let rows: Vec<Vec<&Used>> = match colors {
Some(n) if n < units.len() => {
let keys: Vec<Color> = units.iter().map(|g| method.key_color(&unit_colors(g)).unwrap_or_default()).collect();
let weights: Vec<f32> = units.iter().map(|g| g.iter().map(|u| u.count as f32).sum()).collect();
@@ -275,7 +289,7 @@ fn parse_rows(p: &Value, cmd: &str) -> Result<Vec<Row>> {
Some(v) => vec![cur(v)?],
None => return Err(bad(cmd, "each row needs `from`")),
};
Ok(Row { from, to: new(r.get("to"))?, exclude: bool_or(r, "exclude", false) })
Ok(Row { from, to: new(r.get("to"))?, exclude: switch_or(r, "exclude", false, cmd)? })
})
.collect(),
_ => Err(bad(cmd, "missing `map` (rows [{from, to}] or {current: new})")),
@@ -419,10 +433,11 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
let row_swatches: Vec<Option<Swatch>> =
rows.iter().map(|r| r.new_color().and_then(|c| lib_swatches.iter().find(|w| w.paint.color() == Some(c)).cloned())).collect();
let ids = s.doc()?.selection.objects.clone();
let group = str_param(p, "group");
let group = text_param(p, "group", C)?;
if ids.is_empty() && group.is_none() {
return Err(bad(C, "select artwork to recolour, or give a colour `group`"));
}
check_switches(p, &["includeImages", "includePatterns"], C)?;
let scope = Scope { fill: true, stroke: true, ..Scope::of(p) };
let label = if ids.is_empty() { "Edit Color Group" } else { "Recolor Artwork" };
// `model`: keep each colour's model (default), take the new colour's own, or the document's.
@@ -448,7 +463,12 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
};
// The colours a colour group takes follow the model too (`document` converts them).
let in_doc_model = |c: Color| doc_model.map_or(c, |m| c.in_model(m));
let new: Vec<Color> = match p.get("groupColors").and_then(Value::as_array) {
let group_colors = match p.get("groupColors") {
None | Some(Value::Null) => None,
Some(Value::Array(cs)) => Some(cs),
Some(_) => return Err(bad(C, "`groupColors` must be an array")),
};
let new: Vec<Color> = match group_colors {
Some(cs) => cs
.iter()
.map(|v| {
@@ -459,7 +479,7 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
.collect::<Result<_>>()?,
None => rows.iter().filter_map(Row::new_color).collect(),
};
let rename = str_param(p, "rename");
let rename = text_param(p, "rename", C)?;
let (changed, edit, added, converted) = s.edit(label, |d, _| {
// The rows' library swatches join the document (one it has with that name and colour is
// used as it is).
@@ -518,12 +538,13 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
fn randomize(_: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "recolor.randomize";
let rows = parse_rows(p, C)?;
let mut rng = Rng::new(p.get("seed").and_then(Value::as_u64).unwrap_or(0));
let mut rng = Rng::new(super::menucmds::whole_or(p, "seed", 0, C)?);
let (order, saturation_brightness) = (switch_or(p, "order", true, C)?, switch_or(p, "saturationBrightness", false, C)?);
let mut news: Vec<Color> = rows.iter().filter_map(Row::new_color).collect();
if bool_or(p, "order", true) {
if order {
rng.shuffle(&mut news);
}
if bool_or(p, "saturationBrightness", false) {
if saturation_brightness {
news = news.into_iter().map(|c| rng.saturation_brightness(c)).collect();
}
let mut news = news.into_iter();
+13 -9
View File
@@ -14,9 +14,8 @@ use vectorcraft_doc::style_libs::{self, STYLE_LIBRARIES, STYLES_EXT, builtin_sty
use vectorcraft_doc::{Document, GraphicStyle, PatternDef, StyleLibrary};
use super::menucmds::squash;
use super::swatch::str_list;
use super::swatchlib::{FileData, LibraryFile, LibraryInfo, stem};
use super::{CommandSpec, Result, Session, always, bad, bool_or, cmd, has_doc, str_param, unique_name};
use super::swatchlib::{FileData, LibraryFile, LibraryInfo, names_param, stem};
use super::{CommandSpec, Result, Session, always, bad, checked_ids_param, cmd, has_doc, str_param, switch_or, text_param, unique_name};
pub fn specs() -> Vec<CommandSpec> {
vec![
@@ -154,8 +153,12 @@ fn adopt(d: &mut Document, lib: &StyleLibrary, g: &GraphicStyle, renamed: &mut H
fn add(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "graphicStyle.addFromLibrary";
let (info, lib) = library_param(s, p, C)?;
let mut names = str_list(p, "names");
names.extend(str_param(p, "name").map(str::to_string));
let (apply, add) = (switch_or(p, "apply", false, C)?, switch_or(p, "add", false, C)?);
if p.get("ids").is_some_and(|v| !v.is_null()) {
checked_ids_param(s, p, "ids", C)?;
}
let mut names = names_param(p, C)?;
names.extend(text_param(p, "name", C)?.map(str::to_string));
if let Some(n) = names.iter().find(|n| lib.style(n).is_none()) {
return Err(bad(C, format!("no graphic style `{n}` in `{}`", lib.name)));
}
@@ -192,8 +195,8 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
});
let first = picked.first().map(|g| doc_name(&g.name));
let targets = p.get("ids").is_some() || !super::appearance::appearance_targets(s, &json!({}))?.is_empty();
if let Some(name) = first.filter(|_| bool_or(p, "apply", false) && targets) {
let mut params = json!({"name": name, "add": bool_or(p, "add", false)});
if let Some(name) = first.filter(|_| apply && targets) {
let mut params = json!({"name": name, "add": add});
if let Some(ids) = p.get("ids") {
params["ids"] = ids.clone();
}
@@ -208,8 +211,9 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
fn save(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "graphicStyle.saveLibrary";
let st = s.doc()?;
let name = str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()).map_or_else(|| stem(&st.title()).to_string(), str::to_string);
let lib = StyleLibrary::from_document(&st.doc, &str_list(p, "names"), name).map_err(|e| bad(C, e))?;
let name = text_param(p, "name", C)?.map(str::trim).filter(|n| !n.is_empty()).map_or_else(|| stem(&st.title()).to_string(), str::to_string);
let names = names_param(p, C)?;
let lib = StyleLibrary::from_document(&st.doc, &names, name).map_err(|e| bad(C, e))?;
let mut out = json!({ "count": lib.len() });
s.style_libraries.write(p, &lib.name, STYLES_EXT, FileData::Text(style_libs::write(&lib)), &mut out, C)?;
Ok(out)
+39 -19
View File
@@ -18,7 +18,7 @@ use super::fileio::{create_dir, extension, read_file, write_file, write_new_file
use super::menucmds::squash;
use super::package::free_name;
use super::swatch::{map_default_paints, map_swatch_links, str_list, swatch_json};
use super::{Color, CommandSpec, Result, Session, always, bad, bool_or, cmd, has_doc, str_param};
use super::{Color, CommandSpec, Result, Session, always, bad, cmd, has_doc, switch_or, text_param};
pub fn specs() -> Vec<CommandSpec> {
vec![
@@ -201,7 +201,8 @@ impl<L: LibraryFile> Libraries<L> {
FileData::Text(text) => text.as_bytes(),
FileData::Binary(bytes) => bytes.as_slice(),
};
if bool_or(p, "user", false) {
let path = text_param(p, "path", cmd)?;
if switch_or(p, "user", false, cmd)? {
let dir = self
.user_dir()
.ok_or_else(|| bad(cmd, "no user library folder here (save with a path, or without one to get the data)"))?
@@ -213,7 +214,7 @@ impl<L: LibraryFile> Libraries<L> {
self.rescan();
out["path"] = json!(path);
out["library"] = json!(format!("user/{file}"));
} else if let Some(path) = str_param(p, "path") {
} else if let Some(path) = path {
write_file(path, bytes)?;
out["path"] = json!(path);
} else {
@@ -230,14 +231,14 @@ impl<L: LibraryFile> Libraries<L> {
/// or a file with the same extension and bytes as one there, is that User Defined library;
/// others are listed as loaded until the app quits.
pub(crate) fn load(&mut self, p: &Value, cmd: &str, parse: impl FnOnce(&[u8], &str) -> Result<L>) -> Result<(LibraryInfo, Arc<L>)> {
let path = str_param(p, "path");
let bytes = match (path, str_param(p, "data"), str_param(p, "dataBase64")) {
let path = text_param(p, "path", cmd)?;
let bytes = match (path, text_param(p, "data", cmd)?, text_param(p, "dataBase64", cmd)?) {
(Some(path), ..) => read_file(path)?,
(None, Some(text), _) => text.as_bytes().to_vec(),
(None, None, Some(b64)) => vectorcraft_format::base64_decode(b64).ok_or_else(|| bad(cmd, "bad dataBase64"))?,
_ => return Err(bad(cmd, "give `path`, `data` or `dataBase64`")),
};
let file = str_param(p, "name").map(str::to_string).or_else(|| path.map(file_name)).unwrap_or_else(|| "Library".into());
let file = text_param(p, "name", cmd)?.map(str::to_string).or_else(|| path.map(file_name)).unwrap_or_else(|| "Library".into());
let lib = parse(&bytes, &file)?;
self.rescan();
let user =
@@ -428,19 +429,36 @@ pub fn library_swatches(s: &Session, key: &str) -> Option<Vec<Swatch>> {
Some(library(s, key)?.1.iter().filter(own).cloned().collect())
}
/// The `limitTo` parameter's palette ([`limit_palette`]); `None` without one (or for "").
/// The `limitTo` parameter's palette ([`limit_palette`]); `None` without one (or for "" or null).
/// A `limitTo` that is not a string, such as `5`, names no library.
pub(crate) fn limit_param(s: &Session, p: &Value, cmd: &str) -> Result<Option<Palette>> {
let Some(key) = str_param(p, "limitTo").filter(|k| !k.is_empty()) else { return Ok(None) };
let palette = limit_palette(s, key)
.ok_or_else(|| bad(cmd, format!("no swatch library `{key}` (see swatch.library.list; \"{DOCUMENT_SWATCHES}\": the document's swatches)")))?;
let no_library =
|key: &str| bad(cmd, format!("no swatch library `{key}` (see swatch.library.list; \"{DOCUMENT_SWATCHES}\": the document's swatches)"));
let key = match p.get("limitTo") {
None | Some(Value::Null) => return Ok(None),
Some(Value::String(k)) if k.is_empty() => return Ok(None),
Some(Value::String(k)) => k.as_str(),
Some(v) => return Err(no_library(&v.to_string())),
};
let palette = limit_palette(s, key).ok_or_else(|| no_library(key))?;
if palette.is_empty() {
return Err(bad(cmd, format!("`{key}` has no colours")));
}
Ok(Some(palette))
}
/// The `names` parameter ([`str_list`]): empty when it is left out or null; anything but a list of
/// strings is an error.
pub(super) fn names_param(p: &Value, cmd: &str) -> Result<Vec<String>> {
match p.get("names") {
None | Some(Value::Null) => Ok(vec![]),
Some(Value::Array(a)) if a.iter().all(Value::is_string) => Ok(str_list(p, "names")),
Some(_) => Err(bad(cmd, "`names` must be a list of names")),
}
}
fn library_param(s: &Session, p: &Value, cmd: &str) -> Result<(LibraryInfo, Arc<SwatchLibrary>)> {
let key = str_param(p, "library").ok_or_else(|| bad(cmd, "missing `library` (see swatch.library.list)"))?;
let key = text_param(p, "library", cmd)?.ok_or_else(|| bad(cmd, "missing `library` (see swatch.library.list)"))?;
library(s, key).ok_or_else(|| bad(cmd, format!("no swatch library `{key}` (see swatch.library.list)")))
}
@@ -479,14 +497,15 @@ fn document_library(d: &vectorcraft_doc::Document, names: &[String], name: Strin
fn save(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "swatch.library.save";
let path = str_param(p, "path");
let format = match str_param(p, "format").or_else(|| path.map(|p| p.rsplit_once('.').map_or("", |(_, e)| e))).filter(|f| !f.is_empty()) {
let path = text_param(p, "path", C)?;
let format = match text_param(p, "format", C)?.or_else(|| path.map(|p| p.rsplit_once('.').map_or("", |(_, e)| e))).filter(|f| !f.is_empty()) {
Some(f) => PaletteFormat::parse(f).ok_or_else(|| bad(C, format!("unknown format `{f}` (vcswatches, gpl, ase or css)")))?,
None => PaletteFormat::Native,
};
let names = names_param(p, C)?;
let st = s.doc()?;
let name = str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()).map_or_else(|| stem(&st.title()).to_string(), str::to_string);
let lib = document_library(&st.doc, &str_list(p, "names"), name, C)?;
let name = text_param(p, "name", C)?.map(str::trim).filter(|n| !n.is_empty()).map_or_else(|| stem(&st.title()).to_string(), str::to_string);
let lib = document_library(&st.doc, &names, name, C)?;
let bytes = palette_io::write(&lib, format).map_err(|e| bad(C, e))?;
let data = if format.binary() { FileData::Binary(bytes) } else { FileData::Text(String::from_utf8_lossy(&bytes).into_owned()) };
let mut out = json!({"format": format.id(), "count": lib.iter().filter(|w| format.holds(&w.paint)).count()});
@@ -571,13 +590,14 @@ fn picked<'a>(lib: &'a SwatchLibrary, names: &[String], cmd: &str) -> Result<Vec
fn add(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "swatch.library.add";
let (info, lib) = library_param(s, p, C)?;
let apply = match str_param(p, "apply") {
let apply = match text_param(p, "apply", C)? {
None => None,
Some("fill") => Some("paint.setFill"),
Some("stroke") => Some("paint.setStroke"),
Some(other) => return Err(bad(C, format!("apply is \"fill\" or \"stroke\", not `{other}`"))),
};
let picked = picked(&lib, &str_list(p, "names"), C)?;
let focus = switch_or(p, "focus", true, C)?;
let picked = picked(&lib, &names_param(p, C)?, C)?;
// Swatches the document already has (same name and paint) are reused, not added again.
let d = &s.doc()?.doc;
let (existing, fresh): (Vec<_>, Vec<_>) = picked.iter().partition(|(_, w)| d.swatch(&w.name).is_some_and(|x| x.paint == w.paint));
@@ -622,7 +642,7 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
let mut out = json!({"library": info.id, "added": added.iter().map(|(_, n)| n).collect::<Vec<_>>(), "existing": existing});
if let (Some(cmd), Some((_, first))) = (apply, picked.first()) {
let name = added.iter().find(|(l, _)| *l == first.name).map_or(first.name.clone(), |(_, n)| n.clone());
s.execute(cmd, &json!({"swatch": name, "focus": bool_or(p, "focus", true)}))?;
s.execute(cmd, &json!({"swatch": name, "focus": focus}))?;
// Adding and applying undo together.
squash(s, undo_before, "Add to Swatches");
out["applied"] = json!(name);
@@ -631,7 +651,7 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
}
fn reset_defaults(s: &mut Session, p: &Value) -> Result<Value> {
let replace = bool_or(p, "replace", false);
let replace = switch_or(p, "replace", false, "swatch.resetDefaults")?;
let model = s.doc()?.doc.color_mode.model();
let (defaults, groups) = default_swatches(model);
// With replace, links to swatches that are no longer global ones are dropped (the art keeps
+13
View File
@@ -48,6 +48,19 @@ fn every_guide_colour_is_a_library_member() {
assert!(s.execute("color.harmony", &json!({"color": "#3a7bd5", "rule": "triad", "limitTo": "document"})).is_err(), "no document");
}
/// A `limitTo` that is not a string names no library: `color.harmony` refuses it with the error it
/// gives for a name it does not know, and reads a null as no limit.
#[test]
fn a_limit_that_is_not_a_string_names_no_library() {
let mut s = Session::new();
for (limit, shown) in [(json!(5), "`5`"), (json!(true), "`true`"), (json!(["web-safe-216"]), "`[\"web-safe-216\"]`")] {
let e = s.execute("color.harmony", &json!({"color": "#3a7bd5", "rule": "triad", "limitTo": limit})).unwrap_err().to_string();
assert!(e.contains(&format!("no swatch library {shown}")), "{limit}: {e}");
}
let free = s.execute("color.harmony", &json!({"color": "#3a7bd5", "rule": "triad", "limitTo": null})).unwrap();
assert_eq!(free["colors"][0], "#3a7bd5");
}
#[test]
fn document_swatches_limit_the_guide_and_recolor() {
let mut s = Session::new();
+48
View File
@@ -572,6 +572,54 @@ fn model_param_must_be_a_string_when_given() {
assert_eq!(fill(&s, art).to_hex(), "#1a1a1a", "nothing changed");
}
/// Recolor Artwork's commands refuse a parameter of the wrong form, such as `limitTo: 5` or
/// `includeImages: "false"`, with an error that names it; the art keeps its color and no undo
/// step is recorded. A null counts as a value left out.
#[test]
fn recolor_commands_refuse_params_of_the_wrong_form() {
let mut s = session();
let art = rect(&mut s, json!("#1a1a1a"));
select_all(&mut s);
let undo = |s: &Session| s.doc().unwrap().history.undo.len();
let before = undo(&s);
let map = json!({"#1a1a1a": "#ff0000"});
let rows = json!([{"from": ["#1a1a1a"], "to": "#ff0000", "exclude": "true"}]);
for (cmd, p, msg) in [
("recolor.apply", json!({"map": map, "limitTo": 5}), "no swatch library `5`"),
("recolor.apply", json!({"map": map, "limitTo": true}), "no swatch library `true`"),
("recolor.reduce", json!({"limitTo": 216}), "no swatch library `216`"),
("recolor.apply", json!({"map": map, "includeImages": "false"}), "`includeImages` must be true or false"),
("recolor.apply", json!({"map": map, "includePatterns": 0}), "`includePatterns` must be true or false"),
("recolor.apply", json!({"map": map, "method": 5}), "unknown method `5`"),
("recolor.reduce", json!({"method": true}), "unknown method `true`"),
("recolor.reduce", json!({"colors": "2"}), "`colors` must be a number"),
("recolor.reduce", json!({"colors": 2.5}), "`colors` must be a whole number"),
("recolor.reduce", json!({"preserve": "none"}), "`preserve` must be an object"),
("recolor.reduce", json!({"preserve": {"white": "false"}}), "`white` must be true or false"),
("recolor.apply", json!({"map": rows}), "`exclude` must be true or false"),
("recolor.apply", json!({"map": map, "group": 5}), "`group` must be a string"),
("recolor.apply", json!({"map": map, "rename": 5}), "`rename` must be a string"),
("recolor.apply", json!({"map": map, "groupColors": "#00ff00"}), "`groupColors` must be an array"),
("recolor.randomize", json!({"map": rows}), "`exclude` must be true or false"),
("recolor.randomize", json!({"map": map, "seed": "4"}), "`seed` must be a number"),
("recolor.randomize", json!({"map": map, "seed": -1}), "`seed` must be a whole number"),
("recolor.randomize", json!({"map": map, "order": "false"}), "`order` must be true or false"),
("recolor.randomize", json!({"map": map, "saturationBrightness": 1}), "`saturationBrightness` must be true or false"),
] {
let e = s.execute(cmd, &p).unwrap_err().to_string();
assert!(e.contains(msg), "{cmd} {p}: {e}");
}
assert_eq!(fill(&s, art).to_hex(), "#1a1a1a", "nothing changed");
assert_eq!(undo(&s), before, "no undo step");
let r = run(&mut s, "recolor.reduce", json!({"colors": null, "method": null, "limitTo": null, "preserve": null}));
assert_eq!(r["method"], "scaleTints");
let r = run(&mut s, "recolor.randomize", json!({"map": map, "seed": null, "order": null, "saturationBrightness": null}));
assert_eq!(r["map"][0]["to"], "rgb 255 0 0");
let p = json!({"map": map, "limitTo": null, "method": null, "includeImages": null, "includePatterns": null, "group": null, "rename": null, "groupColors": null});
run(&mut s, "recolor.apply", p);
assert_eq!(fill(&s, art).to_hex(), "#ff0000");
}
#[test]
fn document_model_converts_the_colours_of_a_group_too() {
let mut s = session();
+53
View File
@@ -162,6 +162,59 @@ fn save_and_load_round_trip_keeps_cmyk_spot_and_groups() {
assert!(s.execute("swatch.library.save", &json!({"user": true})).is_err(), "no user folder in a headless session");
}
/// The swatch library commands, and the graphic style library save and load that share their file
/// handling, refuse a parameter of the wrong form, such as `user: "true"` or `names: "Grays"`, with
/// an error that names it; the document keeps its swatches and no undo step is recorded. A null
/// counts as a value left out.
#[test]
fn library_commands_refuse_params_of_the_wrong_form() {
let mut s = session();
run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 50, "height": 50}));
let swatches = |s: &Session| doc(s).swatches_iter().map(|w| w.name.clone()).collect::<Vec<_>>();
let (names, before) = (swatches(&s), undo_len(&s));
let data = run(&mut s, "swatch.library.save", json!({"names": ["Grays"]}))["data"].clone();
for (cmd, p, msg) in [
("swatch.library.get", json!({"library": 5}), "`library` must be a string"),
("swatch.library.add", json!({"library": "earth-tones", "names": "Clay"}), "`names` must be a list of names"),
("swatch.library.add", json!({"library": "earth-tones", "names": ["Clay", 3]}), "`names` must be a list of names"),
("swatch.library.add", json!({"library": "earth-tones", "names": ["Clay"], "apply": 5}), "`apply` must be a string"),
("swatch.library.add", json!({"library": "earth-tones", "apply": "fill", "focus": "true"}), "`focus` must be true or false"),
("swatch.resetDefaults", json!({"replace": "true"}), "`replace` must be true or false"),
("swatch.library.save", json!({"user": "true"}), "`user` must be true or false"),
("swatch.library.save", json!({"path": 5}), "`path` must be a string"),
("swatch.library.save", json!({"format": 5}), "`format` must be a string"),
("swatch.library.save", json!({"name": 5}), "`name` must be a string"),
("swatch.library.save", json!({"names": "Grays"}), "`names` must be a list of names"),
("swatch.library.load", json!({"path": 5, "data": data}), "`path` must be a string"),
("swatch.library.load", json!({"data": 5}), "`data` must be a string"),
("swatch.library.load", json!({"dataBase64": 5}), "`dataBase64` must be a string"),
("swatch.library.load", json!({"data": data, "name": 5}), "`name` must be a string"),
("graphicStyle.saveLibrary", json!({"user": "true"}), "`user` must be true or false"),
("graphicStyle.saveLibrary", json!({"names": "Halo"}), "`names` must be a list of names"),
("graphicStyle.saveLibrary", json!({"name": 5}), "`name` must be a string"),
("graphicStyle.addFromLibrary", json!({"library": "shadows-glows", "names": "Halo"}), "`names` must be a list of names"),
("graphicStyle.addFromLibrary", json!({"library": "shadows-glows", "name": 5}), "`name` must be a string"),
("graphicStyle.addFromLibrary", json!({"library": "shadows-glows", "apply": "true"}), "`apply` must be true or false"),
("graphicStyle.addFromLibrary", json!({"library": "shadows-glows", "apply": true, "add": "true"}), "`add` must be true or false"),
("graphicStyle.addFromLibrary", json!({"library": "shadows-glows", "apply": true, "ids": "x"}), "ids must be an array"),
("graphicStyle.loadLibrary", json!({"path": 5, "data": "{}"}), "`path` must be a string"),
] {
let e = s.execute(cmd, &p).unwrap_err().to_string();
assert!(e.contains(msg), "{cmd} {p}: {e}");
}
assert_eq!((swatches(&s), undo_len(&s)), (names.clone(), before), "nothing changed");
let r = run(&mut s, "swatch.library.save", json!({"path": null, "format": null, "name": null, "names": null, "user": null}));
assert_eq!(r["format"], "vcswatches");
let r = run(&mut s, "swatch.library.load", json!({"path": null, "data": data, "dataBase64": null, "name": null}));
assert_eq!(r["count"], 9);
run(&mut s, "swatch.resetDefaults", json!({"replace": null}));
let r = run(&mut s, "swatch.library.add", json!({"library": "earth-tones", "names": null, "apply": null, "focus": null}));
assert!(r["added"].as_array().is_some_and(|a| !a.is_empty()) && r.get("applied").is_none(), "{r}");
run(&mut s, "graphicStyle.saveLibrary", json!({"name": null, "names": null}));
let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "shadows-glows", "name": null, "names": null}));
assert!(r["added"].as_array().is_some_and(|a| !a.is_empty()), "{r}");
}
#[test]
fn libraries_saved_in_the_user_folder_are_user_defined() {
let dir = temp_dir("user");
+2
View File
@@ -4717,6 +4717,7 @@
@msg `freeform.lines` must be an array `freeform.lines` ha de ser una matriu
@msg `freeform.points` must be an array `freeform.points` ha de ser una matriu
@msg `gradient` must be an object `gradient` ha de ser un objecte
@msg `groupColors` must be an array `groupColors` ha de ser una matriu
@msg `id` is the step reference {r}, which only `run` and `command_batch` resolve `id` és la referència del pas {r}, que només resolen `run` i `command_batch`
@msg `ids` must be an array of object ids, not {v} `ids` ha de ser una matriu d’identificadors d’objecte, no {v}
@msg `library` is a library id or name (see {list}) `library` és un identificador o un nom de biblioteca (vegeu {list})
@@ -4727,6 +4728,7 @@
@msg `name` is empty `name` és buit
@msg `newName` is empty `newName` és buit
@msg `pathfinder` must be an object `pathfinder` ha de ser un objecte
@msg `preserve` must be an object `preserve` ha de ser un objecte
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` ha de ser 1, 3 o 5
@msg `select` must be true or false `select` ha de ser cert o fals
@msg `stops` must be an array `stops` ha de ser una matriu
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4243,8 +4243,10 @@ imageTrace Outline Obrys
@msg `freeform.points` must be an array `freeform.points` musí být pole
@msg `freeform.lines` must be an array `freeform.lines` musí být pole
@msg `gradient` must be an object `gradient` musí být objekt
@msg `groupColors` must be an array `groupColors` musí být pole
@msg `library` is a library id or name (see {list}) `library` je ID nebo název knihovny (viz {list})
@msg `mode` applies to freeform gradients `mode` platí pro volné přechody
@msg `preserve` must be an object `preserve` musí být objekt
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` musí být 1, 3 nebo 5
@msg `stops` must be an array `stops` musí být pole
@msg `svg` must be an object of SVG options (see document.formats) `svg` musí být objekt s možnostmi SVG (viz document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3856,8 +3856,10 @@ theme Appearance Erscheinungsbild
@msg `freeform.points` must be an array `freeform.points` muss ein Array sein
@msg `freeform.lines` must be an array `freeform.lines` muss ein Array sein
@msg `gradient` must be an object `gradient` muss ein Objekt sein
@msg `groupColors` must be an array `groupColors` muss ein Array sein
@msg `library` is a library id or name (see {list}) `library` ist die ID oder der Name einer Bibliothek (siehe {list})
@msg `mode` applies to freeform gradients `mode` gilt für Freiformverläufe
@msg `preserve` must be an object `preserve` muss ein Objekt sein
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` muss 1, 3 oder 5 sein
@msg `stops` must be an array `stops` muss ein Array sein
@msg `svg` must be an object of SVG options (see document.formats) `svg` muss ein Objekt mit SVG-Optionen sein (siehe document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2174,8 +2174,10 @@ axis Both Και τα δύο
@msg `freeform.points` must be an array Το `freeform.points` πρέπει να είναι πίνακας
@msg `freeform.lines` must be an array Το `freeform.lines` πρέπει να είναι πίνακας
@msg `gradient` must be an object Το `gradient` πρέπει να είναι αντικείμενο
@msg `groupColors` must be an array Το `groupColors` πρέπει να είναι πίνακας
@msg `library` is a library id or name (see {list}) Το `library` είναι αναγνωριστικό ή όνομα βιβλιοθήκης (δείτε {list})
@msg `mode` applies to freeform gradients Το `mode` ισχύει για ντεγκραντέ ελεύθερης μορφής
@msg `preserve` must be an object Το `preserve` πρέπει να είναι αντικείμενο
@msg `sampleSize` must be 1, 3 or 5 Το `sampleSize` πρέπει να είναι 1, 3 ή 5
@msg `stops` must be an array Το `stops` πρέπει να είναι πίνακας
@msg `svg` must be an object of SVG options (see document.formats) Το `svg` πρέπει να είναι αντικείμενο επιλογών SVG (δείτε document.formats)
Can't render this file because it contains an unexpected character in line 164 and column 5.
+2
View File
@@ -3936,8 +3936,10 @@ axis Both Ambos
@msg `freeform.points` must be an array `freeform.points` debe ser una lista
@msg `freeform.lines` must be an array `freeform.lines` debe ser una lista
@msg `gradient` must be an object `gradient` debe ser un objeto
@msg `groupColors` must be an array `groupColors` debe ser una lista
@msg `library` is a library id or name (see {list}) `library` es el id o el nombre de una biblioteca (consulta {list})
@msg `mode` applies to freeform gradients `mode` se aplica a los degradados de forma libre
@msg `preserve` must be an object `preserve` debe ser un objeto
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` debe ser 1, 3 o 5
@msg `stops` must be an array `stops` debe ser una lista
@msg `svg` must be an object of SVG options (see document.formats) `svg` debe ser un objeto con opciones de SVG (consulta document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4158,8 +4158,10 @@ imageTrace Outline Ääriviiva
@msg `freeform.points` must be an array `freeform.points` on oltava taulukko
@msg `freeform.lines` must be an array `freeform.lines` on oltava taulukko
@msg `gradient` must be an object `gradient` on oltava objekti
@msg `groupColors` must be an array `groupColors` on oltava taulukko
@msg `library` is a library id or name (see {list}) `library` on kirjaston tunnus tai nimi (ks. {list})
@msg `mode` applies to freeform gradients `mode` koskee vapaamuotoisia liukuvärejä
@msg `preserve` must be an object `preserve` on oltava objekti
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` on oltava 1, 3 tai 5
@msg `stops` must be an array `stops` on oltava taulukko
@msg `svg` must be an object of SVG options (see document.formats) `svg` on oltava SVG-asetusten objekti (ks. document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3933,8 +3933,10 @@ axis Both Les deux
@msg `freeform.points` must be an array `freeform.points` doit être un tableau
@msg `freeform.lines` must be an array `freeform.lines` doit être un tableau
@msg `gradient` must be an object `gradient` doit être un objet
@msg `groupColors` must be an array `groupColors` doit être un tableau
@msg `library` is a library id or name (see {list}) `library` est l’identifiant ou le nom d’une bibliothèque (voir {list})
@msg `mode` applies to freeform gradients `mode` s’applique aux dégradés de forme libre
@msg `preserve` must be an object `preserve` doit être un objet
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` doit valoir 1, 3 ou 5
@msg `stops` must be an array `stops` doit être un tableau
@msg `svg` must be an object of SVG options (see document.formats) `svg` doit être un objet d’options SVG (voir document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4462,10 +4462,12 @@ imageTrace Outline Körvonal
@msg `freeform.lines` must be an array `freeform.lines`: tömb kell
@msg `freeform.points` must be an array `freeform.points`: tömb kell
@msg `gradient` must be an object `gradient`: objektum kell
@msg `groupColors` must be an array `groupColors`: tömb kell
@msg `id` is the step reference {r}, which only `run` and `command_batch` resolve az `id` lépéshivatkozás ({r}), ezt csak a `run` és a `command_batch` oldja fel
@msg `ids` must be an array of object ids, not {v} `ids`: objektumazonosítók tömbje kell, ez nem az: {v}
@msg `library` is a library id or name (see {list}) a `library` könyvtárazonosító vagy -név lehet (lásd: {list})
@msg `mode` applies to freeform gradients a `mode` a szabad formájú színátmenetekre vonatkozik
@msg `preserve` must be an object `preserve`: objektum kell
@msg `sampleSize` must be 1, 3 or 5 `sampleSize`: 1, 3 vagy 5 kell
@msg `stops` must be an array `stops`: tömb kell
@msg `svg` must be an object of SVG options (see document.formats) `svg`: SVG-beállításokat tartalmazó objektum kell (lásd: document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3976,8 +3976,10 @@ axis Both Entrambi
@msg `freeform.points` must be an array `freeform.points` deve essere un array
@msg `freeform.lines` must be an array `freeform.lines` deve essere un array
@msg `gradient` must be an object `gradient` deve essere un oggetto
@msg `groupColors` must be an array `groupColors` deve essere un array
@msg `library` is a library id or name (see {list}) `library` è l'id o il nome di una libreria (vedi {list})
@msg `mode` applies to freeform gradients `mode` si applica alle sfumature a forma libera
@msg `preserve` must be an object `preserve` deve essere un oggetto
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` deve essere 1, 3 o 5
@msg `stops` must be an array `stops` deve essere un array
@msg `svg` must be an object of SVG options (see document.formats) `svg` deve essere un oggetto di opzioni SVG (vedi document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4085,7 +4085,9 @@ swatch Bright Violet ビビッドバイオレット
@msg unknown freeform mode `{m}` (points, lines) `{m}` は不明なフリーフォームのモードです (points、lines)
@msg `aspect` must be a number (%) `aspect` には数値 (%) を指定してください
@msg `gradient` must be an object `gradient` にはオブジェクトを指定してください
@msg `groupColors` must be an array `groupColors` には配列を指定してください
@msg `mode` applies to freeform gradients `mode` はフリーフォームグラデーションにのみ適用されます
@msg `preserve` must be an object `preserve` にはオブジェクトを指定してください
@msg `stops` must be an array `stops` には配列を指定してください
@msg `{k}` must be [x, y] `{k}` は [x, y] の形式で指定してください
@msg a gradient needs at least two stops グラデーションには 2 つ以上の分岐点が必要です
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3729,8 +3729,10 @@ font-technology Variable ცვლადი
@msg `freeform.points` must be an array `freeform.points` უნდა იყოს მასივი
@msg `freeform.lines` must be an array `freeform.lines` უნდა იყოს მასივი
@msg `gradient` must be an object `gradient` უნდა იყოს ობიექტი
@msg `groupColors` must be an array `groupColors` უნდა იყოს მასივი
@msg `library` is a library id or name (see {list}) `library` არის ბიბლიოთეკის id ან სახელი (იხ. {list})
@msg `mode` applies to freeform gradients `mode` ეხება თავისუფალი ფორმის გრადიენტებს
@msg `preserve` must be an object `preserve` უნდა იყოს ობიექტი
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` უნდა იყოს 1, 3 ან 5
@msg `stops` must be an array `stops` უნდა იყოს მასივი
@msg `svg` must be an object of SVG options (see document.formats) `svg` უნდა იყოს SVG-ის პარამეტრების ობიექტი (იხ. document.formats)
Can't render this file because it is too large.
+2
View File
@@ -4017,7 +4017,9 @@ theme Appearance 외관
@msg unknown freeform mode `{m}` (points, lines) 알 수 없는 자유형 모드 `{m}`(points, lines)
@msg `aspect` must be a number (%) `aspect`는 숫자(%)여야 합니다
@msg `gradient` must be an object `gradient`는 개체여야 합니다
@msg `groupColors` must be an array `groupColors`는 배열이어야 합니다
@msg `mode` applies to freeform gradients `mode`는 자유형 그레이디언트에 적용됩니다
@msg `preserve` must be an object `preserve`는 개체여야 합니다
@msg `stops` must be an array `stops`는 배열이어야 합니다
@msg `{k}` must be [x, y] `{k}`은(는) [x, y]여야 합니다
@msg a gradient needs at least two stops 그레이디언트에는 정지점이 두 개 이상 필요합니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4067,8 +4067,10 @@ imageTrace Outline Kontur
@msg `freeform.points` must be an array `freeform.points` musi być tablicą
@msg `freeform.lines` must be an array `freeform.lines` musi być tablicą
@msg `gradient` must be an object `gradient` musi być obiektem
@msg `groupColors` must be an array `groupColors` musi być tablicą
@msg `library` is a library id or name (see {list}) `library` to identyfikator lub nazwa biblioteki (patrz {list})
@msg `mode` applies to freeform gradients `mode` dotyczy gradientów dowolnych
@msg `preserve` must be an object `preserve` musi być obiektem
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` musi wynosić 1, 3 lub 5
@msg `stops` must be an array `stops` musi być tablicą
@msg `svg` must be an object of SVG options (see document.formats) `svg` musi być obiektem opcji SVG (patrz document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3521,8 +3521,10 @@ axis Both Ambos
@msg `freeform.points` must be an array `freeform.points` deve ser uma matriz
@msg `freeform.lines` must be an array `freeform.lines` deve ser uma matriz
@msg `gradient` must be an object `gradient` deve ser um objeto
@msg `groupColors` must be an array `groupColors` deve ser uma matriz
@msg `library` is a library id or name (see {list}) `library` é o id ou o nome de uma biblioteca (veja {list})
@msg `mode` applies to freeform gradients `mode` se aplica a gradientes de forma livre
@msg `preserve` must be an object `preserve` deve ser um objeto
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` deve ser 1, 3 ou 5
@msg `stops` must be an array `stops` deve ser uma matriz
@msg `svg` must be an object of SVG options (see document.formats) `svg` deve ser um objeto com opções de SVG (veja document.formats)
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -3972,8 +3972,10 @@ axis Both Оба
@msg `freeform.points` must be an array `freeform.points` должно быть массивом
@msg `freeform.lines` must be an array `freeform.lines` должно быть массивом
@msg `gradient` must be an object `gradient` должно быть объектом
@msg `groupColors` must be an array `groupColors` должно быть массивом
@msg `library` is a library id or name (see {list}) `library` — это идентификатор или имя библиотеки (см. {list})
@msg `mode` applies to freeform gradients `mode` применяется к произвольным градиентам
@msg `preserve` must be an object `preserve` должно быть объектом
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` должно быть 1, 3 или 5
@msg `stops` must be an array `stops` должно быть массивом
@msg `svg` must be an object of SVG options (see document.formats) `svg` должно быть объектом параметров SVG (см. document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3853,8 +3853,10 @@ axis Both Båda
@msg `freeform.points` must be an array `freeform.points` måste vara en array
@msg `freeform.lines` must be an array `freeform.lines` måste vara en array
@msg `gradient` must be an object `gradient` måste vara ett objekt
@msg `groupColors` must be an array `groupColors` måste vara en array
@msg `library` is a library id or name (see {list}) `library` är ett biblioteks-id eller biblioteksnamn (se {list})
@msg `mode` applies to freeform gradients `mode` gäller för fria gradienter
@msg `preserve` must be an object `preserve` måste vara ett objekt
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` måste vara 1, 3 eller 5
@msg `stops` must be an array `stops` måste vara en array
@msg `svg` must be an object of SVG options (see document.formats) `svg` måste vara ett objekt med SVG-alternativ (se document.formats)
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -3742,8 +3742,10 @@ axis Both Her İkisi
@msg `freeform.points` must be an array "freeform.points" bir dizi olmalıdır
@msg `freeform.lines` must be an array "freeform.lines" bir dizi olmalıdır
@msg `gradient` must be an object "gradyan" bir nesne olmalıdır
@msg `groupColors` must be an array `groupColors` bir dizi olmalıdır
@msg `library` is a library id or name (see {list}) 'kitaplık' bir kitaplık kimliği veya adıdır (bkz. {list})
@msg `mode` applies to freeform gradients "mod" serbest biçimli degradeler için geçerlidir
@msg `preserve` must be an object `preserve` bir nesne olmalıdır
@msg `sampleSize` must be 1, 3 or 5 "sampleSize" 1, 3 veya 5 olmalıdır
@msg `stops` must be an array "duraklar" bir dizi olmalıdır
@msg `svg` must be an object of SVG options (see document.formats) `svg`, SVG seçeneklerinin bir nesnesi olmalıdır (bkz. document.formats)
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -3960,8 +3960,10 @@ axis Both Обидві
@msg `freeform.points` must be an array `freeform.points` має бути масивом
@msg `freeform.lines` must be an array `freeform.lines` має бути масивом
@msg `gradient` must be an object `gradient` має бути об’єктом
@msg `groupColors` must be an array `groupColors` має бути масивом
@msg `library` is a library id or name (see {list}) `library` — це ідентифікатор або ім’я бібліотеки (див. {list})
@msg `mode` applies to freeform gradients `mode` застосовується до довільних градієнтів
@msg `preserve` must be an object `preserve` має бути об’єктом
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` має бути 1, 3 або 5
@msg `stops` must be an array `stops` має бути масивом
@msg `svg` must be an object of SVG options (see document.formats) `svg` має бути об’єктом параметрів SVG (див. document.formats)
Can't render this file because it contains an unexpected character in line 178 and column 5.
+2
View File
@@ -4112,10 +4112,12 @@ theme Appearance Giao diện
@msg `freeform.lines` must be an array `freeform.lines` phải là một mảng
@msg `freeform.points` must be an array `freeform.points` phải là một mảng
@msg `gradient` must be an object `gradient` phải là một đối tượng
@msg `groupColors` must be an array `groupColors` phải là một mảng
@msg `id` is the step reference {r}, which only `run` and `command_batch` resolve `id` là tham chiếu bước {r}, chỉ `run` và `command_batch` giải quyết
@msg `ids` must be an array of object ids, not {v} `ids` phải là mảng id đối tượng, không phải {v}
@msg `library` is a library id or name (see {list}) `library` là id hoặc tên thư viện (xem {list})
@msg `mode` applies to freeform gradients `mode` áp dụng cho chuyển sắc tự do
@msg `preserve` must be an object `preserve` phải là một đối tượng
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` phải là 1, 3 hoặc 5
@msg `stops` must be an array `stops` phải là một mảng
@msg `svg` must be an object of SVG options (see document.formats) `svg` phải là đối tượng tùy chọn SVG (xem document.formats)
Can't render this file because it contains an unexpected character in line 200 and column 5.
+2
View File
@@ -3819,7 +3819,9 @@ imageTrace Outline 轮廓
@msg unknown freeform mode `{m}` (points, lines) 未知的任意形状模式 `{m}` (points、lines)
@msg `aspect` must be a number (%) `aspect` 必须是数字 (%)
@msg `gradient` must be an object `gradient` 必须是对象
@msg `groupColors` must be an array `groupColors` 必须是数组
@msg `mode` applies to freeform gradients `mode` 仅适用于任意形状渐变
@msg `preserve` must be an object `preserve` 必须是对象
@msg `stops` must be an array `stops` 必须是数组
@msg `{k}` must be [x, y] `{k}` 必须是 [x, y]
@msg a gradient needs at least two stops 渐变至少需要两个色标
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1763,7 +1763,9 @@ file-filter Templates 範本
@msg unknown freeform mode `{m}` (points, lines) 未知的任意形狀模式 `{m}` (points、lines)
@msg `aspect` must be a number (%) `aspect` 必須是數字 (%)
@msg `gradient` must be an object `gradient` 必須是物件
@msg `groupColors` must be an array `groupColors` 必須是陣列
@msg `mode` applies to freeform gradients `mode` 只適用於任意形狀漸層
@msg `preserve` must be an object `preserve` 必須是物件
@msg `stops` must be an array `stops` 必須是陣列
@msg `{k}` must be [x, y] `{k}` 必須是 [x, y]
@msg a gradient needs at least two stops 漸層至少需要兩個色標
Can't render this file because it contains an unexpected character in line 6 and column 47.
+15 -1
View File
@@ -538,7 +538,9 @@ Colors blends, inversions and Recolor Artwork keep each colour's model (a blend
Guide panel shows: the harmony rule's colours, base first, and per colour its row of `2·steps+1` variations
(shades, cool or muted on the left, tints, warm or vivid on the right). With `limitTo` (a swatch library id or name,
or `"document"` for the document's swatches) every colour snaps to the library's nearest colour (ΔE 2000), as the
panel's Limit to Library does (`ui.colorGuideLimit`); Recolor Artwork's `limitTo` takes the same values.
panel's Limit to Library does (`ui.colorGuideLimit`); Recolor Artwork's `limitTo` takes the same values. A `limitTo`
that is not a string, such as `5`, names no library and gets the same error as an unknown name; `null` or `""` sets no
limit.
Color Themes (local only: no online service) keeps five-colour themes in the preferences. `colorTheme.save
{colors: [...]}` saves up to five colours as they are, `colorTheme.save {color, rule}` saves the five-colour theme a
@@ -1835,6 +1837,13 @@ is linked to a swatch rewritten with another color keeps its row's new color, ev
linked to the swatch, as does art whose color the row converts into the art's own color model. Without `group`,
the swatch keeps its color and the recolored art is no longer linked to it.
The `recolor.*` commands refuse parameters of the wrong form. A `limitTo` or `method` that is not a string gets the
error for an unknown library or method. Other wrong-form parameters get an error that names them.
`includeImages`, `includePatterns`, a row's `exclude`, `order`, `saturationBrightness` and the `white`, `black` and
`grays` switches of `preserve` must be true or false; `colors` and `seed` must be whole numbers; `group` and `rename`
must be strings, `groupColors` must be an array and `preserve` must be an object. Each refusal leaves the document unchanged and records no undo step. A `null`
counts as a value left out except for `recolor.apply`'s `model`, which must be a string when given.
```json
{"name":"run_command","arguments":{"command":"recolor.reduce","params":{"colors":2,"preserve":{"grays":true}}}}
{"name":"run_command","arguments":{"command":"recolor.apply","params":{"map":[{"from":["cmyk 0 100 100 0","cmyk 0 40 40 0"],"to":"cmyk 100 50 0 0"}],"method":"scaleTints"}}}
@@ -1908,6 +1917,11 @@ app (a library file as it is; a document's swatches or a library loaded from `da
as `.vcswatches`) under a name no file there has, and lists it as User Defined from then on; until the app
quits, commands given its loaded id use the copy → `{library, name, count, path, copied}` (`copied: false`
when the folder already held the same file). Without a user library folder the command is disabled.
These commands refuse a parameter of the wrong form with an error that names it: `library`, `apply`, `path`, `data`,
`dataBase64`, `format` and `name` must be strings, `names` must be a list of names, and `focus`, `replace` and `user`
must be true or false. `graphicStyle.saveLibrary` checks `path`, `name`, `names` and `user` the same way,
`graphicStyle.addFromLibrary` checks `name`, `names`, `apply`, `add` and `ids` before it adds a style, and `graphicStyle.loadLibrary` checks `path`, `data`, `dataBase64` and `name`. A `null` for one of these parameters counts
as a value left out.
## Graphic style libraries
+1
View File
@@ -893,6 +893,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Swatches:** names follow the colour model and are unique; Swatch Options (process/spot, Global, Gray/RGB/HSB/CMYK/Web, live preview); editing a global or spot swatch recolours every linked fill, stroke and text run in one undo step; multi-select and delete with confirmation; New Swatch and New Color Group dialogs (a group from the selected artwork's colours); swatches in colour groups convert, separate and count like any other. The Swatches panel selects ranges and whole groups, has a find field, and drags swatches to reorder, regroup or paint art; Add Used Colors, Select All Unused, Merge, Ungroup and Sort by Kind; a CMYK document keeps applied colours in CMYK. Swatch libraries: nine generated colour libraries and five gradient libraries in a library panel, saved as `.vcswatches`, `.gpl`, swatch exchange (`.ase`) or CSS, and loaded from `.vcswatches`, `.gpl` and `.ase` files (`.ase` both ways: RGB, CMYK, Lab and Gray; global, spot and process; groups) and from color books (`.acb`, read only: the RGB, CMYK or Lab colors of a book such as a Pantone book a user owns, named with its prefix and suffix, spot unless it says process, #831); `swatch.library.copyToUser` copies a loaded library into the User Defined folder without replacing a file, and a file with the same extension and bytes as a User Defined library opens as that library. Tints of global and spot colours ("Name 40%") stay linked and separate as a percentage of their plate. Spot colours can be defined in Lab (Spot Colors options). A built-in [Registration] swatch prints on every plate.
Editing, renaming, deleting or merging a global swatch also changes the colors linked to it in graph designs, the art of brushes and the tile that pattern editing's Cancel puts back, in the same undo step as the rest of the art; Default Swatches with replace unlinks them there too.
- **Missing library files:** a persistent swatch or graphic style library panel whose file is gone at startup reopens and stays in the preferences, showing the file's path with Locate Library… (`ui.libraryPanel.locate`), which reads the file from where it is now into the same panel.
- **Swatch library and Recolor Artwork parameters:** the `swatch.library.*` commands, `swatch.resetDefaults`, the graphic style library save, load and add, the `recolor.*` commands and `color.harmony`'s `limitTo` refuse parameters of the wrong form, such as `names: "Grays"`, `limitTo: 5` or `includeImages: "false"`. Errors name the parameter, except that a `limitTo` that is not a string, such as `5`, gets an unknown-library error. A `null` counts as left out except for `recolor.apply`'s `model`, which must be a string when given.
- **Colour:** a Color Picker (colour field, H/S/B/R/G/B channel slider, HSB/RGB/Lab/CMYK fields, hex, web-only snapping, out-of-gamut correction); the Color panel shows a selected colour in its own model and, with nothing selected, the new-art defaults in the mode picked last, which it keeps between sessions; Alt-click paints the other proxy; proxies look through groups and show "?" for mixed paint; Invert, Complement, Apply Last Color (`,`) and Apply Last Gradient (`.`) keep the colour model; every paint command feeds the recent colours. The Color Guide has 21 harmony rules, Steps and Variation options and Limit to Library. Adjust Color Balance (its Global mode shifts tints) and Saturate preview live; Edit Colors and Recolor also reach meshes, images and patterns. Recolor Artwork has colour reduction, five recolour methods, presets, Limit to Library, a colour-wheel Edit tab and Edit Color Group; a local Color Themes panel saves five-colour themes. Fill/Stroke chips open swatch and mixer popovers (the Properties panel has the Control bar's Fill and Stroke chips, Stroke link and weight spinner, with nothing selected too), panels have Illustrator's Window menu keys (Align Shift+F7, Transform Shift+F8, Pathfinder Cmd+Shift+F9, Layers F7, Character Cmd+T…; every panel can be given one in Edit › Keyboard Shortcuts), and Eyedropper Options pick up and apply chosen attributes (Alt and Shift+Alt, image pixels). An ICC CMYK profile converts straight into the RGB working space (not through sRGB, so a wider space such as Wide Gamut RGB keeps colours sRGB can't hold) with the colour settings' black-point compensation, on the canvas and in RGB exports alike (the built-in Generic CMYK compensates both ways, so RGB black separated to a rich black displays as black again). Fills and strokes can overprint (Attributes panel, Overprint Black options), and CMYK documents blend in inks on screen, in PDF and when flattening. Document Color Mode converts the art, symbols, pattern tiles and swatches through the colour settings: Gray stays Gray, RGB greys separate into four-colour greys and a rich black as Illustrator does, or go on the black plate only with `grays: "black"` (also for `document.open {colorMode}`).
A Cmd/Ctrl-click on the Color panel's spectrum applies Complement and a Cmd/Ctrl+Shift-click applies Invert to the active Fill or Stroke (`paint.complement`, `paint.invert`); with Alt also held, the click recolors the inactive one, as the reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) list them.
View › Proof Setup › Color blindness – Tritanopia-type turns Proof Colors on and proofs the canvas with the tritanopia simulation of Brettel, Viénot and Mollon (1997). The Separations Preview panel's Proof Setup list offers the same target (`view.proofSetup {target: "tritanopia"}`). Grays keep their value.