mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 12:47:13 -06:00
Merge pull request #644 from dotCooCoo/feat/ase-import
Swatches: swatch exchange (.ase) files open as swatch libraries
This commit is contained in:
@@ -70,8 +70,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
|
||||
impl LibraryFile for StyleLibrary {
|
||||
const EXTS: &'static [&'static str] = &[STYLES_EXT];
|
||||
fn read(text: &str, stem: &str) -> std::result::Result<Self, String> {
|
||||
style_libs::read(text, stem)
|
||||
fn read(bytes: &[u8], stem: &str) -> std::result::Result<Self, String> {
|
||||
style_libs::read(&String::from_utf8_lossy(bytes), stem)
|
||||
}
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
|
||||
@@ -69,7 +69,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Other Library…",
|
||||
["Window", "Swatch Libraries"],
|
||||
None,
|
||||
"{path? | data?: file text | dataBase64?, name?: file name (default: the path's)} load a .vcswatches or .gpl library, or the swatches of any document VectorCraft opens (see document.formats), for the library panel (Window → Swatch Libraries lists it until the app quits) → {library: id, name, count}",
|
||||
"{path? | data?: file text | dataBase64?, name?: file name (default: the path's)} load a .vcswatches, .gpl or .ase (swatch exchange) library, or the swatches of any document VectorCraft opens (see document.formats), for the library panel (Window → Swatch Libraries lists it until the app quits) → {library: id, name, count}",
|
||||
always,
|
||||
load
|
||||
),
|
||||
@@ -98,23 +98,24 @@ struct Extra<L> {
|
||||
pub trait LibraryFile: Sized {
|
||||
/// The extensions of its files (lower case).
|
||||
const EXTS: &'static [&'static str];
|
||||
/// Read a library file whose name without the extension is `stem` (an unnamed library's name).
|
||||
fn read(text: &str, stem: &str) -> std::result::Result<Self, String>;
|
||||
/// Read the bytes of a library file whose name without the extension is `stem` (an unnamed
|
||||
/// library's name).
|
||||
fn read(bytes: &[u8], stem: &str) -> std::result::Result<Self, String>;
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
impl LibraryFile for SwatchLibrary {
|
||||
const EXTS: &'static [&'static str] = LIBRARY_EXTS;
|
||||
fn read(text: &str, stem: &str) -> std::result::Result<Self, String> {
|
||||
palette_io::read(text, stem)
|
||||
fn read(bytes: &[u8], stem: &str) -> std::result::Result<Self, String> {
|
||||
palette_io::read_bytes(bytes, stem)
|
||||
}
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
/// The extensions of library files [`palette_io::read`] reads.
|
||||
pub const LIBRARY_EXTS: &[&str] = &["vcswatches", "gpl"];
|
||||
/// The extensions of library files [`palette_io::read_bytes`] reads.
|
||||
pub const LIBRARY_EXTS: &[&str] = &["vcswatches", "gpl", "ase"];
|
||||
|
||||
impl<L: LibraryFile> Libraries<L> {
|
||||
pub fn user_dir(&self) -> Option<&str> {
|
||||
@@ -133,7 +134,7 @@ impl<L: LibraryFile> Libraries<L> {
|
||||
let Some(dir) = self.user_dir.clone() else { return };
|
||||
for path in library_files(&dir, L::EXTS) {
|
||||
let file = file_name(&path);
|
||||
let Some(lib) = read_file(&path).ok().and_then(|b| L::read(&String::from_utf8_lossy(&b), stem(&file)).ok()) else { continue };
|
||||
let Some(lib) = read_file(&path).ok().and_then(|b| L::read(&b, stem(&file)).ok()) else { continue };
|
||||
let info = LibraryInfo { id: format!("user/{file}"), name: lib.name().to_string(), category: "user" };
|
||||
self.extra.push(Extra { info, path: Some(path), lib: Arc::new(lib) });
|
||||
}
|
||||
@@ -351,12 +352,11 @@ fn load(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "swatch.library.load";
|
||||
let (info, lib) = s.swatch_libraries.load(p, C, |bytes, file| {
|
||||
// A library file, or a document whose swatches become the library.
|
||||
match std::str::from_utf8(bytes).ok().filter(|t| palette_io::sniff(t)) {
|
||||
Some(t) => palette_io::read(t, stem(file)).map_err(|e| bad(C, e)),
|
||||
None => {
|
||||
let doc = super::fileio::load(file, bytes).map_err(|e| bad(C, e.to_string()))?.doc;
|
||||
document_library(&doc, &[], stem(file).to_string(), C)
|
||||
}
|
||||
if palette_io::sniff_bytes(bytes) {
|
||||
palette_io::read_bytes(bytes, stem(file)).map_err(|e| bad(C, e))
|
||||
} else {
|
||||
let doc = super::fileio::load(file, bytes).map_err(|e| bad(C, e.to_string()))?.doc;
|
||||
document_library(&doc, &[], stem(file).to_string(), C)
|
||||
}
|
||||
})?;
|
||||
Ok(json!({"library": info.id, "name": info.name, "count": lib.len()}))
|
||||
@@ -417,12 +417,17 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
} else {
|
||||
s.edit("Add to Swatches", |d, _| {
|
||||
let mut out = vec![];
|
||||
// Each library group goes into one document group: the group of that name the document
|
||||
// already had, or one made here.
|
||||
let before = d.swatch_groups.len();
|
||||
let mut placed: Vec<(&str, usize)> = vec![];
|
||||
for (group, w) in fresh {
|
||||
let name = d.free_swatch_name(&w.name);
|
||||
let sw = Swatch { name: name.clone(), ..(*w).clone() };
|
||||
match group {
|
||||
Some(g) => {
|
||||
let i = match d.swatch_groups.iter().position(|x| x.name == *g) {
|
||||
let found = placed.iter().find(|(n, _)| *n == g).map(|&(_, i)| i);
|
||||
let i = match found.or_else(|| d.swatch_groups.iter().take(before).position(|x| x.name == *g)) {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
let name = d.free_swatch_name(g);
|
||||
@@ -430,6 +435,9 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
d.swatch_groups.len() - 1
|
||||
}
|
||||
};
|
||||
if found.is_none() {
|
||||
placed.push((g, i));
|
||||
}
|
||||
d.swatch_groups[i].swatches.push(sw);
|
||||
}
|
||||
None => d.swatches.push(sw),
|
||||
|
||||
@@ -197,6 +197,74 @@ fn another_documents_swatches_load_as_a_library() {
|
||||
assert!(s.execute("swatch.library.load", &json!({"data": "hello", "name": "x.txt"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swatch_exchange_files_load_add_and_list_as_user_libraries() {
|
||||
let bytes = vectorcraft_testkit::ase::sample();
|
||||
let mut s = session();
|
||||
let r = run(&mut s, "swatch.library.load", json!({"dataBase64": vectorcraft_format::base64_encode(&bytes), "name": "Brand.ase"}));
|
||||
assert_eq!((r["library"].as_str(), r["name"].as_str(), r["count"].as_u64()), (Some("loaded/Brand.ase"), Some("Brand"), Some(4)));
|
||||
let g = run(&mut s, "swatch.library.get", json!({"library": r["library"]}));
|
||||
let swatches = g["swatches"].as_array().unwrap();
|
||||
let kinds: Vec<(&str, bool, bool)> =
|
||||
swatches.iter().map(|w| (w["name"].as_str().unwrap(), w["global"].as_bool().unwrap(), w["spot"].as_bool().unwrap())).collect();
|
||||
assert_eq!(kinds, [("Sky", true, false), ("Ink", true, true), ("Mist", false, false), ("Clay", false, false)]);
|
||||
assert_eq!(swatches[3]["color"], json!({"model": "lab", "l": 50.0, "a": 20.0, "b": -30.0}));
|
||||
// Added to the document, the spot color stays a spot color and the group stays a color group.
|
||||
run(&mut s, "swatch.library.add", json!({"library": r["library"]}));
|
||||
let d = doc(&s);
|
||||
assert!(d.swatch("Ink").is_some_and(|w| w.spot && w.global));
|
||||
assert_eq!(d.swatch_groups.iter().find(|g| g.name == "Neutrals").map(|g| g.swatches.len()), Some(2));
|
||||
// A damaged file is an error from the swatch exchange reader, not from the document loader.
|
||||
let cut = vectorcraft_format::base64_encode(&bytes[..bytes.len() - 1]);
|
||||
let e = s.execute("swatch.library.load", &json!({"dataBase64": cut, "name": "Cut.ase"})).unwrap_err().to_string();
|
||||
assert!(e.contains("cut short"), "{e}");
|
||||
// `.ase` files in the user library folder are User Defined libraries.
|
||||
let dir = temp_dir("ase");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("Brand.ase"), &bytes).unwrap();
|
||||
s.swatch_libraries.set_user_dir(Some(dir.to_string_lossy().to_string()));
|
||||
let list = run(&mut s, "swatch.library.list", json!({}));
|
||||
let user: Vec<(&str, u64)> = list["libraries"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|l| l["category"] == "user")
|
||||
.map(|l| (l["id"].as_str().unwrap(), l["count"].as_u64().unwrap()))
|
||||
.collect();
|
||||
assert_eq!(user, [("user/Brand.ase", 4)]);
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_library_group_stays_one_group_when_a_swatch_has_its_name() {
|
||||
use vectorcraft_testkit::ase::{Block, ase};
|
||||
let mut s = session();
|
||||
assert!(doc(&s).swatch("Blue").is_some(), "a new document has a swatch named Blue");
|
||||
let bytes = ase(&[
|
||||
Block::GroupStart("Blue"),
|
||||
Block::Color("Deep", b"RGB ", &[0.0, 0.0, 0.4], 2),
|
||||
Block::Color("Pale", b"RGB ", &[0.6, 0.7, 1.0], 2),
|
||||
Block::GroupEnd,
|
||||
Block::GroupStart("Blue 2"),
|
||||
Block::Color("Frost", b"RGB ", &[0.9, 0.95, 1.0], 2),
|
||||
Block::GroupEnd,
|
||||
]);
|
||||
let r = run(&mut s, "swatch.library.load", json!({"dataBase64": vectorcraft_format::base64_encode(&bytes), "name": "Blues.ase"}));
|
||||
run(&mut s, "swatch.library.add", json!({"library": r["library"]}));
|
||||
let groups: Vec<(&str, Vec<&str>)> =
|
||||
doc(&s).swatch_groups.iter().map(|g| (g.name.as_str(), g.swatches.iter().map(|w| w.name.as_str()).collect())).collect();
|
||||
assert!(groups.contains(&("Blue 2", vec!["Deep", "Pale"])), "{groups:?}");
|
||||
assert!(groups.contains(&("Blue 2 2", vec!["Frost"])), "the library's own Blue 2 stays apart: {groups:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_libraries_sent_as_bytes_load_when_they_are_not_valid_utf8() {
|
||||
let mut s = session();
|
||||
let latin1 = vectorcraft_format::base64_encode(b"GIMP Palette\n237 28 36 Rouge fonc\xe9\n");
|
||||
let r = run(&mut s, "swatch.library.load", json!({"dataBase64": latin1, "name": "Rouge.gpl"}));
|
||||
assert_eq!(r["count"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradient_libraries_are_listed_and_gradient_swatches_rename_and_take_new_gradients() {
|
||||
let mut s = session();
|
||||
|
||||
Reference in New Issue
Block a user