mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 20:08:42 +00:00
Merge pull request #7188 from dotCooCoo/fix/color-gamut-cache-profile
Color: the out-of-gamut warning updates after a profile change
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"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
|
||||
),
|
||||
|
||||
@@ -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::<Vec<u8>>();
|
||||
let mut bytes = cms::icc_bytes(builtin).unwrap().to_vec();
|
||||
let (from, to) = (utf16(builtin), utf16(&format!("{name:<width$}", width = builtin.len())));
|
||||
let at = bytes.windows(from.len()).position(|w| w == from).unwrap();
|
||||
bytes[at..at + to.len()].copy_from_slice(&to);
|
||||
bytes
|
||||
};
|
||||
let name = "Gamut Reload CMYK";
|
||||
let dir = vectorcraft_testkit::temp_dir("gamut-reload-cmyk");
|
||||
let mut app = app();
|
||||
let load = |app: &mut VectorcraftApp, builtin: &str| {
|
||||
let path = dir.join(format!("{}.icc", builtin.len()));
|
||||
std::fs::write(&path, renamed(builtin, name)).unwrap();
|
||||
let r = app.run("color.loadProfile", json!({"path": path.to_str().unwrap()})).unwrap();
|
||||
assert_eq!(r["name"], name);
|
||||
};
|
||||
let ctx = egui::Context::default();
|
||||
let blue = Color::rgb(0.0, 0.0, 1.0);
|
||||
load(&mut app, cms::GENERIC_CMYK);
|
||||
app.run("edit.assignProfile", json!({"cmyk": name})).unwrap();
|
||||
let before = gamut_fix(&mut app, &ctx, "picker-gamut", &blue);
|
||||
assert!(before.is_some(), "pure blue is out of the loaded profile's gamut");
|
||||
load(&mut app, cms::DEVICE_CMYK);
|
||||
let fresh = in_gamut(&mut app, &blue);
|
||||
assert_ne!(fresh, before, "the new file gives another answer");
|
||||
assert_eq!(gamut_fix(&mut app, &ctx, "picker-gamut", &blue), fresh);
|
||||
}
|
||||
|
||||
/// The selected art takes each colour as it is picked; OK keeps the last as one undo step and
|
||||
/// Cancel puts the art back.
|
||||
#[test]
|
||||
|
||||
@@ -248,8 +248,9 @@ pub(crate) fn warning_chip(ui: &mut Ui, (icon, warning): (&str, &str), fix: &Col
|
||||
.inner
|
||||
}
|
||||
|
||||
/// The printable colour closest to `c` (its working-CMYK reproduction, back in RGB) when `c` is out
|
||||
/// of the CMYK gamut, through `color.convert`.
|
||||
/// The printable color closest to `c` (its reproduction in the active CMYK space, the document's
|
||||
/// assigned CMYK profile or else the working CMYK space, back in RGB) when `c` is out of that
|
||||
/// space's gamut, through `color.convert`.
|
||||
pub(crate) fn in_gamut(app: &mut VectorcraftApp, c: &Color) -> Option<Color> {
|
||||
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<Color> {
|
||||
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<Color> {
|
||||
match pstate::<Option<(Color, Option<Color>)>>(ctx, key) {
|
||||
Some((k, fix)) if k == *c => fix,
|
||||
let under = (cms::active_settings(), cms::profiles_generation());
|
||||
match pstate::<Option<GamutFix>>(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<Color>);
|
||||
|
||||
/// 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<Option<Mode>, D::Error> {
|
||||
|
||||
@@ -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::<Option<(Color, Option<Color>)>>(&ctx, "color-gamut").expect("RGB mode checks the gamut");
|
||||
let (of, _, fix) = pstate::<Option<GamutFix>>(&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::<Option<(Color, Option<Color>)>>(&ctx, "color-gamut").unwrap().1, None);
|
||||
assert_eq!(pstate::<Option<GamutFix>>(&ctx, "color-gamut").unwrap().2, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+3
-1
@@ -1482,7 +1482,9 @@ preferences (the folder the desktop app uses, so both see the same ones; `VECTOR
|
||||
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.
|
||||
target of `view.proofSetup` simulates the working 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
|
||||
|
||||
@@ -711,6 +711,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`.
|
||||
- **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`.
|
||||
- **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`, `profile_errors_name_the_file_by_its_name_outside_the_read_roots`, `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`.
|
||||
|
||||
Reference in New Issue
Block a user