diff --git a/crates/engine/src/cmd/swatchlib.rs b/crates/engine/src/cmd/swatchlib.rs index 98e3272d0..36c67f696 100644 --- a/crates/engine/src/cmd/swatchlib.rs +++ b/crates/engine/src/cmd/swatchlib.rs @@ -162,7 +162,7 @@ impl Libraries { 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 Libraries { 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 Libraries { 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 { diff --git a/crates/engine/src/tests_swatchlib.rs b/crates/engine/src/tests_swatchlib.rs index 6be4e4f5a..7b17fa586 100644 --- a/crates/engine/src/tests_swatchlib.rs +++ b/crates/engine/src/tests_swatchlib.rs @@ -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 { + fn acb_str(out: &mut Vec, s: &str) { + let units: Vec = 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};