diff --git a/crates/color/src/palette_io.rs b/crates/color/src/palette_io.rs index fa193273..b37463b9 100644 --- a/crates/color/src/palette_io.rs +++ b/crates/color/src/palette_io.rs @@ -57,6 +57,19 @@ impl PaletteFormat { pub fn binary(self) -> bool { self == PaletteFormat::Ase } + /// Does [`write`] keep a swatch with `paint` in a file of this format? None and patterns are + /// never written; `.gpl` and `.ase` hold solid colors only, and CSS holds no freeform gradients. + pub fn holds(self, paint: &Paint) -> bool { + match paint { + Paint::Solid { .. } => true, + Paint::Gradient(g) => match self { + PaletteFormat::Native => true, + PaletteFormat::Css => g.gradient.kind != GradientKind::Freeform, + PaletteFormat::Gpl | PaletteFormat::Ase => false, + }, + Paint::None | Paint::Pattern { .. } => false, + } + } } /// The native file: a header around the library. @@ -792,6 +805,23 @@ mod tests { assert!(read(&text, "x").is_err(), "CSS is written only"); } + #[test] + fn holds_counts_the_swatches_each_format_writes() { + // `sample()` holds the spot color Ink, the linear gradient Fade, the pattern Tiles and two solid colors in a group. + let mut lib = sample(); + let Paint::Gradient(mut mesh) = lib.swatches[1].paint.clone() else { panic!("Fade is a gradient") }; + mesh.gradient.kind = GradientKind::Freeform; + lib.swatches.push(Swatch { name: "Mesh".into(), paint: Paint::Gradient(mesh), global: false, spot: false }); + lib.swatches.push(Swatch { name: "[None]".into(), paint: Paint::None, global: false, spot: false }); + let held = |f: PaletteFormat| lib.iter().filter(|w| f.holds(&w.paint)).count(); + assert_eq!(PaletteFormat::ALL.map(held), [5, 3, 3, 4], "vcswatches, gpl, ase, css"); + for f in [PaletteFormat::Native, PaletteFormat::Gpl, PaletteFormat::Ase] { + assert_eq!(read_bytes(&write(&lib, f).unwrap(), "x").unwrap().len(), held(f), "{f:?}"); + } + let css = written(&lib, PaletteFormat::Css); + assert_eq!(css.lines().filter(|l| l.starts_with(" --")).count(), held(PaletteFormat::Css), "one property each: {css}"); + } + /// A swatch exchange file of `blocks` (type, body). fn ase(blocks: &[(u16, Vec)]) -> Vec { let mut out = b"ASEF\0\x01\0\0".to_vec(); @@ -1088,5 +1118,15 @@ mod tests { prop_assert!(same_swatches(&g.swatches, &ase_expected(&want.swatches)), "{}: {:?}", g.name, g.swatches); } } + + /// A written `.vcswatches`, `.gpl` or `.ase` file reads back with as many swatches as + /// [`PaletteFormat::holds`] counts. + #[test] + fn written_files_read_back_with_the_swatches_holds_counts(lib in arb_library()) { + for f in [PaletteFormat::Native, PaletteFormat::Gpl, PaletteFormat::Ase] { + let back = read_bytes(&write(&lib, f).unwrap(), &lib.name).unwrap(); + prop_assert_eq!(back.len(), lib.iter().filter(|w| f.holds(&w.paint)).count(), "{:?}", f); + } + } } } diff --git a/crates/engine/src/cmd/swatchlib.rs b/crates/engine/src/cmd/swatchlib.rs index 9c1a9298..33adf24f 100644 --- a/crates/engine/src/cmd/swatchlib.rs +++ b/crates/engine/src/cmd/swatchlib.rs @@ -63,7 +63,7 @@ pub fn specs() -> Vec { "Save Swatch Library…", ["Window", "Swatches"], None, - "{path?, format?: \"vcswatches\" (lossless JSON: colour models, global, spot, gradients, groups) | \"gpl\" (8-bit RGB palette; groups as `# Group:` comments) | \"ase\" (swatch exchange, binary: solid colors in their own model (RGB, CMYK, Lab, Gray) as global, spot or process colors, and color groups; gradients are left out, and a tint swatch is saved as the color it shows) | \"css\" (custom properties on :root) (default: the path's extension, else vcswatches), names?: [swatch or colour group names] (default: all; None and patterns are never saved), name?: library name (default: the document's), user?: false (save into the user library folder, listed under User Defined)} save the document's swatches as a library → {path, format, count, library?: id when saved to the user folder}; without path or user → {data: the file's text, format, count}, or {dataBase64, format, count} for ase", + "{path?, format?: \"vcswatches\" (lossless JSON: colour models, global, spot, gradients, groups) | \"gpl\" (8-bit RGB palette; groups as `# Group:` comments) | \"ase\" (swatch exchange, binary: solid colors in their own model (RGB, CMYK, Lab, Gray) as global, spot or process colors, and color groups; gradients are left out, and a tint swatch is saved as the color it shows) | \"css\" (custom properties on :root) (default: the path's extension, else vcswatches), names?: [swatch or colour group names] (default: all; None and patterns are never saved), name?: library name (default: the document's), user?: false (save into the user library folder, listed under User Defined)} save the document's swatches as a library → {path, format, count: the swatches written (gpl and ase hold solid colors only, and css leaves freeform gradients out), library?: id when saved to the user folder}; without path or user → {data: the file's text, format, count}, or {dataBase64, format, count} for ase", has_doc, save ), @@ -454,7 +454,7 @@ fn save(s: &mut Session, p: &Value) -> Result { let lib = document_library(&st.doc, &str_list(p, "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.len()}); + let mut out = json!({"format": format.id(), "count": lib.iter().filter(|w| format.holds(&w.paint)).count()}); s.swatch_libraries.write(p, &lib.name, format.id(), data, &mut out, C)?; Ok(out) } diff --git a/crates/engine/src/tests_swatchlib.rs b/crates/engine/src/tests_swatchlib.rs index 68f1d8ec..0783e159 100644 --- a/crates/engine/src/tests_swatchlib.rs +++ b/crates/engine/src/tests_swatchlib.rs @@ -283,6 +283,26 @@ fn swatch_exchange_files_save_with_models_kinds_and_groups() { let _ = std::fs::remove_dir_all(dir); } +#[test] +fn save_counts_the_swatches_the_file_holds() { + let mut s = session(); + // A new document has 46 solid colors and 4 linear or radial gradients besides None. Mesh is a freeform gradient. + run(&mut s, "swatch.new", json!({"name": "Mesh", "gradient": {"kind": "freeform"}})); + let mut counts = vec![]; + for format in ["vcswatches", "gpl", "ase", "css"] { + let r = run(&mut s, "swatch.library.save", json!({ "format": format })); + // The libraries load back with that many swatches, and the CSS file has one property for each. + let held = if format == "css" { + r["data"].as_str().unwrap().lines().filter(|l| l.starts_with(" --")).count() as u64 + } else { + let load = json!({"data": r["data"], "dataBase64": r["dataBase64"], "name": format!("Saved.{format}")}); + run(&mut s, "swatch.library.load", load)["count"].as_u64().unwrap() + }; + counts.push((format, r["count"].as_u64().unwrap(), held)); + } + assert_eq!(counts, [("vcswatches", 51, 51), ("gpl", 46, 46), ("ase", 46, 46), ("css", 50, 50)]); +} + #[test] fn loaded_libraries_copy_into_the_user_folder_without_replacing_files() { use vectorcraft_testkit::ase::{Block, ase, sample}; diff --git a/docs/mcp.md b/docs/mcp.md index b49165ce..249b9c9a 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -931,7 +931,8 @@ groups; `.gpl` is 8-bit RGB; a swatch exchange `.ase` file keeps solid colors in (RGB, CMYK, Lab or Gray) as global, spot or process colors, and color groups, writes a tint swatch as the color it shows and leaves gradients out; CSS writes custom properties); without `path` it returns `{data}`, or `{dataBase64}` for `.ase`, and `user: true` saves into the user library -folder of the desktop app (listed as category `user`, User Defined). `swatch.library.load {path? | data? | +folder of the desktop app (listed as category `user`, User Defined). The reply's `count` is the number of swatches +written: `.gpl` and `.ase` hold solid colors only, and CSS leaves freeform gradients out. `swatch.library.load {path? | data? | dataBase64?, name?}` loads a `.vcswatches`, `.gpl`, swatch exchange (`.ase`) or color book (`.acb`) file, or another document's swatches, as a library to add from. From an `.ase` file it reads RGB, CMYK, Lab and Gray colors as global, spot or process swatches and keeps their color groups. A color book (such as a Pantone book you own) gives its RGB,