diff --git a/apps/vectorcraft-cli/src/main.rs b/apps/vectorcraft-cli/src/main.rs index 734f8a056..57b3d4122 100644 --- a/apps/vectorcraft-cli/src/main.rs +++ b/apps/vectorcraft-cli/src/main.rs @@ -236,11 +236,12 @@ fn main() -> ExitCode { } /// A headless session that remembers what the user keeps between runs: the PDF presets saved, the -/// ICC profiles loaded, the plug-ins installed (#2168, #2174, #2541) and the custom dictionary's words (#4936), in the folders +/// ICC profiles loaded, the plug-ins installed (#2168, #2174, #2541), the custom dictionary's words (#4936) and the Color Settings (#6335), in the folders /// and files next to the preferences that the desktop app uses too. `VECTORCRAFT_NO_PREFS` and `VECTORCRAFT_CONFIG_DIR` apply /// as they do there. fn with_stores(mut h: Headless) -> Headless { prefs_dir::attach_profile_store(&mut h.session); + prefs_dir::attach_color_settings(&mut h.session); prefs_dir::attach_plugin_store(&mut h.session); prefs_dir::attach_pdf_preset_store(&mut h.session); prefs_dir::attach_custom_dictionary(&mut h.session); diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs index 7e4f245d3..b97ee681f 100644 --- a/apps/vectorcraft/src/main.rs +++ b/apps/vectorcraft/src/main.rs @@ -456,6 +456,8 @@ fn main() -> std::process::ExitCode { let mut session = Session::new(); // The profiles loaded before come back before the preferences name any of them. prefs_dir::attach_profile_store(&mut session); + // Then Edit › Color Settings, which may name one of those profiles. + prefs_dir::attach_color_settings(&mut session); // Before any plug-in loads: this launch's marker in the Launch folder next to the // preferences (none for runs without preferences), and Safe Mode. let launch_dir = prefs_path().filter(|_| prefs_enabled()).and_then(|p| Some(p.parent()?.join("Launch"))); diff --git a/apps/vectorcraft/src/prefs_dir.rs b/apps/vectorcraft/src/prefs_dir.rs index 8e6402703..063d9b1aa 100644 --- a/apps/vectorcraft/src/prefs_dir.rs +++ b/apps/vectorcraft/src/prefs_dir.rs @@ -41,6 +41,14 @@ pub fn attach_profile_store(session: &mut vectorcraft_engine::Session) { } } +/// Make Edit › Color Settings kept in `Color Settings.json` the active ones (#6335), and keep them +/// there when they change. After [`attach_profile_store`]: they may name a loaded profile. +pub fn attach_color_settings(session: &mut vectorcraft_engine::Session) { + if let Some(path) = store_dir("Color Settings.json") { + session.attach_color_settings(path); + } +} + /// Install the plug-ins Install Plug-in kept in the `Plug-ins` folder (#2541), and keep the ones /// installed from now on there. pub fn attach_plugin_store(session: &mut vectorcraft_engine::Session) { diff --git a/crates/engine/src/cmd/colormgmt.rs b/crates/engine/src/cmd/colormgmt.rs index d9b5784b6..03b171de7 100644 --- a/crates/engine/src/cmd/colormgmt.rs +++ b/crates/engine/src/cmd/colormgmt.rs @@ -32,7 +32,7 @@ pub fn specs() -> Vec { "Color Settings…", ["Edit"], Some("Cmd+Shift+K"), - "{rgb?: profile, cmyk?: profile, intent?: \"perceptual\"|\"relative\"|\"saturation\"|\"absolute\", bpc?: bool} set the working spaces → {rgb, cmyk, intent, bpc, profiles: [{name, kind, builtin}]}", + "{rgb?: profile, cmyk?: profile, intent?: \"perceptual\"|\"relative\"|\"saturation\"|\"absolute\", bpc?: bool} set the working spaces → {rgb, cmyk, intent, bpc, profiles: [{name, kind, builtin}]}. The desktop app and vectorcraft-cli keep them in `Color Settings.json` next to the preferences and use them again at the next start (not with VECTORCRAFT_NO_PREFS); saved settings naming a profile that is gone leave the defaults", always, color_settings ), @@ -192,6 +192,8 @@ fn color_settings(s: &mut Session, p: &Value) -> Result { st.bpc = switch_or(p, "bpc", st.bpc, C)?; if st != before { cms::set_active(&st).map_err(|e| bad(C, e.to_string()))?; + // Saved for the next start when the app attached a settings file (#6335). + s.store_color_settings(&st); touch_all(s); } Ok(settings_json(&st)) diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index d18afc87e..abde09e82 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -155,6 +155,9 @@ pub struct Session { /// Where the ICC profiles `color.loadProfile` loads are kept; `None`: only for the process /// ([`Session::attach_profile_store`]). profile_dir: Option, + /// The file Edit › Color Settings are kept in; `None`: only for the process + /// ([`Session::attach_color_settings`]). + color_settings_file: Option, /// Where the plug-ins `plugin.install` installs are kept; `None`: only for the process /// ([`Session::attach_plugin_store`]). plugin_dir: Option, diff --git a/crates/engine/src/session/documents.rs b/crates/engine/src/session/documents.rs index 8a1ef8f69..66525c9af 100644 --- a/crates/engine/src/session/documents.rs +++ b/crates/engine/src/session/documents.rs @@ -82,6 +82,7 @@ impl Session { pdf_preset_dir: None, dictionary_file: None, profile_dir: None, + color_settings_file: None, plugin_dir: None, ignored_plugin_grants: Default::default(), plugin_output: None, diff --git a/crates/engine/src/store.rs b/crates/engine/src/store.rs index ae98bdf4f..f6fe8ff5b 100644 --- a/crates/engine/src/store.rs +++ b/crates/engine/src/store.rs @@ -11,6 +11,9 @@ //! * **ICC profiles**: the bytes of each profile `color.loadProfile` registered, as `.icc` files //! named after the profile, never the path they came from (a profile in a system folder may //! move or go away). Attaching the folder registers them again. +//! * **Color Settings**: the working spaces, intent and black point compensation +//! `edit.colorSettings` sets, as one JSON file (#6335). Attaching it, after the profiles, makes +//! them the active settings again. //! * **Plug-ins**: the module of each plug-in `plugin.install` installs, as a `.wasm` file named //! after its id (as Illustrator keeps plug-ins in its Plug-ins folder); `plugin.remove` deletes it. //! Attaching the folder installs them again, except in Safe Mode and except the ones the user @@ -293,6 +296,31 @@ impl Session { } } + /// Keep Edit › Color Settings (the working spaces, intent and black point compensation) in the + /// file `path`, and make the settings saved there the active ones (#6335). After + /// [`Session::attach_profile_store`]: they may name a profile it registers. Settings that can't be + /// read, or that name a profile that is gone, leave the defaults with a log record, and the file + /// as it is until the settings change. + pub fn attach_color_settings(&mut self, path: PathBuf) { + if path.exists() { + let restored = read_capped(&path, MAX_PRESET_FILE) + .and_then(|bytes| serde_json::from_slice::(&bytes).map_err(|e| e.to_string())) + .and_then(|settings| vectorcraft_color::cms::set_active(&settings).map_err(|e| e.to_string())); + if let Err(e) = restored { + log::warn!("not using the saved color settings {}: {e}", path.display()); + } + } + self.color_settings_file = Some(path); + } + + /// Write `settings` to the file [`Session::attach_color_settings`] named; `false` when they + /// didn't go there. + pub(crate) fn store_color_settings(&self, settings: &vectorcraft_color::cms::ColorSettings) -> bool { + let Some(path) = &self.color_settings_file else { return false }; + let (Some(dir), Some(file)) = (path.parent(), path.file_name().and_then(|f| f.to_str())) else { return false }; + serde_json::to_vec_pretty(settings).is_ok_and(|bytes| put(dir, file, &bytes)) + } + /// Keep the plug-ins this session installs in `dir`, take the disabled plug-ins saved there /// ([`DISABLED_FILE`]), and install the stored ones ([`Session::load_stored_plugins`]). pub fn attach_plugin_store(&mut self, dir: PathBuf) { diff --git a/crates/engine/src/tests/store.rs b/crates/engine/src/tests/store.rs index 37eafe6e4..44494aa53 100644 --- a/crates/engine/src/tests/store.rs +++ b/crates/engine/src/tests/store.rs @@ -202,3 +202,66 @@ fn stored_profiles_register_again_and_removing_one_removes_its_copy() { assert!(s.execute("color.removeProfile", &json!({"name": cms::SRGB})).is_err(), "built-in profiles stay"); s.execute("color.removeProfile", &json!({"name": other})).unwrap(); } + +/// #6335: Edit › Color Settings come back in the next session: the working spaces (a loaded +/// profile among them, registered again by the profile store first), the intent and black point +/// compensation. A session nobody attached a file to keeps nothing, and nothing is written before +/// the settings change. +#[test] +fn color_settings_come_back_in_the_next_session() { + let _g = super::colormgmt::GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); + let root = fresh_dir("color-settings"); + let (profiles, file) = (root.join("Color Profiles"), root.join("Color Settings.json")); + let loaded = "StoreRGB-6"; + let icc = vectorcraft_testkit::temp_dir("store-color-settings").join("loaded.icc"); + std::fs::write(&icc, icc_named(loaded)).unwrap(); + + let mut a = Session::new(); + a.attach_profile_store(profiles.clone()); + a.attach_color_settings(file.clone()); + a.execute("color.loadProfile", &json!({"path": icc.to_str().unwrap()})).unwrap(); + a.execute("edit.colorSettings", &json!({})).unwrap(); + assert!(!file.exists(), "nothing to keep before the settings change"); + a.execute("edit.colorSettings", &json!({"rgb": loaded, "cmyk": cms::DEVICE_CMYK, "intent": "perceptual", "bpc": false})).unwrap(); + let chosen = cms::ColorSettings { rgb: loaded.into(), cmyk: cms::DEVICE_CMYK.into(), intent: cms::Intent::Perceptual, bpc: false }; + assert_eq!(cms::active_settings(), chosen); + + // The next start begins with the defaults. + cms::set_active(&cms::ColorSettings::default()).unwrap(); + let mut b = Session::new(); + b.attach_profile_store(profiles); + b.attach_color_settings(file.clone()); + assert_eq!(cms::active_settings(), chosen, "the saved settings are the active ones again"); + + // Without a file, a change is kept only for the process. + let mut c = Session::new(); + c.execute("edit.colorSettings", &json!({"bpc": true})).unwrap(); + let saved: cms::ColorSettings = serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap(); + assert_eq!(saved, chosen, "the file is left alone"); + + cms::set_active(&cms::ColorSettings::default()).unwrap(); + b.execute("color.removeProfile", &json!({"name": loaded})).unwrap(); +} + +/// #6335: saved Color Settings that can't be used (a profile that is gone, a damaged or oversized +/// file) leave the defaults and the file as it is; the next change writes over it. +#[test] +fn saved_color_settings_that_cannot_be_used_leave_the_defaults() { + let _g = super::colormgmt::GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); + let file = fresh_dir("color-settings-bad").join("Color Settings.json"); + std::fs::create_dir_all(file.parent().unwrap()).unwrap(); + let gone = json!({"rgb": cms::SRGB, "cmyk": "A Profile That Is Gone", "intent": "perceptual", "bpc": false}).to_string().into_bytes(); + let oversized = vec![b' '; MAX_PRESET_FILE as usize + 1]; + for bytes in [gone, b"not json".to_vec(), oversized] { + cms::set_active(&cms::ColorSettings::default()).unwrap(); + std::fs::write(&file, &bytes).unwrap(); + let mut s = Session::new(); + s.attach_color_settings(file.clone()); + assert_eq!(cms::active_settings(), cms::ColorSettings::default()); + assert_eq!(std::fs::read(&file).unwrap(), bytes, "left as it is"); + s.execute("edit.colorSettings", &json!({"bpc": false})).unwrap(); + let saved: cms::ColorSettings = serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap(); + assert!(!saved.bpc && saved.cmyk == cms::GENERIC_CMYK, "written over by the next change: {saved:?}"); + } + cms::set_active(&cms::ColorSettings::default()).unwrap(); +} diff --git a/docs/mcp.md b/docs/mcp.md index 566f2e368..9bbd39eb1 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1114,7 +1114,7 @@ before anything runs, with the step's number and its entry (`entry 3`) or line ( ### What a headless session remembers -`vectorcraft-cli run` and the headless `mcp` server keep two things between runs, in folders next to the +`vectorcraft-cli run` and the headless `mcp` server keep these between runs, in folders and files next to the preferences (the folder the desktop app uses, so both see the same ones; `VECTORCRAFT_CONFIG_DIR` moves it): - **Saved PDF presets** (`pdf.preset.save`, `.delete`, `.import`): one `.vcpdfpresets` file per preset in @@ -1125,8 +1125,12 @@ preferences (the folder the desktop app uses, so both see the same ones; `VECTOR command, so `edit.colorSettings {"cmyk": "…"}` or a preset's output intent can name it in a later run. The copy is what counts: the original may move or be deleted. `color.removeProfile {name}` forgets a profile and its copy (not while it is a working space). +- **Color Settings** (`edit.colorSettings`): the RGB and CMYK working spaces, intent and black point + compensation go into `Color Settings.json` when they change, and are the active settings again at the next + start, after the profiles above are registered (#6335). Saved settings that name a profile that is no longer + there, or a damaged file, leave the defaults (logged), and the next change writes over the file. -Not remembered: Color Settings, the document, anything else. Nothing is read or written when +Not remembered: the document, anything else. Nothing is read or written when `VECTORCRAFT_NO_PREFS` is set, and a server confined with `--automation-read-root` or `--automation-write-root` keeps nothing either (the folders are outside its roots). A file that is damaged, too large (a profile over 32 MB, a presets file over 1 MB) or not a valid preset or profile is skipped and logged. When the