diff --git a/crates/color/src/cms.rs b/crates/color/src/cms.rs index 46dcdb52ba..c5e5bc5bea 100644 --- a/crates/color/src/cms.rs +++ b/crates/color/src/cms.rs @@ -531,7 +531,7 @@ impl Cms { } } - /// Gamut warning: can this colour be printed in the working CMYK space? + /// Gamut warning: can this color be printed in this context's CMYK space? pub fn out_of_gamut(&self, c: &Color) -> bool { self.gamut_error(c) > GAMUT_THRESHOLD } diff --git a/crates/color/src/lib.rs b/crates/color/src/lib.rs index 0d84e9e6b3..3afca8063c 100644 --- a/crates/color/src/lib.rs +++ b/crates/color/src/lib.rs @@ -102,12 +102,13 @@ impl Color { pub fn to_lab(&self) -> cms::Lab { cms::active().lab(self) } - /// Colour-managed CMYK in the working CMYK space with `intent` (see [`Color::to_cmyk`] for the - /// profile-free formula). + /// Color-managed CMYK in the active CMYK space (the front document's assigned CMYK profile, else + /// the working CMYK space) with `intent` (see [`Color::to_cmyk`] for the profile-free formula). pub fn to_cmyk_managed(&self, intent: cms::Intent) -> [f32; 4] { cms::active().to_cmyk(self, intent) } - /// Gamut warning: true when this colour can't be reproduced in the working CMYK space. + /// Gamut warning: true when this color can't be reproduced in the active CMYK space (the front + /// document's assigned CMYK profile, else the working CMYK space). pub fn out_of_gamut(&self) -> bool { cms::active().out_of_gamut(self) } diff --git a/crates/engine/src/cmd/colormgmt.rs b/crates/engine/src/cmd/colormgmt.rs index 16990fc2ea..be00fd4f50 100644 --- a/crates/engine/src/cmd/colormgmt.rs +++ b/crates/engine/src/cmd/colormgmt.rs @@ -88,7 +88,7 @@ pub fn specs() -> Vec { "Gamut Warning", [], None, - "{ids?|id?, colors?: [color] (#rrggbbaa and #rgba are refused)} colours that can't be printed in the working CMYK (selection, or the whole document when nothing is selected) → {checked, outOfGamut: [{hex, deltaE, count}]}", + "{ids?|id?, colors?: [color] (#rrggbbaa and #rgba are refused)} colors that can't be printed in the document's assigned CMYK profile, else the working CMYK (selection, or the whole document when nothing is selected) → {checked, outOfGamut: [{hex, deltaE, count}]}", has_doc, gamut_check ), diff --git a/crates/ui-egui/src/dialogs/color_picker/tests.rs b/crates/ui-egui/src/dialogs/color_picker/tests.rs index 4d678b1069..5587b9c560 100644 --- a/crates/ui-egui/src/dialogs/color_picker/tests.rs +++ b/crates/ui-egui/src/dialogs/color_picker/tests.rs @@ -106,6 +106,94 @@ fn gamut_correction_gives_a_printable_colour() { assert_eq!(gamut_fix(&mut app, &ctx, "picker-gamut", &Color::rgb8(128, 128, 128)), None); } +/// The cached warning follows the CMYK space it is checked in: assigning the document a CMYK +/// profile, removing it, or changing the working CMYK space updates it for the same color. +#[test] +fn gamut_warning_follows_a_profile_change() { + const CHILD: &str = "VECTORCRAFT_TEST_GAMUT_PROFILE_CHANGE"; + if std::env::var_os(CHILD).is_none() { + // Color settings are process-wide: change them in a separate test process. + let output = std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "dialogs::color_picker::tests::gamut_warning_follows_a_profile_change", "--nocapture"]) + .env(CHILD, "1") + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + output.status.success() && stdout.contains("test result: ok. 1 passed; 0 failed;"), + "isolated gamut test failed:\n{stdout}\n{}", + String::from_utf8_lossy(&output.stderr), + ); + return; + } + use vectorcraft_color::cms; + let mut app = app(); + let ctx = egui::Context::default(); + let blue = Color::rgb(0.0, 0.0, 1.0); + let fix = gamut_fix(&mut app, &ctx, "picker-gamut", &blue); + assert!(fix.is_some(), "pure blue is out of the default CMYK gamut"); + // Device CMYK has no gamut to fall outside of. + app.run("edit.assignProfile", json!({"cmyk": cms::DEVICE_CMYK})).unwrap(); + assert_eq!(gamut_fix(&mut app, &ctx, "picker-gamut", &blue), None); + app.run("edit.assignProfile", json!({"cmyk": null})).unwrap(); + assert_eq!(gamut_fix(&mut app, &ctx, "picker-gamut", &blue), fix); + app.run("edit.colorSettings", json!({"cmyk": cms::DEVICE_CMYK})).unwrap(); + assert_eq!(gamut_fix(&mut app, &ctx, "picker-gamut", &blue), None); + app.run("edit.colorSettings", json!({"cmyk": cms::GENERIC_CMYK})).unwrap(); + assert_eq!(gamut_fix(&mut app, &ctx, "picker-gamut", &blue), fix); +} + +/// Loading a new file of the document's assigned CMYK profile, under the same name, updates the +/// cached warning for the same color. +#[test] +fn gamut_warning_follows_a_profile_loaded_again() { + const CHILD: &str = "VECTORCRAFT_TEST_GAMUT_PROFILE_RELOAD"; + if std::env::var_os(CHILD).is_none() { + // Color settings and loaded profiles are process-wide: change them in a separate test process. + let output = std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "dialogs::color_picker::tests::gamut_warning_follows_a_profile_loaded_again", "--nocapture"]) + .env(CHILD, "1") + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + output.status.success() && stdout.contains("test result: ok. 1 passed; 0 failed;"), + "isolated gamut test failed:\n{stdout}\n{}", + String::from_utf8_lossy(&output.stderr), + ); + return; + } + use vectorcraft_color::cms; + // A built-in CMYK profile's file renamed `name`: padded with spaces, which names drop. + let renamed = |builtin: &str, name: &str| { + let utf16 = |s: &str| s.encode_utf16().flat_map(u16::to_be_bytes).collect::>(); + let mut bytes = cms::icc_bytes(builtin).unwrap().to_vec(); + let (from, to) = (utf16(builtin), utf16(&format!("{name: Option { let r = app.run("color.convert", json!({"color": color_json(c), "to": "cmyk"})).ok()?; if !r["outOfGamut"].as_bool()? { @@ -261,18 +262,25 @@ pub(crate) fn in_gamut(app: &mut VectorcraftApp, c: &Color) -> Option { Some(Color::rgb(v(&rgb, 0)? as f32, v(&rgb, 1)? as f32, v(&rgb, 2)? as f32)) } -/// [`in_gamut`], cached under `key` for the last colour asked (panels ask every frame). +/// [`in_gamut`], cached under `key` for the last color asked (panels ask every frame) together with +/// the active color settings and profiles generation it was asked under, so a change of Color +/// Settings or of the document's assigned profiles updates the warning while the color stays the same. pub(crate) fn gamut_fix(app: &mut VectorcraftApp, ctx: &egui::Context, key: &str, c: &Color) -> Option { - match pstate::)>>(ctx, key) { - Some((k, fix)) if k == *c => fix, + let under = (cms::active_settings(), cms::profiles_generation()); + match pstate::>(ctx, key) { + Some((k, at, fix)) if k == *c && at == under => fix, _ => { let fix = in_gamut(app, c); - set_pstate(ctx, key, Some((*c, fix))); + set_pstate(ctx, key, Some((*c, under, fix))); fix } } } +/// A [`gamut_fix`] cache entry: the color, the active color settings and profiles generation it was +/// asked under, and its correction. +pub(crate) type GamutFix = (Color, (cms::ColorSettings, u64), Option); + /// Serde reader for the saved panel mode ([`crate::UiState::color_mode`]): a menu mode, else none, /// so an unknown value doesn't lose the rest of the preferences. pub fn deserialize_mode<'de, D: serde::Deserializer<'de>>(d: D) -> Result, D::Error> { diff --git a/crates/ui-egui/src/panels/color/tests.rs b/crates/ui-egui/src/panels/color/tests.rs index 4538a8e5ce..cd7b88cdd0 100644 --- a/crates/ui-egui/src/panels/color/tests.rs +++ b/crates/ui-egui/src/panels/color/tests.rs @@ -349,12 +349,12 @@ fn pure_blue_is_out_of_gamut_in_rgb() { let ctx = egui::Context::default(); app.run("paint.setFill", json!({"color": "#0000ff"})).unwrap(); frame(&mut app, &ctx, vec![], egui::Modifiers::NONE); - let (of, fix) = pstate::)>>(&ctx, "color-gamut").expect("RGB mode checks the gamut"); + let (of, _, fix) = pstate::>(&ctx, "color-gamut").expect("RGB mode checks the gamut"); assert_eq!(of.to_hex(), "#0000ff"); assert!(fix.is_some_and(|f| !f.out_of_gamut())); app.run("paint.setFill", json!({"color": "#808080"})).unwrap(); frame(&mut app, &ctx, vec![], egui::Modifiers::NONE); - assert_eq!(pstate::)>>(&ctx, "color-gamut").unwrap().1, None); + assert_eq!(pstate::>(&ctx, "color-gamut").unwrap().2, None); } #[test] diff --git a/docs/mcp.md b/docs/mcp.md index 9930b33734..c3d3223b3e 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1435,7 +1435,9 @@ preferences (the folder the desktop app uses, so both see the same ones; `VECTOR 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. + CMYK space in such a document too. `color.convert`'s `outOfGamut` and `deltaE`, `color.gamutCheck`, and the + out-of-gamut warning of the Color panel and Color Picker check against the assigned CMYK profile, else the + working CMYK space; the warning updates when either changes. 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 diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 976677ee36..a8ab1472f9 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -707,6 +707,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`. +- **Out-of-gamut warning after a profile change:** the out-of-gamut warning and its closest-color correction in the Color panel and Color Picker ([the help page](https://helpx.adobe.com/illustrator/using/adjusting-colors.html)) check the color against the document's assigned CMYK profile, else the working CMYK space, and update when Edit › Assign Profile… or Edit › Color Settings… changes that space or a loaded profile changes, without a change of color. Which CMYK space the warning uses is VectorCraft's own rule. Tests: `gamut_warning_follows_a_profile_change` and `gamut_warning_follows_a_profile_loaded_again` in `dialogs/color_picker/tests.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).