Fix #3395: color book library tabs use file name

## Summary
Swatch library panels title imported and User Defined .acb color books from the file name (without the extension), matching Illustrator. Host temporary import paths still fall back to the book title when copying into the user library folder (#2996).

## Tests
- `color_book_tab_titles_use_the_file_name` covers user-folder scan, load by path, and copyToUser keeping the source file name.
- Existing `copying_a_color_book_from_a_temporary_path_uses_its_title` unchanged.

Closes storytold/vectorcraft#3395

Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-10-22 11:17:31 +07:00
committed by bflatastic
co-authored by nrq6993-a11y
parent 15b041be81
commit 3d4d34fc15
2 changed files with 59 additions and 5 deletions
+13 -5
View File
@@ -162,7 +162,7 @@ impl<L: LibraryFile> Libraries<L> {
let file = file_name(&path);
let read = || crate::file_access::unconfined(|| read_file(&path)).ok().and_then(|b| L::read(&b, stem(&file)).ok());
let Some(lib) = crate::guard::catch_panic(read).ok().flatten() else { continue };
let info = LibraryInfo { id: format!("user/{file}"), name: lib.name().to_string(), category: "user" };
let info = LibraryInfo { id: format!("user/{file}"), name: library_tab_name(&file, lib.name()), category: "user" };
self.extra.push(Extra { info, path: Some(path), lib: Arc::new(lib) });
}
}
@@ -245,7 +245,7 @@ impl<L: LibraryFile> Libraries<L> {
if let Some(e) = path.and_then(user) {
return Ok((e.info.clone(), e.lib.clone()));
}
let info = LibraryInfo { id: format!("loaded/{}", path.unwrap_or(&file)), name: lib.name().to_string(), category: "loaded" };
let info = LibraryInfo { id: format!("loaded/{}", path.unwrap_or(&file)), name: library_tab_name(&file, lib.name()), category: "loaded" };
let lib = Arc::new(lib);
self.extra.retain(|x| x.info.id != info.id);
self.extra.push(Extra { info: info.clone(), path: path.map(str::to_string), lib: lib.clone() });
@@ -278,9 +278,10 @@ impl<L: LibraryFile> Libraries<L> {
Some(p) => {
let ext = extension(&p);
let base = file_name(&p);
// Color books (.acb) name the tab from the book title inside the file; a sandbox
// copy of an import often lands as tmp….tmp.acb (#2996).
let file = if ext == "acb" || temporary_library_file_name(&base) { library_file_name(lib.name(), &ext) } else { base };
// A sandbox copy of an import often lands as tmp….tmp.acb (#2996): store it under the
// book title. A normal .acb path keeps its file name so the tab matches Illustrator
// (#3395).
let file = if temporary_library_file_name(&base) { library_file_name(lib.name(), &ext) } else { base };
(file, read_file(&p)?)
}
None => (library_file_name(lib.name(), ext), encode(&lib)?),
@@ -318,6 +319,13 @@ fn temporary_library_file_name(name: &str) -> bool {
lower.contains(".tmp.")
}
/// The name library panels and menus show: color books (.acb) use the file name without its
/// extension (Illustrator), except when the path is a host temporary (#2996), then the book title.
fn library_tab_name(file: &str, lib_name: &str) -> String {
let base = file_name(file);
if extension(&base) == "acb" && !temporary_library_file_name(&base) { stem(&base).to_string() } else { lib_name.to_string() }
}
/// The file name of library `name` with extension `ext`: characters file systems reject become
/// `-`, and dots and spaces at either end are dropped. A name with nothing left is `Library`.
pub fn library_file_name(name: &str, ext: &str) -> String {
+46
View File
@@ -468,6 +468,52 @@ fn copying_a_color_book_from_a_temporary_path_uses_its_title() {
let _ = std::fs::remove_dir_all(dir);
}
/// Color books in the user library folder and opened by path show the file name as the tab title
/// (#3395), not only the title stored inside the book.
#[test]
fn color_book_tab_titles_use_the_file_name() {
fn acb_bytes(title: &str) -> Vec<u8> {
fn acb_str(out: &mut Vec<u8>, s: &str) {
let units: Vec<u16> = s.encode_utf16().collect();
out.extend((units.len() as u32).to_be_bytes());
units.iter().for_each(|u| out.extend(u.to_be_bytes()));
}
let mut bytes = b"8BCB".to_vec();
bytes.extend(1u16.to_be_bytes());
bytes.extend(3000u16.to_be_bytes());
for s in [title, "PANTONE ", " C", ""] {
acb_str(&mut bytes, s);
}
bytes.extend(1u16.to_be_bytes());
bytes.extend(7u16.to_be_bytes());
bytes.extend(0u16.to_be_bytes());
bytes.extend(0u16.to_be_bytes());
acb_str(&mut bytes, "185");
bytes.extend(b"TST001");
bytes.extend([228, 0, 43]);
bytes.extend(b"spflspot");
bytes
}
let dir = temp_dir("acb-name");
let user = dir.join("user");
let elsewhere = dir.join("elsewhere");
std::fs::create_dir_all(&user).unwrap();
std::fs::create_dir_all(&elsewhere).unwrap();
let on_disk = user.join("PANTONE Solid Coated.acb");
std::fs::write(&on_disk, acb_bytes("Internal Book Title")).unwrap();
let named = elsewhere.join("Brand Colors.acb");
std::fs::write(&named, acb_bytes("Another Internal Title")).unwrap();
let mut s = session();
s.swatch_libraries.set_user_dir(Some(user.to_string_lossy().to_string()));
assert_eq!(run(&mut s, "swatch.library.get", json!({ "library": "user/PANTONE Solid Coated.acb" }))["name"], "PANTONE Solid Coated");
let loaded = run(&mut s, "swatch.library.load", json!({ "path": named.to_string_lossy() }))["library"].clone();
assert_eq!(run(&mut s, "swatch.library.get", json!({ "library": loaded }))["name"], "Brand Colors");
let r = run(&mut s, "swatch.library.copyToUser", json!({ "library": loaded }));
assert_eq!((r["library"].as_str(), r["name"].as_str(), r["copied"].as_bool()), (Some("user/Brand Colors.acb"), Some("Brand Colors"), Some(true)));
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};