mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 06:01:18 +00:00
Fix #6335: Color Settings are remembered across restarts
edit.colorSettings set only the process-wide colour settings, so the RGB and CMYK working spaces, intent and black point compensation went back to the defaults at every start, although the profiles color.loadProfile loads are kept (#2168, #2174). The engine now writes them to a settings file when they change (Session::attach_color_settings), and the desktop app and vectorcraft-cli keep that file as `Color Settings.json` next to the preferences, attached right after the profile store so a saved working space may be a loaded profile. Saved settings that can't be used (a profile that is gone, a damaged or oversized file) leave the defaults with a log record, and the next change writes over them. Nothing is read or written with VECTORCRAFT_NO_PREFS.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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")));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"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<Value> {
|
||||
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))
|
||||
|
||||
@@ -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<std::path::PathBuf>,
|
||||
/// The file Edit › Color Settings are kept in; `None`: only for the process
|
||||
/// ([`Session::attach_color_settings`]).
|
||||
color_settings_file: Option<std::path::PathBuf>,
|
||||
/// Where the plug-ins `plugin.install` installs are kept; `None`: only for the process
|
||||
/// ([`Session::attach_plugin_store`]).
|
||||
plugin_dir: Option<std::path::PathBuf>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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::<vectorcraft_color::cms::ColorSettings>(&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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
+6
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user