Color: assigned profiles follow the document in front

The profiles Edit > Assign Profile gives a document stayed the active
color settings after another document came to the front, so a new,
opened or switched-to document drew and converted its colors through the
previous document's profile, and closing the last document left it in
use.

The session now puts the front document's profiles in use (the working
spaces where it has none, or no document is open) after a new or opened
document, a tab switch, a close, File > Revert, every command including
each batch step and Undo or Redo of Assign Profile, a failed batch or
plug-in call, a script's rollback and slice boundaries, a canceled
preview, undo group or undo scope, and for each document Export All Open
and the update of placed documents work in. The active settings are
rebuilt, and the documents redrawn, only when they change.
This commit is contained in:
dotCooCoo
2026-11-13 07:44:19 -08:00
parent 77970c6924
commit 5741c7bba0
13 changed files with 156 additions and 10 deletions
+4 -1
View File
@@ -132,8 +132,10 @@ fn export_all_open(s: &mut Session, p: &Value) -> Result<Value> {
let active = s.active;
for i in 0..s.docs.len() {
// Each document in turn is the one Save a Copy writes; switching tabs this way leaves every
// document's tool and interaction alone, and the active one comes back below.
// document's tool and interaction alone, and the active one comes back below. Its colors
// are written through its own profiles.
s.active = Some(i);
s.sync_front_profiles();
let Ok(st) = s.doc() else { continue };
let title = st.doc.title.clone();
let stem = fileio::file_stem(st.path.as_deref().unwrap_or(&title));
@@ -160,6 +162,7 @@ fn export_all_open(s: &mut Session, p: &Value) -> Result<Value> {
}
}
s.active = active;
s.sync_front_profiles();
Ok(json!({ "files": files, "failed": failed, "format": format.id }))
}
+6 -4
View File
@@ -5,9 +5,11 @@
//! Colour settings and the proof view are process-wide (`vectorcraft_color::cms::active`,
//! `vectorcraft_render::proof::view`). A document's assigned profiles are stored in
//! `Document::color_profiles` (files before format v3: `Document.unknown["colorProfiles"]`) and
//! take the place of the working spaces in the active settings when assigned, and when the working
//! settings change while the document is in front ([`use_front_profiles`]); the working spaces stay
//! as Edit → Color Settings set them.
//! take the place of the working spaces in the active settings while the document is in front
//! ([`use_front_profiles`]): when they are assigned, when the working settings change, and when the
//! document comes to the front or its assignment comes back (an open, a switch, a close, a revert,
//! an undo or a rollback, [`Session::sync_front_profiles`]); the working spaces stay as Edit → Color
//! Settings set them.
use std::collections::BTreeMap;
use std::sync::Arc;
@@ -181,7 +183,7 @@ fn touch_all(s: &mut Session) {
/// Make the active settings the working settings with the profiles the front document is assigned
/// in place of the working spaces (none, or no document open: the working spaces). The open
/// documents redraw when the active settings change.
fn use_front_profiles(s: &mut Session) {
pub(crate) fn use_front_profiles(s: &mut Session) {
let (rgb, cmyk) = s.active().map(|d| doc_profiles(&d.doc)).unwrap_or_default();
if cms::use_document_profiles(rgb.as_deref(), cmyk.as_deref()) {
touch_all(s);
+1
View File
@@ -41,6 +41,7 @@ pub(super) fn batch(s: &mut Session, p: &Value) -> Result<Value> {
roll_back(s, start, &stash);
s.notify_text_tool("batch.rollBack");
s.active = active;
s.sync_front_profiles();
(s.paint, s.fill_active, s.draw_mode, s.draw_inside, s.clipboard, s.untitled_counter, s.explicit_undo_group) = saved;
s.pending_paint = None;
return Err(e);
+4 -1
View File
@@ -1331,14 +1331,17 @@ pub(crate) fn refresh_placed(s: &mut Session, saved: u64, path: &str) {
if targets.is_empty() {
return;
}
// Each document is updated as the active one, without switching tools.
// Each document is updated as the active one, through its own profiles, without switching
// tools.
let back = s.active;
for i in targets {
s.active = Some(i);
s.sync_front_profiles();
// A file that can't be read leaves what its objects show: links.check reports it.
let _ = update_where(s, None, Some(&path));
}
s.active = back;
s.sync_front_profiles();
}
// ---------- relative paths ----------
+1
View File
@@ -413,6 +413,7 @@ pub(crate) fn transaction(s: &mut Session, label: &str, f: impl FnOnce(&mut Sess
st.interaction = None;
}
saved.restore(s);
s.sync_front_profiles();
Err(e)
}
}
+5
View File
@@ -672,6 +672,7 @@ impl Savepoint {
s.pending_paint = self.pending_paint;
self.choices.put(s);
s.journal.truncate(self.journal);
s.sync_front_profiles();
exact
}
}
@@ -2199,6 +2200,9 @@ fn failure_text(report: &Value) -> String {
fn run_slice(s: &mut Session, job: &mut Job) -> std::result::Result<std::result::Result<Progress, RunError>, String> {
let depth = std::mem::replace(&mut s.depth, 0);
job.world.swap(s);
// The run's front document draws and converts through its own profiles, and the interface's
// again after the slice.
s.sync_front_profiles();
s.script_in_run = true;
let cell = job.host.session.clone();
std::mem::swap(s, &mut *cell.borrow_mut());
@@ -2221,6 +2225,7 @@ fn run_slice(s: &mut Session, job: &mut Job) -> std::result::Result<std::result:
s.script_in_run = false;
job.world.swap(s);
s.depth = depth;
s.sync_front_profiles();
r
}
+16 -1
View File
@@ -220,6 +220,7 @@ impl Session {
self.active = Some(index);
if switched {
self.sync_front_composite_fonts();
self.sync_front_profiles();
}
true
} else {
@@ -227,6 +228,15 @@ impl Session {
}
}
/// Make the profiles the document in front is assigned the ones colors are drawn and converted
/// through, in place of the working spaces (with no document open, or none assigned: the working
/// spaces), after a switch, an open, a close, a revert or a restored document brought another
/// document or another assignment to the front. The open documents redraw when the active color
/// settings change.
pub(crate) fn sync_front_profiles(&mut self) {
cmd::colormgmt::use_front_profiles(self);
}
/// Register the composite fonts of the document now in front ([`cmd::compositefonts::sync`]),
/// after a switch or a close brought it there. Its type set in a composite font that differs
/// from the one of that name before (added, changed or removed) is measured again, and type not
@@ -316,6 +326,7 @@ impl Session {
let i = self.docs.len() - 1;
self.active = Some(i);
self.sync_opened_composite_fonts(i);
self.sync_front_profiles();
if self.depth == 0
&& let Some(label) = self.explicit_undo_group.clone()
{
@@ -384,7 +395,10 @@ impl Session {
}
match back_names {
Some(names) => self.refresh_document_text_bounds(index, |t| t.cached_bounds.is_none() && !sets_in_any(t, &names)),
None => self.sync_opened_composite_fonts(index),
None => {
self.sync_opened_composite_fonts(index);
self.sync_front_profiles();
}
}
true
}
@@ -403,6 +417,7 @@ impl Session {
}
self.active = if self.docs.is_empty() { None } else { Some(index.min(self.docs.len() - 1)) };
self.sync_front_composite_fonts();
self.sync_front_profiles();
true
}
+4
View File
@@ -311,6 +311,8 @@ impl Session {
self.paint = paint.clone();
}
self.pending_paint = None;
// The previous preview's Assign Profile is undone with it.
self.sync_front_profiles();
let rw = cmd::distortcmds::perspective_rewrite(self, cmd, params).or_else(|| cmd::bounds_rewrite(self, cmd, params));
let (cmd, params) = rw.as_ref().map_or((cmd, params), |(c, p)| (c.as_str(), p));
// A selected object's converted appearance must not replace the current paints while
@@ -436,6 +438,8 @@ impl Session {
st.undo_interaction();
// Guides locked and slices locked or hidden during the interaction stay deselected.
deselect_hidden(&menu, &st.doc, &mut st.selection);
// A canceled preview's Assign Profile is undone with it.
self.sync_front_profiles();
// With New Art Has Basic Appearance off, new art takes the appearance of the selection as
// the document has it again, not as a preview left it.
self.inherit_new_art();
+4
View File
@@ -168,6 +168,10 @@ impl Session {
let (doc, journal, grouped) = (self.active().map(|d| d.uid), self.journal.len(), self.undo_group_docs());
let open = (self.depth == 0).then(|| self.docs.iter().map(|d| d.uid).collect::<Vec<_>>());
let r = if self.depth == 0 { self.run_guarded(id, run) } else { self.run_nested(run) };
// Colors are drawn and converted through the profiles of the document in front now, also
// after an undone or redone Assign Profile, a failed or panicked command's documents put
// back, and a batch step, before the next step converts through them.
self.sync_front_profiles();
if r.is_err() && self.depth == 0 {
self.batch_paint_docs.clear();
// A failed or panicked command's documents are put back: type is drawn in their
+6
View File
@@ -164,6 +164,10 @@ impl Session {
if self.depth == 0 {
self.trim_histories();
}
// A canceled group's Assign Profile is undone with it.
if cancel {
self.sync_front_profiles();
}
}
/// When journal entry `n` is a `command.batch` with `steps` noted in canceled undo groups and
@@ -280,5 +284,7 @@ impl Session {
g.journal.retain(|&(n, _)| n < scope.journal);
g.steps.retain(|&(n, _)| n < scope.journal);
}
// A canceled scope's Assign Profile is undone with it.
self.sync_front_profiles();
}
}
+100
View File
@@ -180,6 +180,106 @@ fn a_batch_converts_through_the_profile_it_assigns() {
assert_eq!(converted, fill(&device, b));
}
/// The profiles a document is assigned are in use while it is in front: a new or opened document
/// assigned none draws through the working spaces, switching back to the assigned document puts its
/// profile in use again, and closing a document puts the profiles of the document left in front, or
/// with none open the working spaces, in use.
#[test]
fn assigned_profiles_follow_the_front_document() {
let _g = settings();
let dir = fresh_dir("front-profiles");
let _reset = ColorSettingsReset { profile: None, paths: vec![dir.clone()] };
std::fs::create_dir_all(&dir).unwrap();
let tagged = dir.join("tagged.vectorcraft");
let mut s = session();
let working = cms::working_settings();
assert_ne!(working.cmyk, cms::DEVICE_CMYK);
exec(&mut s, "edit.assignProfile", json!({"cmyk": cms::DEVICE_CMYK}));
exec(&mut s, "file.saveAs", json!({"path": tagged.to_str().unwrap()}));
exec(&mut s, "file.new", json!({"width": 100, "height": 100}));
assert_eq!(cms::active_settings(), working, "a new document draws through the working spaces");
exec(&mut s, "document.activate", json!({"index": 0}));
assert_eq!(cms::active_settings().cmyk, cms::DEVICE_CMYK, "back in front, the document draws through its profile");
assert!(s.set_active(1));
assert_eq!(cms::active_settings(), working, "a tab switch to the untagged document");
exec(&mut s, "file.close", json!({}));
assert_eq!(cms::active_settings().cmyk, cms::DEVICE_CMYK, "the document left in front draws through its profile");
exec(&mut s, "file.close", json!({}));
assert_eq!(cms::active_settings(), working, "no document open: the working spaces");
exec(&mut s, "document.open", json!({"path": tagged.to_str().unwrap()}));
assert_eq!(cms::active_settings().cmyk, cms::DEVICE_CMYK, "an opened document draws through its saved profile");
}
/// Undo and Redo of Assign Profile, and File › Revert to a file assigned no profile, put the
/// profiles the document has then in use.
#[test]
fn undo_redo_and_revert_put_the_document_profiles_in_use() {
let _g = settings();
let dir = fresh_dir("undo-profiles");
let _reset = ColorSettingsReset { profile: None, paths: vec![dir.clone()] };
std::fs::create_dir_all(&dir).unwrap();
let untagged = dir.join("untagged.vectorcraft");
let mut s = session();
let working = cms::working_settings();
exec(&mut s, "file.saveAs", json!({"path": untagged.to_str().unwrap()}));
exec(&mut s, "edit.assignProfile", json!({"cmyk": cms::DEVICE_CMYK}));
assert_eq!(cms::active_settings().cmyk, cms::DEVICE_CMYK);
exec(&mut s, "edit.undo", json!({}));
assert_eq!(doc_profiles(&s.doc().unwrap().doc), (None, None));
assert_eq!(cms::active_settings(), working, "undone: the working spaces");
exec(&mut s, "edit.redo", json!({}));
assert_eq!(doc_profiles(&s.doc().unwrap().doc), (None, Some(cms::DEVICE_CMYK.to_string())));
assert_eq!(cms::active_settings().cmyk, cms::DEVICE_CMYK, "redone: the assigned profile");
exec(&mut s, "file.revert", json!({}));
assert_eq!(doc_profiles(&s.doc().unwrap().doc), (None, None));
assert_eq!(cms::active_settings(), working, "reverted: the working spaces");
}
/// A batch that assigns a profile and then fails puts the document back without it, and the
/// working spaces back in use.
#[test]
fn a_failed_batch_puts_the_profiles_in_use_back() {
let _g = settings();
let _reset = ColorSettingsReset { profile: None, paths: vec![] };
let mut s = session();
let working = cms::working_settings();
let steps = json!([
{"command": "edit.assignProfile", "params": {"cmyk": cms::DEVICE_CMYK}},
{"command": "document.activate", "params": {"index": 99}},
]);
assert!(s.execute("command.batch", &json!({"commands": steps})).is_err());
assert_eq!(doc_profiles(&s.doc().unwrap().doc), (None, None));
assert_eq!(cms::active_settings(), working);
}
/// A batch step that switches documents makes the next step convert through the profiles of the
/// document it switched to, not those of the document the batch began in.
#[test]
fn a_batch_converts_the_document_it_switches_to_through_its_profiles() {
let _g = settings();
let _reset = ColorSettingsReset { profile: None, paths: vec![] };
let blue = |s: &mut Session| {
let id = rect(s, 10.0, 10.0, 50.0, 50.0);
exec(s, "paint.setFill", json!({"ids": [id.0], "color": "#3366cc"}));
id
};
let fill = |s: &Session, id| s.doc().unwrap().doc.node(id).unwrap().appearance.fill_paint().color();
let mut working = session();
let w = blue(&mut working);
exec(&mut working, "file.documentColorMode", json!({"mode": "cmyk"}));
let mut s = session();
let b = blue(&mut s);
exec(&mut s, "file.new", json!({"width": 100, "height": 100}));
exec(&mut s, "edit.assignProfile", json!({"cmyk": cms::DEVICE_CMYK}));
let steps = json!([
{"command": "document.activate", "params": {"index": 0}},
{"command": "file.documentColorMode", "params": {"mode": "cmyk"}},
]);
exec(&mut s, "command.batch", json!({"commands": steps}));
assert_eq!(s.active_index(), Some(0));
assert_eq!(fill(&s, b), fill(&working, w), "converted through the working CMYK space");
}
/// View › Proof Setup › Working CMYK simulates the working CMYK space of Color Settings in a
/// document assigned another CMYK profile, in Proof Colors and in Separations Preview, and a change
/// of the working CMYK space shows in the next proof while the document keeps its profile.
+4 -2
View File
@@ -1434,8 +1434,10 @@ preferences (the folder the desktop app uses, so both see the same ones; `VECTOR
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.
`edit.assignProfile` tags the document only: colors show and convert through the profiles it assigns, and the
working spaces and the file stay as they are. The `workingCmyk` target of `view.proofSetup` simulates the working
CMYK space in such a document too.
working spaces and the file stay as they are. The profiles in use are those of the document in front, after
`file.new`, `document.open`, `document.activate`, `file.close`, `file.revert`, `edit.undo`, `edit.redo` and a
failed `command.batch` too; with none assigned, or no document open, the working spaces are. The `workingCmyk`
target of `view.proofSetup` simulates the working CMYK space in such a document too.
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
+1 -1
View File
@@ -703,7 +703,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Document Setup modal handoff:** Document Setup → Edit Artboards applies valid settings, closes the dialog and activates the Artboard tool. Pointer-click tests cover successful handoff, invalid settings, OK, Cancel and Undo in `dialogs/document_setup.rs`.
- **Document Setup › Color:** a Color tab shows the document color mode (RGB Color or CMYK Color), a Convert to CMYK or Convert to RGB box for a mode change (cleared: the colors keep their numbers), and the RGB and CMYK profiles assigned to the document, with a working-space entry for none. OK runs `edit.assignProfile` and then `file.documentColorMode` with the dialog's other changes in one `command.batch`, and the document's changes are one undo step. An unchanged Color tab adds no color commands, and Cancel, or OK with nothing changed, records no undo step. A Color tab field with a value of the wrong kind is an error, and the dialog stays open. The tab is VectorCraft's own layout; File › Document Color Mode and Edit › Assign Profile… stay as they are. Tests in `dialogs/document_setup.rs`.
- **Assign Profile and the working spaces:** Edit › Assign Profile… tags the document and leaves the working spaces of Edit › Color Settings… as they are, in Color Settings, the Separations panel and the settings saved for the next start. Colors show and convert through the profiles assigned; a change in Color Settings keeps the profiles the document in front is assigned in use, and removing an assigned profile in use puts the working space back in its place. View › Proof Setup › Working CMYK simulates the working CMYK space of Color Settings in Proof Colors and Separations Preview, also in a document assigned another CMYK profile. Public references: [Changing the color profile for a document and About color working spaces](https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=150) and [Soft-proof presets](https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=134). Tests in `engine/tests/color_settings.rs`.
- **Assign Profile and the working spaces:** Edit › Assign Profile… tags the document and leaves the working spaces of Edit › Color Settings… as they are, in Color Settings, the Separations panel and the settings saved for the next start. Colors show and convert through the profiles assigned; a change in Color Settings keeps the profiles the document in front is assigned in use, and removing an assigned profile in use puts the working space back in its place. The profiles in use follow the document in front: a new, opened or reverted document, a switch to another tab, a close, Undo and Redo of Assign Profile, a failed batch, each step of a batch, Export All Open and the update of placed documents put that document's profiles in use, and the working spaces where it is assigned none or no document is open. View › Proof Setup › Working CMYK simulates the working CMYK space of Color Settings in Proof Colors and Separations Preview, also in a document assigned another CMYK profile. Public references: [Changing the color profile for a document and About color working spaces](https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=150) and [Soft-proof presets](https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=134). Tests in `engine/tests/color_settings.rs`.
- **New Document's More Settings and Save Preset:** More Settings opens a pop-up over New Document with only Number of Artboards, the artboard layout, Change to Right-to-Left Layout, Spacing and Columns; Done, Escape or a click outside closes it and keeps the values. Save Preset turns the top of Preset Details into a Save Document Preset heading with the preset's name, Cancel and Save, and hides the document's name, Close and Create until the preset is saved or canceled; nothing in that mode creates a document. Save Preset's default name is the first "Preset N" no saved preset has. VectorCraft's own layout. Harness tests in `dialogs/new_document.rs` cover both.
- **New Document profiles:** each built-in category's presets start documents from that category's profile, a native document holding the starting swatches, graphic styles, brushes, symbols and other assets, the ruler and Document Setup options and the print settings ([Create a custom new document profile](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/start-a-new-file/create-a-custom-new-document-profile.html)). New Document's Manage Profiles pop-up opens a profile to edit and resets one to the shipped profile (`file.newProfiles`, `file.newProfiles.edit`, `file.newProfiles.reset`; `file.new {profile}`). The profiles live in VectorCraft's own `New Document Profiles` folder next to the preferences; a profile that cannot be read or is larger than 64 MiB leaves the defaults and is reported. Tests: `tests::newdoc::an_edited_profile_starts_the_documents_of_its_category`, `the_profile_follows_the_preset_access_and_reset`, `a_damaged_or_oversized_profile_leaves_the_defaults_and_is_reported`, `profile_names_are_checked_and_editing_needs_the_folder`, `dialogs::new_document::tests::manage_profiles_edits_and_resets_the_profiles`.
- **Control bar with the Artboard tool:** while the Artboard tool is in use, the Control bar shows the active artboard's controls in place of the object controls: Preset, Portrait and Landscape, New Artboard, Delete Artboard, the Name field, Artboard Options, Rearrange All Artboards, the Transform link, Move and Scale Artwork with Artboard and, while the bar has room, the reference point with X and Y, and W and H with the Constrain Width and Height Proportions link. Each runs the command its Properties or Transform panel counterpart runs (`artboard.setProps`, `artboard.new`, `artboard.delete`, `tool.options {tool: "artboard"}`), and the reference point is the one the Transform panel uses. In an 800-point window the controls up to the Transform link fit, Move and Scale Artwork with Artboard show as their boxes alone with their names in tooltips, and the link opens the Transform panel with the artboard's position and size. Pointer and key tests in `panels/artboards.rs` cover the controls, W about the reference point, the orientation and the name. Public reference: [Using multiple artboards](https://web.archive.org/web/20210925094818/https://helpx.adobe.com/illustrator/using/using-multiple-artboards.html) (archived copy).