Merge pull request #7703 from dotCooCoo/feat/color-management-policies

Color Settings: RGB and CMYK policies keep, take off or convert a document's profiles on open
This commit is contained in:
BFlat
2026-11-15 15:46:48 +07:00
committed by GitHub
40 changed files with 809 additions and 28 deletions
+98 -2
View File
@@ -94,6 +94,50 @@ impl Intent {
}
}
/// A color management policy (Edit → Color Settings): what opening a document does with a profile
/// it is tagged with that is not the working space of its model.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Policy {
/// The document loses the profile; its color numbers stay as they are.
Off,
/// The document keeps the profile.
#[default]
PreserveEmbedded,
/// The colors are converted from the profile into the working space, which keeps their
/// appearance, and the document loses the profile.
Convert,
/// CMYK only: the document loses the profile; its color numbers stay as they are.
PreserveNumbers,
}
impl Policy {
/// The RGB policies, in menu order.
pub const RGB: [Policy; 3] = [Policy::Off, Policy::PreserveEmbedded, Policy::Convert];
/// The CMYK policies, in menu order.
pub const CMYK: [Policy; 4] = [Policy::Off, Policy::PreserveEmbedded, Policy::Convert, Policy::PreserveNumbers];
/// Parse an id of [`Policy::id`].
pub fn parse(s: &str) -> Option<Self> {
[Policy::Off, Policy::PreserveEmbedded, Policy::Convert, Policy::PreserveNumbers].into_iter().find(|p| p.id() == s)
}
pub fn id(&self) -> &'static str {
match self {
Policy::Off => "off",
Policy::PreserveEmbedded => "preserveEmbedded",
Policy::Convert => "convert",
Policy::PreserveNumbers => "preserveNumbers",
}
}
pub fn label(&self) -> &'static str {
match self {
Policy::Off => "Off",
Policy::PreserveEmbedded => "Preserve Embedded Profiles",
Policy::Convert => "Convert to Working Space",
Policy::PreserveNumbers => "Preserve Numbers",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProfileKind {
@@ -134,11 +178,25 @@ pub struct ColorSettings {
pub intent: Intent,
/// Black-point compensation.
pub bpc: bool,
/// The color management policy for RGB profiles (one of [`Policy::RGB`]). Settings saved
/// without one take the default.
#[serde(default)]
pub rgb_policy: Policy,
/// The color management policy for CMYK profiles. Settings saved without one take the default.
#[serde(default)]
pub cmyk_policy: Policy,
}
impl Default for ColorSettings {
fn default() -> Self {
Self { rgb: SRGB.into(), cmyk: GENERIC_CMYK.into(), intent: Intent::RelativeColorimetric, bpc: true }
Self {
rgb: SRGB.into(),
cmyk: GENERIC_CMYK.into(),
intent: Intent::RelativeColorimetric,
bpc: true,
rgb_policy: Policy::default(),
cmyk_policy: Policy::default(),
}
}
}
@@ -518,6 +576,41 @@ impl Cms {
Color::Rgb { r, g, b }
}
/// An RGB or CMYK colour of this context's space as the colour of the same model in `dest`'s
/// space that looks the same, converted with `intent` (Edit → Color Settings › Convert to
/// Working Space). RGB converts directly from profile to profile, and CMYK between two loaded
/// profiles through ProPhoto RGB ([`IccProfile::to_same_model_of`]), both with this context's
/// black-point compensation; CMYK goes through Lab when either space is built in. Grey and
/// Lab colours stay as they are.
pub fn convert_into(&self, dest: &Cms, c: &Color, intent: Intent) -> Color {
match *c {
Color::Rgb { r, g, b } => {
let icc = |s: &RgbSpace| match s {
RgbSpace::Icc(p) => Some(p.clone()),
RgbSpace::Srgb => builtin_rgb_cache(SRGB),
};
let [r, g, b] = icc(&self.rgb)
.zip(icc(&dest.rgb))
.and_then(|(s, d)| s.to_rgb_of(&d, &[r, g, b], intent, self.settings.bpc))
.unwrap_or_else(|| dest.srgb_to_rgb(self.rgb_to_srgb([r, g, b])));
Color::Rgb { r, g, b }
}
Color::Cmyk { c, m, y, k } => {
let inks = [c, m, y, k].map(|v| v.clamp(0.0, 1.0));
let direct = match (&self.cmyk, &dest.cmyk) {
(CmykSpace::Icc(s), CmykSpace::Icc(d)) => s.to_same_model_of(d, &inks, intent, self.settings.bpc),
_ => None,
};
let [c, m, y, k] = match direct.as_deref() {
Some(&[c, m, y, k]) => [c, m, y, k],
_ => dest.lab_to_cmyk(self.cmyk_to_lab(inks), intent),
};
Color::Cmyk { c, m, y, k }
}
Color::Gray { .. } | Color::Lab { .. } => *c,
}
}
/// How far (ΔE2000) the best CMYK reproduction of `c` is from `c` (0 for CMYK/grey colours).
pub fn gamut_error(&self, c: &Color) -> f32 {
match c {
@@ -966,8 +1059,11 @@ pub fn working_settings() -> ColorSettings {
}
/// Replace the working settings (Edit → Color Settings) and make them the active settings (fails,
/// leaving both unchanged, if a profile is unknown).
/// leaving both unchanged, if a profile is unknown or the RGB policy is Preserve Numbers).
pub fn set_active(settings: &ColorSettings) -> Result<(), CmsError> {
if !Policy::RGB.contains(&settings.rgb_policy) {
return Err(CmsError::Unsupported(format!("the RGB policy can't be {}", settings.rgb_policy.id())));
}
let cms = Arc::new(Cms::new(settings)?);
*WORKING.write().unwrap_or_else(|e| e.into_inner()) = Some(cms.clone());
install(cms);
+78
View File
@@ -34,6 +34,17 @@ type Xf = Option<Arc<TransformF32Executor>>;
/// generation only lets the cache drop what reloads left behind.
type DirectKey = (u64, u64, usize, bool);
/// The transforms of [`IccProfile::to_same_model_of`]: into ProPhoto RGB's primaries with linear
/// tone curves, where black-point compensation is a linear remap, and from there into the
/// destination.
struct SameModel {
into: Arc<TransformF32Executor>,
out_of: Arc<TransformF32Executor>,
/// The source's and the destination's black as luminance in the linear space (both 0 without
/// compensation).
black: [f32; 2],
}
/// A parsed ICC profile plus lazily-built transforms to/from sRGB.
pub struct IccProfile {
pub name: String,
@@ -49,6 +60,9 @@ pub struct IccProfile {
/// Direct transforms into other RGB profiles ([`Self::to_rgb_of`]), by destination, intent
/// and black-point compensation.
direct: Mutex<Vec<(DirectKey, Option<Arc<Direct>>)>>,
/// Transforms into other profiles of the same model ([`Self::to_same_model_of`]), keyed as
/// [`Self::direct`] is.
same_model: Mutex<Vec<(DirectKey, Option<Arc<SameModel>>)>>,
}
/// A transform from one profile into an RGB profile without passing through sRGB: into the
@@ -99,6 +113,7 @@ impl IccProfile {
to_srgb: Default::default(),
from_srgb: Default::default(),
direct: Mutex::new(Vec::new()),
same_model: Mutex::new(Vec::new()),
})
}
@@ -264,6 +279,69 @@ impl IccProfile {
Some(Direct { to_linear, to_dest: Some(to_dest), black })
}
/// Device values of this profile → device values of `dest`, a profile of the same model,
/// converted with `intent` through ProPhoto RGB rather than sRGB, so colours outside sRGB
/// keep their place. With `bpc` (and an intent other than absolute), this profile's black
/// maps onto the destination's black (black-point compensation). `None` when `dest` is of
/// another model or no transform can be built.
pub fn to_same_model_of(&self, dest: &IccProfile, v: &[f32], intent: Intent, bpc: bool) -> Option<Vec<f32>> {
if dest.kind != self.kind || v.len() != self.channels() {
return None;
}
let bpc = bpc && intent != Intent::AbsoluteColorimetric;
let generation = profiles_generation();
let key = (dest.id, generation, intent_index(intent), bpc);
let t = {
let mut cache = self.same_model.lock().unwrap_or_else(|e| e.into_inner());
cache.retain(|(k, _)| k.1 == generation);
match cache.iter().find(|(k, _)| *k == key) {
Some((_, t)) => t.clone(),
None => {
let t = self.build_same_model(dest, intent, bpc).map(Arc::new);
cache.push((key, t.clone()));
t
}
}
}?;
let mut lin = [0.0f32; 3];
t.into.transform(v, &mut lin).ok()?;
let [from, to] = t.black;
if from > 0.0 || to > 0.0 {
lin = lin.map(|x| to + (x - from) * (1.0 - to) / (1.0 - from));
}
let lin = lin.map(|x| x.clamp(0.0, 1.0));
let mut out = vec![0.0f32; dest.channels()];
t.out_of.transform(&lin, &mut out).ok()?;
Some(out.into_iter().map(|x| x.clamp(0.0, 1.0)).collect())
}
fn build_same_model(&self, dest: &IccProfile, intent: Intent, bpc: bool) -> Option<SameModel> {
let mut linear = rgb_space(super::PROPHOTO_RGB)?;
for trc in [&mut linear.red_trc, &mut linear.green_trc, &mut linear.blue_trc] {
*trc = Some(ToneReprCurve::Parametric(vec![1.0]));
}
linear.cicp = None;
let into = self.profile.create_transform_f32(self.layout(), &linear, Layout::Rgb, Self::opts(intent)).ok()?;
let out_of = linear.create_transform_f32(Layout::Rgb, &dest.profile, dest.layout(), Self::opts(intent)).ok()?;
let black = if bpc {
// Each profile's black: the darkest colour it reproduces, found as `build_direct`
// finds it, as luminance in the linear space.
let luminance = |p: &IccProfile, xf: &Arc<TransformF32Executor>| -> Option<f32> {
let inks = p.from_srgb([0.0; 3], Intent::RelativeColorimetric)?;
let mut lin = [0.0f32; 3];
xf.transform(&inks, &mut lin).ok()?;
let y = |c: &moxcms::Xyzd| c.y as f32;
let lum = lin[0] * y(&linear.red_colorant) + lin[1] * y(&linear.green_colorant) + lin[2] * y(&linear.blue_colorant);
Some(lum.clamp(0.0, 0.5))
};
let back = dest.profile.create_transform_f32(dest.layout(), &linear, Layout::Rgb, Self::opts(intent)).ok()?;
[luminance(self, &into)?, luminance(dest, &back)?]
} else {
[0.0; 2]
};
Some(SameModel { into, out_of, black })
}
/// Whether transforms can be built for this profile (checked at registration).
pub fn usable(&self) -> bool {
self.to_xf(Intent::RelativeColorimetric).is_some() && self.inverse_xf(Intent::RelativeColorimetric).is_some()
+36
View File
@@ -442,3 +442,39 @@ fn a_document_gets_the_same_context_for_the_same_profiles() {
let unknown = for_document(Some("no such profile"), Some(SRGB));
assert_eq!(unknown.settings(), &working_settings(), "an unknown profile or one of the other model leaves the working space");
}
/// The color management policies parse from their ids, Preserve Numbers is a CMYK policy only, and
/// settings saved before the policies existed read with the default policy.
#[test]
fn policies_parse_and_default_in_saved_settings() {
for p in Policy::CMYK {
assert_eq!(Policy::parse(p.id()), Some(p));
}
assert_eq!(Policy::parse("bogus"), None);
assert!(!Policy::RGB.contains(&Policy::PreserveNumbers));
let old: ColorSettings =
serde_json::from_str(r#"{"rgb": "sRGB IEC61966-2.1", "cmyk": "Device CMYK (uncalibrated)", "intent": "perceptual", "bpc": false}"#).unwrap();
assert_eq!((old.rgb_policy, old.cmyk_policy), (Policy::PreserveEmbedded, Policy::PreserveEmbedded));
let saved = serde_json::to_value(ColorSettings { cmyk_policy: Policy::PreserveNumbers, ..Default::default() }).unwrap();
assert_eq!((saved["rgbPolicy"].as_str(), saved["cmykPolicy"].as_str()), (Some("preserveEmbedded"), Some("preserveNumbers")));
}
/// Convert to Working Space keeps a color's appearance: an RGB color of a wide space converts into
/// sRGB, and a CMYK color of one CMYK space into another, with numbers that look the same. Grey and
/// Lab colors stay as they are.
#[test]
fn convert_into_keeps_the_appearance() {
let wide = Cms::new(&ColorSettings { rgb: WIDE_GAMUT_RGB.into(), cmyk: DEVICE_CMYK.into(), ..Default::default() }).unwrap();
let working = cms();
let rgb = Color::Rgb { r: 0.25, g: 0.4, b: 0.6 };
let moved = wide.convert_into(&working, &rgb, Intent::RelativeColorimetric);
assert_ne!(moved, rgb, "the numbers change");
assert!(de(wide.display_rgb(&rgb), working.display_rgb(&moved)) < 1.0, "{rgb:?} → {moved:?}");
let ink = Color::Cmyk { c: 0.2, m: 0.4, y: 0.6, k: 0.1 };
let moved = wide.convert_into(&working, &ink, Intent::RelativeColorimetric);
assert!(matches!(moved, Color::Cmyk { .. }) && moved != ink, "{ink:?} → {moved:?}");
assert!(delta_e2000(wide.lab(&ink), working.lab(&moved)) < 3.0, "{ink:?} → {moved:?}");
for c in [Color::Gray { k: 0.3 }, Color::Lab { l: 50.0, a: 10.0, b: -20.0 }] {
assert_eq!(wide.convert_into(&working, &c, Intent::RelativeColorimetric), c);
}
}
+52
View File
@@ -274,3 +274,55 @@ fn settings_built_before_a_reload_dont_leave_the_old_transform_to_the_new_ones()
assert!(in_srgb.iter().zip(want).any(|(a, b)| (a - b).abs() > 0.05), "sRGB {in_srgb:?} vs ProPhoto {want:?}");
assert!(in_prophoto.iter().zip(want).all(|(a, b)| (a - b).abs() < 1e-4), "after the reload {in_prophoto:?}, want {want:?}");
}
/// A CMYK profile converts into another CMYK profile, ink for ink, and refuses a profile
/// of another model or inks of the wrong count.
#[test]
fn cmyk_profiles_convert_into_each_other_directly() {
let generic = IccProfile::from_bytes(Some("A".into()), &icc_bytes(GENERIC_CMYK).unwrap()).unwrap();
let device = IccProfile::from_bytes(Some("B".into()), &icc_bytes(DEVICE_CMYK).unwrap()).unwrap();
let srgb = IccProfile::from_bytes(None, &icc_bytes(SRGB).unwrap()).unwrap();
let inks = [0.2, 0.4, 0.6, 0.1];
let out = generic.to_same_model_of(&device, &inks, Intent::RelativeColorimetric, false).unwrap();
assert_eq!(out.len(), 4);
assert!(out.iter().zip(inks).any(|(a, b)| (a - b).abs() > 0.01), "{out:?}");
assert!(generic.to_same_model_of(&srgb, &inks, Intent::RelativeColorimetric, false).is_none());
assert!(generic.to_same_model_of(&device, &inks[..3], Intent::RelativeColorimetric, false).is_none());
}
/// With black-point compensation, a CMYK profile's darkest ink converts into an ink as dark as a
/// darker profile's darkest ink, and a dark grey of a darker profile converts lighter into a
/// profile whose black is lighter; absolute colorimetric never compensates.
#[test]
fn cmyk_to_cmyk_compensates_the_black_point() {
let generic = IccProfile::from_bytes(Some("A".into()), &icc_bytes(GENERIC_CMYK).unwrap()).unwrap();
let device = IccProfile::from_bytes(Some("B".into()), &icc_bytes(DEVICE_CMYK).unwrap()).unwrap();
let rel = Intent::RelativeColorimetric;
let lightness = |p: &IccProfile, inks: &[f32]| lab::srgb_to_lab(p.to_srgb(inks, rel).unwrap()).l;
let black = |p: &IccProfile| p.from_srgb([0.0; 3], rel).unwrap();
// Generic CMYK's black is a dark grey, the uncalibrated profile's is black.
let (light, dark) = (lightness(&generic, &black(&generic)), lightness(&device, &black(&device)));
assert!(light > dark + 5.0, "{light} vs {dark}");
let into_device = |bpc| lightness(&device, &generic.to_same_model_of(&device, &black(&generic), rel, bpc).unwrap());
let (on, off) = (into_device(true), into_device(false));
assert!((on - dark).abs() < 1.5 && off > dark + 5.0, "with {on}, without {off}, the black {dark}");
let grey = device.from_srgb([0.2; 3], rel).unwrap();
let into_generic = |bpc| lightness(&generic, &device.to_same_model_of(&generic, &grey, rel, bpc).unwrap());
let (on, off) = (into_generic(true), into_generic(false));
assert!(on > off + 1.0, "with {on}, without {off}");
for (from, to, inks) in [(&generic, &device, black(&generic)), (&device, &generic, grey)] {
let abs = |bpc| from.to_same_model_of(to, &inks, Intent::AbsoluteColorimetric, bpc).unwrap();
assert_eq!(abs(true), abs(false));
}
// Convert to Working Space (Cms::convert_into) compensates as Color Settings say.
let device_name = register_icc(&icc_bytes(DEVICE_CMYK).unwrap(), Some("Test ICC Device CMYK".into())).unwrap().name;
let rich = Color::Cmyk { c: 0.9, m: 0.8, y: 0.8, k: 1.0 };
let convert = |bpc| {
let dest = Cms::new(&ColorSettings { cmyk: device_name.clone(), bpc, ..ColorSettings::default() }).unwrap();
icc_cmyk(bpc).convert_into(&dest, &rich, rel)
};
assert_ne!(convert(true), convert(false));
// White is untouched.
let white = generic.to_same_model_of(&device, &[0.0; 4], rel, true).unwrap();
assert!(white.iter().all(|v| *v < 0.01), "{white:?}");
}
+129 -3
View File
@@ -16,7 +16,7 @@ use std::collections::BTreeMap;
use std::sync::Arc;
use serde_json::{Value, json};
use vectorcraft_color::cms::{self, ColorSettings, Intent, Model, ProofTarget};
use vectorcraft_color::cms::{self, ColorSettings, Intent, Model, Policy, ProofTarget};
use vectorcraft_color::recolor::Neutral;
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::ColorMode;
@@ -37,7 +37,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}]}. 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",
"{rgb?: profile, cmyk?: profile, intent?: \"perceptual\"|\"relative\"|\"saturation\"|\"absolute\", bpc?: bool, rgbPolicy?: \"off\"|\"preserveEmbedded\"|\"convert\", cmykPolicy?: \"off\"|\"preserveEmbedded\"|\"convert\"|\"preserveNumbers\"} set the working spaces and the color management policies → {rgb, cmyk, intent, bpc, rgbPolicy, cmykPolicy, profiles: [{name, kind, builtin}]}. A policy decides what opening a document (document.open, file.newFromTemplate, file.revert) does with an RGB or CMYK profile it is tagged with that is not the working space: preserveEmbedded (default) keeps it; off and preserveNumbers take it off and keep the color numbers; convert converts the colors of that model into the working space with the intent and bpc and takes it off. The open reply's warnings name each profile taken off or converted, and a document a policy changed opens with unsaved changes. 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
),
@@ -163,12 +163,26 @@ fn on_param(p: &Value, key: &str, cmd: &str) -> Result<Option<bool>> {
switch_or(p, key, false, cmd).map(Some)
}
/// The policy `p` gives for `key`, one of `allowed`, or `None` when `p` leaves it out.
fn policy_param(p: &Value, key: &str, allowed: &[Policy], cmd: &str) -> Result<Option<Policy>> {
let Some(v) = text_param(p, key, cmd)? else { return Ok(None) };
match Policy::parse(v).filter(|x| allowed.contains(x)) {
Some(x) => Ok(Some(x)),
None => {
let ids: Vec<&str> = allowed.iter().map(Policy::id).collect();
Err(bad(cmd, format!("`{key}` must be one of {}", ids.join(", "))))
}
}
}
fn settings_json(st: &ColorSettings) -> Value {
json!({
"rgb": st.rgb,
"cmyk": st.cmyk,
"intent": st.intent.id(),
"bpc": st.bpc,
"rgbPolicy": st.rgb_policy.id(),
"cmykPolicy": st.cmyk_policy.id(),
"profiles": cms::profiles().iter().map(|p| json!({"name": p.name, "kind": p.kind, "builtin": p.builtin})).collect::<Vec<_>>(),
})
}
@@ -205,6 +219,12 @@ fn color_settings(s: &mut Session, p: &Value) -> Result<Value> {
st.intent = i;
}
st.bpc = switch_or(p, "bpc", st.bpc, C)?;
if let Some(v) = policy_param(p, "rgbPolicy", &Policy::RGB, C)? {
st.rgb_policy = v;
}
if let Some(v) = policy_param(p, "cmykPolicy", &Policy::CMYK, C)? {
st.cmyk_policy = v;
}
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).
@@ -530,7 +550,12 @@ pub(crate) fn set_color_mode(d: &mut vectorcraft_doc::Document, mode: ColorMode,
if !convert {
return 0;
}
let conv = mode_converter(mode, intent, grays);
convert_colors(d, &mode_converter(mode, intent, grays))
}
/// Every color of `d` that [`set_color_mode`] converts, converted with `conv` → how many colors of
/// the art changed, as [`set_color_mode`] counts them.
fn convert_colors(d: &mut vectorcraft_doc::Document, conv: &dyn Fn(&Color) -> Color) -> usize {
for sw in d.swatches_iter_mut() {
match &mut sw.paint {
Paint::Solid { color, .. } => *color = conv(color),
@@ -538,6 +563,11 @@ pub(crate) fn set_color_mode(d: &mut vectorcraft_doc::Document, mode: ColorMode,
for st in &mut g.gradient.stops {
st.color = conv(&st.color);
}
if let Some(freeform) = &mut g.freeform {
for point in &mut freeform.points {
point.color = conv(&point.color);
}
}
}
_ => {}
}
@@ -621,6 +651,102 @@ pub(crate) fn set_color_mode(d: &mut vectorcraft_doc::Document, mode: ColorMode,
in_art
}
/// Apply the color management policies of Edit → Color Settings to `d`, a document being opened:
/// each RGB or CMYK profile it is tagged with that is not the working space of its model is kept
/// (Preserve Embedded Profiles), taken off with the color numbers kept (Off, Preserve Numbers), or
/// taken off after the colors of that model were converted from it into the working space with the
/// working intent (Convert to Working Space). A profile that is not available here is taken off
/// by Off and Preserve Numbers and kept by Convert to Working Space, which has nothing to convert
/// from. → a note for each profile, and whether `d` changed.
pub(crate) fn apply_open_policies(d: &mut vectorcraft_doc::Document) -> (Vec<String>, bool) {
let working = cms::working_settings();
let (rgb, cmyk) = doc_profiles(d);
let mut notes = Vec::new();
// What each model's policy does with its profile: `None` keeps it; `Some` takes it off, after
// converting the colors from the profile it holds.
let mut decide = |label: &str, tag: Option<String>, working: &str, policy: Policy, kind: cms::ProfileKind| -> Option<Option<String>> {
let tag = tag.filter(|t| t != working)?;
let available = cms::profile(&tag).is_some_and(|p| p.kind == kind);
match policy {
Policy::PreserveEmbedded => None,
Policy::Off | Policy::PreserveNumbers => {
notes.push(format!(
"the {label} profile “{tag}” the file is tagged with was not used: Color Settings' {label} policy is {}",
policy.label()
));
Some(None)
}
Policy::Convert if available => {
notes.push(format!(
"the {label} colors were converted from the profile “{tag}” the file is tagged with into the working space “{working}”"
));
Some(Some(tag))
}
Policy::Convert => {
notes.push(format!("the {label} profile “{tag}” the file is tagged with is not available, so its colors were not converted"));
None
}
}
};
let rgb_do = decide("RGB", rgb, &working.rgb, working.rgb_policy, cms::ProfileKind::Rgb);
let cmyk_do = decide("CMYK", cmyk, &working.cmyk, working.cmyk_policy, cms::ProfileKind::Cmyk);
if rgb_do.is_none() && cmyk_do.is_none() {
return (notes, false);
}
let (from_rgb, from_cmyk) = (rgb_do.clone().flatten(), cmyk_do.clone().flatten());
if from_rgb.is_some() || from_cmyk.is_some() {
let source = cms::for_document(from_rgb.as_deref(), from_cmyk.as_deref());
let dest = cms::working();
let intent = working.intent;
let (rgb_on, cmyk_on) = (from_rgb.is_some(), from_cmyk.is_some());
let conv = |c: &Color| match c {
Color::Rgb { .. } if rgb_on => source.convert_into(&dest, c, intent),
Color::Cmyk { .. } if cmyk_on => source.convert_into(&dest, c, intent),
_ => *c,
};
convert_colors(d, &conv);
convert_setting_colors(d, &conv);
}
if rgb_do.is_some() {
d.color_profiles.rgb = None;
}
if cmyk_do.is_some() {
d.color_profiles.cmyk = None;
}
(notes, true)
}
/// The colors of `d` outside its art that [`convert_colors`] leaves, converted with `conv`: each
/// artboard's background, the transparency grid and simulated paper colors of Document Setup, and
/// the fill and stroke of the character and paragraph styles.
fn convert_setting_colors(d: &mut vectorcraft_doc::Document, conv: &dyn Fn(&Color) -> Color) {
for c in d.artboards.iter_mut().filter_map(|a| a.color.as_mut()) {
*c = conv(c);
}
for c in &mut d.setup.grid_colors {
*c = conv(c);
}
// A tint of a global swatch is the converted swatch's color at its tint, which
// [`convert_colors`] already gave it.
let bases = global_bases(d);
let mut f = |c: &Color, link: proof::Link| mode_color(c, link, &bases, conv);
for styles in [&mut d.char_styles, &mut d.para_styles] {
if !styles.iter().any(|s| s.attrs.contains_key("fill") || s.attrs.contains_key("stroke")) {
continue;
}
for def in styles.iter_mut() {
for key in ["fill", "stroke"] {
let Some(value) = def.attrs.get_mut(key) else { continue };
let Ok(mut paint) = serde_json::from_value::<Paint>(value.clone()) else { continue };
map_paint_colors(&mut paint, &mut f);
if let Ok(v) = serde_json::to_value(&paint) {
*value = v;
}
}
}
}
}
/// [`proof::map_node_colors`] over `n`'s subtree, then [`map_more_colors`].
fn map_art_colors(n: &mut vectorcraft_doc::Node, f: &mut dyn FnMut(&Color, proof::Link) -> Color) {
proof::map_node_colors(n, f);
File diff suppressed because one or more lines are too long
+52 -10
View File
@@ -660,6 +660,17 @@ pub struct ReadFile {
path: Option<String>,
/// What became of its linked files.
links: Resolved,
/// Whether the color management policies changed the document ([`apply_policies`]).
policy_changed: bool,
}
/// Edit › Color Settings: the color management policies keep, take off or convert from the
/// profiles the document `loaded` holds is tagged with, and their notes go to its warnings →
/// whether they changed the document.
fn apply_policies(loaded: &mut Loaded) -> bool {
let (notes, changed) = crate::cmd::colormgmt::apply_open_policies(&mut loaded.doc);
loaded.warnings.extend(notes);
changed
}
#[cfg(test)]
@@ -680,17 +691,35 @@ impl ReadFile {
/// ([`super::importer`]).
pub fn read_for_open(name: &str, bytes: &[u8], path: Option<String>, p: &Value, update_links: bool, low_res_proxy_eps: bool) -> Result<ReadFile> {
let opts = LoadOptions::from_params("document.open", p)?;
let mut loaded = if opts.link_pages {
linked_pages(name, bytes, path.as_deref(), &opts)?
let (mut loaded, policy_changed) = if opts.link_pages {
let mut loaded = linked_pages(name, bytes, path.as_deref(), &opts)?;
let changed = apply_policies(&mut loaded);
(loaded, changed)
} else {
match super::importer::pick(name, bytes, p)? {
Some(plugin) => super::importer::load(name, bytes, &plugin, p)?,
None => load_with(name, bytes, &opts)?,
Some(plugin) => {
let mut loaded = super::importer::load(name, bytes, &plugin, p)?;
let changed = apply_policies(&mut loaded);
(loaded, changed)
}
None => {
// The color mode `p` asks for converts the colors after the policies did, through
// the profiles the document keeps.
let mut loaded = load_with(name, bytes, &LoadOptions { color_mode: None, ..opts.clone() })?;
let changed = apply_policies(&mut loaded);
if let Some(mode) = opts.color_mode.filter(|m| *m != loaded.doc.color_mode) {
let doc = &mut loaded.doc;
vectorcraft_color::cms::with_active(super::super::colormgmt::doc_cms(doc), || {
super::super::colormgmt::set_color_mode(doc, mode, true, None, opts.grays);
});
}
(loaded, changed)
}
}
};
let links = crate::cmd::links::resolve(&mut loaded.doc, crate::cmd::links::LinkBase::file(path.as_deref()), update_links, low_res_proxy_eps);
Session::refresh_text_bounds(&mut loaded.doc);
Ok(ReadFile { loaded, opts, path, links })
Ok(ReadFile { loaded, opts, path, links, policy_changed })
}
/// The PDF `name` (`bytes`, read from `path`) opened with `linkPages`: its pages as placed documents
@@ -705,7 +734,7 @@ fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, opts: &LoadOptions
/// The second half of [`open_bytes_with`]: open a file [`read_for_open`] read as the new active
/// document → `document.open`'s result.
pub fn open_read(s: &mut Session, file: ReadFile) -> Result<Value> {
open_loaded(s, file.loaded, file.links, file.path, &file.opts, false)
open_loaded(s, file.loaded, file.links, file.path, &file.opts, false, file.policy_changed)
}
/// File → New from Template: any readable file (at `path`, if it is one: its links are looked for
@@ -717,14 +746,24 @@ pub fn open_template(s: &mut Session, name: &str, bytes: &[u8], path: Option<&st
"only this Affinity file's embedded preview could be read: use File › Open to see it with its warning, or export SVG or PDF from Affinity first",
));
}
let policy_changed = apply_policies(&mut loaded);
let base = crate::cmd::links::LinkBase::file(path);
let links = crate::cmd::links::resolve(&mut loaded.doc, base, s.prefs.update_links == "automatically", s.prefs.low_res_proxy_eps);
open_loaded(s, loaded, links, path.map(str::to_string), &LoadOptions::default(), true)
open_loaded(s, loaded, links, path.map(str::to_string), &LoadOptions::default(), true, policy_changed)
}
/// Make a loaded file the new active document; `links` is what became of its linked files, and
/// `opts` are the options it was read with.
fn open_loaded(s: &mut Session, loaded: Loaded, links: Resolved, path: Option<String>, opts: &LoadOptions, as_template: bool) -> Result<Value> {
/// Make a loaded file the new active document; `links` is what became of its linked files, `opts`
/// are the options it was read with, and `policy_changed` says whether the color management
/// policies changed it (it then opens with unsaved changes).
fn open_loaded(
s: &mut Session,
loaded: Loaded,
links: Resolved,
path: Option<String>,
opts: &LoadOptions,
as_template: bool,
policy_changed: bool,
) -> Result<Value> {
let Loaded { mut doc, format, warnings, restored, converted, native_pdf_compatible, .. } =
loaded.with_grid_prefs(&s.prefs).with_unit_prefs(&s.prefs);
// EPS now has an ordinary Save path. Its preview, partial-program and damaged-editing
@@ -765,6 +804,9 @@ fn open_loaded(s: &mut Session, loaded: Loaded, links: Resolved, path: Option<St
let converted = saves_back && converted && s.prefs.append_converted;
let index = s.add_document(doc, path);
let st = s.doc_mut()?;
if policy_changed {
st.mark_unsaved();
}
if let Some(pdf) = native_pdf_compatible {
st.save_options.insert("pdfCompatible".into(), json!(pdf));
}
+12 -3
View File
@@ -1239,14 +1239,23 @@ fn revert(s: &mut Session, _: &Value) -> Result<Value> {
let bytes = read_file(&path).map_err(|e| super::access::naming_in_error(e, &path))?;
let Loaded { mut doc, native_pdf_compatible, .. } = load(&path, &bytes)?.with_grid_prefs(&s.prefs).with_unit_prefs(&s.prefs);
doc.template = false;
// The color management policies apply as when the file is opened.
let (notes, policy_changed) = crate::cmd::colormgmt::apply_open_policies(&mut doc);
// Back to the saved file: its recovery copy has nothing left to recover.
let uid = s.doc()?.uid;
crate::cmd::recovery::forget(s, uid);
s.replace_document(index, doc);
if policy_changed {
s.doc_mut()?.mark_unsaved();
}
if let Some(pdf) = native_pdf_compatible {
s.doc_mut()?.save_options.insert("pdfCompatible".into(), json!(pdf));
}
Ok(json!({ "path": path }))
let mut reply = json!({ "path": path });
if !notes.is_empty() {
reply["warnings"] = json!(notes);
}
Ok(reply)
}
/// `file.formatOptions`: a writable format's options with the values Save would use.
@@ -1345,7 +1354,7 @@ pub(super) fn specs() -> Vec<CommandSpec> {
"New from Template…",
["File"],
Some("Cmd+Shift+N"),
"{path} or {name, dataBase64}: open a template (or any readable file) as a new untitled document → {index, title, format, warnings}",
"{path} or {name, dataBase64}: open a template (or any readable file) as a new untitled document; the color management policies of edit.colorSettings apply as at document.open → {index, title, format, warnings (with the policies' notes)}",
always,
super::load::new_from_template
),
@@ -1354,7 +1363,7 @@ pub(super) fn specs() -> Vec<CommandSpec> {
"Revert",
["File"],
Some("F12"),
"{confirmed?: bool (the app's own: it asks first, then runs the command with it)} discard the changes: reload the saved file into the same tab (history cleared; the tab keeps its place and view). Saved, modified documents only; if the file can't be read the document is left as it is → {path}. The app asks first (a confirm dialog) unless confirmed: true",
"{confirmed?: bool (the app's own: it asks first, then runs the command with it)} discard the changes: reload the saved file into the same tab (history cleared; the tab keeps its place and view). Saved, modified documents only; if the file can't be read the document is left as it is → {path, warnings?: the notes of the color management policies of edit.colorSettings, which apply as at document.open}. The app asks first (a confirm dialog) unless confirmed: true",
can_revert,
revert
),
+167 -1
View File
@@ -823,7 +823,13 @@ fn color_settings_come_back_in_the_next_session() {
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 };
let chosen = cms::ColorSettings {
rgb: loaded.into(),
cmyk: cms::DEVICE_CMYK.into(),
intent: cms::Intent::Perceptual,
bpc: false,
..cms::ColorSettings::default()
};
assert_eq!(cms::active_settings(), chosen);
// The next start begins with the defaults and without the loaded profile, which the profile
@@ -865,3 +871,163 @@ fn saved_color_settings_that_cannot_be_used_leave_the_defaults() {
assert!(!saved.bpc && saved.cmyk == cms::GENERIC_CMYK, "written over by the next change: {saved:?}");
}
}
/// Edit › Color Settings' color management policies decide what opening a document tagged with
/// profiles other than the working spaces does with them: Preserve Embedded Profiles keeps them,
/// Off and Preserve Numbers take them off and keep the color numbers, and Convert to Working Space
/// converts the colors into the working spaces, keeping how they look, and takes them off. A
/// document a policy changed opens with unsaved changes, and the open reply names each profile.
/// A profile that is the working space stays, and File › Revert and File › New from Template apply
/// the policies too.
#[test]
fn color_management_policies_keep_take_off_or_convert_a_tagged_profile() {
let _g = settings();
let dir = fresh_dir("policies");
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 (blue, ink) = (rect(&mut s, 0.0, 0.0, 10.0, 10.0), rect(&mut s, 20.0, 0.0, 10.0, 10.0));
exec(&mut s, "paint.setFill", json!({"ids": [blue.0], "color": "#3366cc"}));
exec(&mut s, "paint.setFill", json!({"ids": [ink.0], "color": {"c": 20, "m": 40, "y": 60, "k": 10}}));
exec(&mut s, "edit.assignProfile", json!({"rgb": cms::WIDE_GAMUT_RGB, "cmyk": cms::DEVICE_CMYK}));
// An artboard background, a character style's fill and a freeform gradient swatch, colors
// outside the art.
let paper = vectorcraft_color::Color::Rgb { r: 0.9, g: 0.8, b: 0.6 };
let free = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
select(&mut s, &[free]);
exec(&mut s, "paint.setFill", json!({"gradient": {"kind": "freeform"}}));
exec(&mut s, "paint.freeform.setPoint", json!({"index": 0, "color": "#e6cc99", "stroke": false}));
{
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
d.artboards[0].color = Some(paper);
let mut style = vectorcraft_doc::TextStyleDef { name: "Inked".into(), attrs: Default::default() };
style.attrs.insert("fill".into(), serde_json::to_value(vectorcraft_color::Paint::solid(paper)).unwrap());
d.char_styles.push(style);
let paint = d.node(free).unwrap().appearance.fill().unwrap().paint.clone();
d.swatches.push(vectorcraft_color::Swatch { name: "Free".into(), paint, global: false, spot: false });
}
exec(&mut s, "file.saveAs", json!({"path": tagged.to_str().unwrap()}));
let settings_colors = |s: &Session| {
let d = &s.doc().unwrap().doc;
let style = serde_json::from_value::<vectorcraft_color::Paint>(d.char_styles[0].attrs["fill"].clone()).unwrap();
(d.artboards[0].color.unwrap(), style.color().unwrap())
};
// The first point of the freeform swatch and of the freeform fill.
let free_points = |s: &Session| {
let d = &s.doc().unwrap().doc;
let first = |p: &vectorcraft_color::Paint| match p {
vectorcraft_color::Paint::Gradient(g) => g.freeform.as_ref().map(|f| f.points[0].color),
_ => None,
};
let swatch = d.swatches.iter().find(|w| w.name == "Free").and_then(|w| first(&w.paint)).unwrap();
(swatch, first(&d.node(free).unwrap().appearance.fill().unwrap().paint).unwrap())
};
let (free0, _) = free_points(&s);
let fill = |s: &Session, id| s.doc().unwrap().doc.node(id).and_then(|n| n.appearance.fill()).and_then(|f| f.paint.color()).unwrap();
let (blue0, ink0) = (fill(&s, blue), fill(&s, ink));
let source = cms::for_document(Some(cms::WIDE_GAMUT_RGB), Some(cms::DEVICE_CMYK));
let notes = |r: &serde_json::Value| r["warnings"].as_array().unwrap().iter().filter(|w| w.as_str().unwrap().contains("profile")).count();
// A session that opened the file under the policies, and the open reply.
let open = |rgb: &str, cmyk: &str| {
let mut s = session();
exec(&mut s, "edit.colorSettings", json!({"rgbPolicy": rgb, "cmykPolicy": cmyk}));
let r = exec(&mut s, "document.open", json!({"path": tagged.to_str().unwrap()}));
(r, s)
};
let (r, kept) = open("preserveEmbedded", "preserveEmbedded");
assert_eq!(doc_profiles(&kept.doc().unwrap().doc), (Some(cms::WIDE_GAMUT_RGB.into()), Some(cms::DEVICE_CMYK.into())));
assert_eq!((fill(&kept, blue), fill(&kept, ink), notes(&r)), (blue0, ink0, 0));
assert!(!kept.doc().unwrap().is_dirty());
for (rgb, cmyk) in [("off", "off"), ("off", "preserveNumbers")] {
let (r, off) = open(rgb, cmyk);
assert_eq!(doc_profiles(&off.doc().unwrap().doc), (None, None), "{cmyk}");
assert_eq!((fill(&off, blue), fill(&off, ink), notes(&r)), (blue0, ink0, 2), "{cmyk}: the numbers stay");
assert!(off.doc().unwrap().is_dirty(), "{cmyk}: the document changed");
}
let (r, converted) = open("convert", "convert");
assert_eq!(doc_profiles(&converted.doc().unwrap().doc), (None, None));
assert_eq!(notes(&r), 2);
let working = cms::working();
let (blue1, ink1) = (fill(&converted, blue), fill(&converted, ink));
assert!(blue1 != blue0 && ink1 != ink0, "the numbers change: {blue1:?} {ink1:?}");
assert!(cms::delta_e2000(source.lab(&blue0), working.lab(&blue1)) < 1.0, "{blue0:?} → {blue1:?}");
assert!(cms::delta_e2000(source.lab(&ink0), working.lab(&ink1)) < 3.0, "{ink0:?} → {ink1:?}");
assert!(converted.doc().unwrap().is_dirty());
let (board, styled) = settings_colors(&converted);
assert_eq!(board, styled, "the artboard and the style convert alike");
assert!(board != paper && cms::delta_e2000(source.lab(&paper), working.lab(&board)) < 1.0, "{paper:?} → {board:?}");
assert_eq!(settings_colors(&kept), (paper, paper), "kept with the profile");
let (swatch_point, fill_point) = free_points(&converted);
assert_eq!(swatch_point, fill_point, "the freeform swatch and fill convert alike");
assert!(swatch_point != free0 && cms::delta_e2000(source.lab(&free0), working.lab(&swatch_point)) < 1.0, "{free0:?} → {swatch_point:?}");
// A color mode asked for on open converts the colors the policy converted, through the working
// spaces, also when the document in front draws through another profile.
let mut s2 = session();
exec(&mut s2, "edit.assignProfile", json!({"rgb": cms::DISPLAY_P3}));
exec(&mut s2, "edit.colorSettings", json!({"rgbPolicy": "convert", "cmykPolicy": "convert"}));
exec(&mut s2, "document.open", json!({"path": tagged.to_str().unwrap(), "colorMode": "cmyk"}));
let intent = working.settings().intent;
let want = working.convert(&blue1, cms::Model::Cmyk, intent);
let got = fill(&s2, blue);
assert!(cms::delta_e2000(working.lab(&want), working.lab(&got)) < 0.01, "{want:?} vs {got:?}");
// A profile that is the working space is kept whatever the policy.
exec(&mut s, "edit.colorSettings", json!({"rgb": cms::WIDE_GAMUT_RGB}));
let (r, same) = open("off", "off");
assert_eq!(doc_profiles(&same.doc().unwrap().doc), (Some(cms::WIDE_GAMUT_RGB.into()), None));
assert_eq!(notes(&r), 1);
exec(&mut s, "edit.colorSettings", json!({"rgb": cms::SRGB}));
// File › Revert reads the file again under the policies.
let (_, mut reverted) = open("preserveEmbedded", "preserveEmbedded");
exec(&mut reverted, "paint.setFill", json!({"ids": [blue.0], "color": "#ff0000"}));
exec(&mut reverted, "edit.colorSettings", json!({"cmykPolicy": "preserveNumbers"}));
let r = exec(&mut reverted, "file.revert", json!({"confirmed": true}));
assert_eq!(doc_profiles(&reverted.doc().unwrap().doc), (Some(cms::WIDE_GAMUT_RGB.into()), None));
assert_eq!((fill(&reverted, blue), notes(&r)), (blue0, 1));
// File › New from Template applies the policies as opening does.
let mut from_template = session();
exec(&mut from_template, "edit.colorSettings", json!({"rgbPolicy": "convert", "cmykPolicy": "off"}));
let r = exec(&mut from_template, "file.newFromTemplate", json!({"path": tagged.to_str().unwrap()}));
assert_eq!(doc_profiles(&from_template.doc().unwrap().doc), (None, None));
assert_eq!(notes(&r), 2);
assert_eq!(fill(&from_template, ink), ink0, "Off keeps the CMYK numbers");
let blue2 = fill(&from_template, blue);
assert!(blue2 != blue0 && cms::delta_e2000(source.lab(&blue0), working.lab(&blue2)) < 1.0, "{blue0:?} → {blue2:?}");
assert!(from_template.doc().unwrap().is_dirty());
}
/// The policies are checked, come back in the reply and in `Color Settings.json`, and a saved file
/// that gives the RGB policy Preserve Numbers, a CMYK policy only, leaves the defaults.
#[test]
fn color_management_policies_are_checked_and_kept() {
let _g = settings();
let dir = fresh_dir("policies-kept");
let _reset = ColorSettingsReset { profile: None, paths: vec![dir.clone()] };
let file = dir.join("Color Settings.json");
let mut s = session();
s.attach_color_settings(file.clone());
for bad in [json!({"rgbPolicy": "preserveNumbers"}), json!({"cmykPolicy": "keep"}), json!({"rgbPolicy": 1})] {
assert!(s.execute("edit.colorSettings", &bad).is_err(), "{bad}");
}
assert!(!file.exists(), "a refused change keeps nothing");
let r = exec(&mut s, "edit.colorSettings", json!({"rgbPolicy": "convert", "cmykPolicy": "preserveNumbers"}));
assert_eq!((r["rgbPolicy"].as_str(), r["cmykPolicy"].as_str()), (Some("convert"), Some("preserveNumbers")));
let saved: cms::ColorSettings = serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
assert_eq!((saved.rgb_policy, saved.cmyk_policy), (cms::Policy::Convert, cms::Policy::PreserveNumbers));
cms::set_active(&cms::ColorSettings::default()).unwrap();
std::fs::write(
&file,
json!({"rgb": cms::SRGB, "cmyk": cms::GENERIC_CMYK, "intent": "relative", "bpc": true, "rgbPolicy": "preserveNumbers"}).to_string(),
)
.unwrap();
let mut next = Session::new();
next.attach_color_settings(file);
assert_eq!(cms::active_settings(), cms::ColorSettings::default());
}
+5
View File
@@ -5053,6 +5053,11 @@ axis Both И двете
@msg Imported library {name} Импортирана библиотека {name}
RGB: RGB:
CMYK: CMYK:
RGB Policy: Правило за RGB:
CMYK Policy: Правило за CMYK:
Preserve Embedded Profiles Запазване на вградените профили
Convert to Working Space Преобразуване в работното пространство
Preserve Numbers Запазване на числата
Texture Текстура
Craquelure… Кракелюр…
Grain… Зърнистост…
Can't render this file because it is too large.
+5
View File
@@ -986,6 +986,11 @@
NTSC Colors NTSC boje
RGB: RGB:
CMYK: CMYK:
RGB Policy: RGB pravilo:
CMYK Policy: CMYK pravilo:
Preserve Embedded Profiles Zadrži ugrađene profile
Convert to Working Space Pretvori u radni prostor
Preserve Numbers Zadrži brojeve
CMYK CMYK
Texture Tekstura
Craquelure… Krakelura…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -765,6 +765,11 @@ antiAlias Sharp Nítid
CMYK Equivalents Equivalents CMYK
CMYK profile in effect Perfil CMYK en vigor
CMYK: CMYK:
RGB Policy: Política RGB:
CMYK Policy: Política CMYK:
Preserve Embedded Profiles Conserva els perfils incrustats
Convert to Working Space Converteix a l'espai de treball
Preserve Numbers Conserva els números
Coarse Dots Punts gruixuts
Collapse Panel Group Replega el grup de panells
Collapse Panels to Icons Replega els panells a icones
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -1005,6 +1005,11 @@
NTSC Colors Barvy NTSC
RGB: RGB:
CMYK: CMYK:
RGB Policy: Zásada RGB:
CMYK Policy: Zásada CMYK:
Preserve Embedded Profiles Zachovat vložené profily
Convert to Working Space Převést do pracovního prostoru
Preserve Numbers Zachovat čísla
CMYK CMYK
Texture Textura
Craquelure… Krakelování…
Can't render this file because it is too large.
+5
View File
@@ -1538,6 +1538,11 @@
NTSC Colors NTSC-farver
RGB: RGB:
CMYK: CMYK:
RGB Policy: RGB-politik:
CMYK Policy: CMYK-politik:
Preserve Embedded Profiles Bevar indlejrede profiler
Convert to Working Space Konverter til arbejdsfarverum
Preserve Numbers Bevar tal
CMYK CMYK
Texture Tekstur
Craquelure… Krakelering…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -5381,6 +5381,11 @@ theme Appearance Erscheinungsbild
The key color becomes the stroke color. The other colors shift their hue by the same amount and keep their saturation and brightness. Die Hauptfarbe wird zur Konturfarbe. Die anderen Farben wandern ebenso weit um den Farbkreis wie die Hauptfarbe und behalten ihre Sättigung und Helligkeit.
RGB: RGB:
CMYK: CMYK:
RGB Policy: RGB-Richtlinie:
CMYK Policy: CMYK-Richtlinie:
Preserve Embedded Profiles Eingebettete Profile beibehalten
Convert to Working Space In Arbeitsfarbraum konvertieren
Preserve Numbers Werte beibehalten
# PSD files.
@msg not a PSD file keine PSD-Datei
@msg Multichannel PSD files can't be read: save it in RGB, CMYK or Grayscale mode PSD-Dateien im Mehrkanalmodus können nicht gelesen werden: Speichern Sie die Datei im RGB-, CMYK- oder Graustufenmodus
Can't render this file because it is too large.
+5
View File
@@ -5353,6 +5353,11 @@ digits Malayalam Μαλαγιάλαμ
@msg Imported library {name} Εισήχθη η βιβλιοθήκη {name}
RGB: RGB:
CMYK: CMYK:
RGB Policy: Πολιτική RGB:
CMYK Policy: Πολιτική CMYK:
Preserve Embedded Profiles Διατήρηση ενσωματωμένων προφίλ
Convert to Working Space Μετατροπή στον χώρο εργασίας
Preserve Numbers Διατήρηση αριθμών
System Title Bar Γραμμή τίτλου συστήματος
VectorCraft Title Bar on Every Desktop Γραμμή τίτλου του VectorCraft σε κάθε επιφάνεια εργασίας
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Όσο είναι ορισμένη, η μεταβλητή περιβάλλοντος VECTORCRAFT_TITLEBAR επιλέγει τη γραμμή τίτλου.
Can't render this file because it is too large.
+5
View File
@@ -5399,6 +5399,11 @@ axis Both Ambos
@msg Imported library {name} Biblioteca {name} importada
RGB: RGB:
CMYK: CMYK:
RGB Policy: Norma RGB:
CMYK Policy: Norma CMYK:
Preserve Embedded Profiles Conservar perfiles incrustados
Convert to Working Space Convertir en espacio de trabajo
Preserve Numbers Conservar números
Texture Textura
Craquelure… Craquelado…
Grain… Granulado…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -989,6 +989,11 @@
NTSC Colors NTSC-värit
RGB: RGB:
CMYK: CMYK:
RGB Policy: RGB-käytäntö:
CMYK Policy: CMYK-käytäntö:
Preserve Embedded Profiles Säilytä upotetut profiilit
Convert to Working Space Muunna työväriavaruuteen
Preserve Numbers Säilytä lukuarvot
CMYK CMYK
Texture Tekstuuri
Craquelure… Krakelyyri…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -5404,6 +5404,11 @@ axis Both Les deux
@msg Imported library {name} Bibliothèque {name} importée
RGB: RVB :
CMYK: CMJN :
RGB Policy: Règle RVB :
CMYK Policy: Règle CMJN :
Preserve Embedded Profiles Conserver les profils incorporés
Convert to Working Space Convertir en espace de travail
Preserve Numbers Conserver les valeurs numériques
Texture Texture
Craquelure… Craquelure…
Grain… Grain…
Can't render this file because it is too large.
+5
View File
@@ -472,6 +472,11 @@
CMYK Images CMYK ઇમેજ
CMYK colours print with their own values; off, they go through the colour settings too. CMYK રંગો પોતાની કિંમતો સાથે છાપે છે; બંધ હોય ત્યારે તે રંગ સેટિંગમાંથી પણ પસાર થાય છે.
CMYK: CMYK:
RGB Policy: RGB નીતિ:
CMYK Policy: CMYK નીતિ:
Preserve Embedded Profiles એમ્બેડ કરેલી પ્રોફાઇલ જાળવો
Convert to Working Space કાર્યકારી સ્પેસમાં રૂપાંતરિત કરો
Preserve Numbers સંખ્યાઓ જાળવો
CSS Properties CSS ગુણધર્મો
Calligraphic Brush કેલિગ્રાફિક બ્રશ
Calligraphic Brush Options કેલિગ્રાફિક બ્રશ વિકલ્પો
Can't render this file because it contains an unexpected character in line 72 and column 5.
+5
View File
@@ -892,6 +892,11 @@ antiAlias Sharp शार्प
CMYK Images CMYK इमेज
CMYK profile in effect इफ़ेक्ट में CMYK प्रोफ़ाइल
CMYK: CMYK:
RGB Policy: RGB नीति:
CMYK Policy: CMYK नीति:
Preserve Embedded Profiles एम्बेड की गई प्रोफ़ाइल बनाए रखें
Convert to Working Space वर्किंग स्पेस में बदलें
Preserve Numbers संख्याएँ बनाए रखें
Coarse Dots मोटे बिंदु
Coincidence: संयोग:
Collapse Panel Group पैनल समूह संक्षिप्त करें
Can't render this file because it is too large.
+5
View File
@@ -696,6 +696,11 @@ font-technology Variable Változó
CMYK Equivalents CMYK-megfelelők
CMYK profile in effect Érvényes CMYK-profil
CMYK: CMYK:
RGB Policy: RGB-házirend:
CMYK Policy: CMYK-házirend:
Preserve Embedded Profiles Beágyazott profilok megtartása
Convert to Working Space Konvertálás a munkaszíntérbe
Preserve Numbers Számértékek megtartása
Coarse Dots Durva pontok
Collapse Panels to Icons Panelek ikonná csukása
Collapse to Icons Ikonná csukás
Can't render this file because it is too large.
+5
View File
@@ -928,6 +928,11 @@
NTSC Colors Warna NTSC
RGB: RGB:
CMYK: CMYK:
RGB Policy: Kebijakan RGB:
CMYK Policy: Kebijakan CMYK:
Preserve Embedded Profiles Pertahankan Profil Tersemat
Convert to Working Space Konversi ke Ruang Kerja
Preserve Numbers Pertahankan Angka
CMYK CMYK
Texture Tekstur
Craquelure… Craquelure…
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
928
929
930
931
932
933
934
935
936
937
938
+5
View File
@@ -5449,6 +5449,11 @@ axis Both Entrambi
@msg Imported library {name} Libreria {name} importata
RGB: RGB:
CMYK: CMYK:
RGB Policy: Criterio RGB:
CMYK Policy: Criterio CMYK:
Preserve Embedded Profiles Mantieni profili incorporati
Convert to Working Space Converti nello spazio di lavoro
Preserve Numbers Mantieni valori numerici
Texture Texture
Craquelure… Craquelé…
Grain… Granulosità…
Can't render this file because it is too large.
+5
View File
@@ -5490,6 +5490,11 @@ swatch Bright Violet ビビッドバイオレット
@msg Imported library {name} ライブラリ {name} を読み込みました
RGB: RGB:
CMYK: CMYK:
RGB Policy: RGB ポリシー:
CMYK Policy: CMYK ポリシー:
Preserve Embedded Profiles 埋め込まれたプロファイルを保持
Convert to Working Space 作業用スペースに変換
Preserve Numbers カラー値を保持
Texture テクスチャ
Craquelure… クラッキング…
Grain… 粒状…
Can't render this file because it is too large.
+5
View File
@@ -894,6 +894,11 @@
NTSC Colors NTSC ფერები
RGB: RGB:
CMYK: CMYK:
RGB Policy: RGB წესი:
CMYK Policy: CMYK წესი:
Preserve Embedded Profiles ჩაშენებული პროფილების შენარჩუნება
Convert to Working Space სამუშაო სივრცეში გადაყვანა
Preserve Numbers რიცხვების შენარჩუნება
CMYK CMYK
Texture ტექსტურა
Craquelure… კრაკელიური…
Can't render this file because it is too large.
+5
View File
@@ -5417,6 +5417,11 @@ theme Appearance 외관
@msg Imported library {name} 라이브러리 {name}을(를) 가져왔습니다
RGB: RGB:
CMYK: CMYK:
RGB Policy: RGB 정책:
CMYK Policy: CMYK 정책:
Preserve Embedded Profiles 포함된 프로필 유지
Convert to Working Space 작업 공간으로 변환
Preserve Numbers 숫자 유지
Texture 텍스처
Craquelure… 크랙…
Grain… 그레인…
Can't render this file because it is too large.
+5
View File
@@ -1446,6 +1446,11 @@
NTSC Colors NTSC-kleuren
RGB: RGB:
CMYK: CMYK:
RGB Policy: RGB-beleid:
CMYK Policy: CMYK-beleid:
Preserve Embedded Profiles Ingesloten profielen behouden
Convert to Working Space Omzetten naar werkruimte
Preserve Numbers Waarden behouden
CMYK CMYK
Texture Textuur
Craquelure… Craquelure…
Can't render this file because it is too large.
+5
View File
@@ -946,6 +946,11 @@
NTSC Colors Kolory NTSC
RGB: RGB:
CMYK: CMYK:
RGB Policy: Zasada RGB:
CMYK Policy: Zasada CMYK:
Preserve Embedded Profiles Zachowaj osadzone profile
Convert to Working Space Konwertuj do przestrzeni roboczej
Preserve Numbers Zachowaj wartości liczbowe
CMYK CMYK
Texture Tekstura
Craquelure… Spękania…
Can't render this file because it is too large.
+5
View File
@@ -3506,6 +3506,11 @@ imageTrace Outline Contorno
NTSC Colors Cores NTSC
RGB: RGB:
CMYK: CMYK:
RGB Policy: Política RGB:
CMYK Policy: Política CMYK:
Preserve Embedded Profiles Preservar perfis incorporados
Convert to Working Space Converter para espaço de trabalho
Preserve Numbers Preservar números
Texture Textura
Craquelure… Craquelê…
Grain… Granulação…
Can't render this file because it is too large.
+5
View File
@@ -5438,6 +5438,11 @@ axis Both Оба
@msg Imported library {name} Импортирована библиотека {name}
RGB: RGB:
CMYK: CMYK:
RGB Policy: Правило RGB:
CMYK Policy: Правило CMYK:
Preserve Embedded Profiles Сохранять встроенные профили
Convert to Working Space Преобразовывать в рабочее пространство
Preserve Numbers Сохранять числа
Texture Текстура
Craquelure… Кракелюры…
Grain… Зерно…
Can't render this file because it is too large.
+5
View File
@@ -5306,6 +5306,11 @@ axis Both Båda
@msg Imported library {name} Importerade biblioteket {name}
RGB: RGB:
CMYK: CMYK:
RGB Policy: RGB-princip:
CMYK Policy: CMYK-princip:
Preserve Embedded Profiles Behåll inbäddade profiler
Convert to Working Space Konvertera till arbetsfärgrymd
Preserve Numbers Behåll värden
Texture Textur
Craquelure… Krakelering…
Grain… Korn…
Can't render this file because it is too large.
+5
View File
@@ -5204,6 +5204,11 @@ axis Both Her İkisi
@msg Imported library {name} İçe aktarılan kitaplık {name}
RGB: RGB:
CMYK: CMYK:
RGB Policy: RGB İlkesi:
CMYK Policy: CMYK İlkesi:
Preserve Embedded Profiles Gömülü Profilleri Koru
Convert to Working Space Çalışma Alanına Dönüştür
Preserve Numbers Sayıları Koru
Texture Doku
Craquelure… Küçük çatlak…
Grain… Tahıl…
Can't render this file because it is too large.
+5
View File
@@ -5433,6 +5433,11 @@ axis Both Обидві
@msg Imported library {name} Імпортовано бібліотеку {name}
RGB: RGB:
CMYK: CMYK:
RGB Policy: Правило RGB:
CMYK Policy: Правило CMYK:
Preserve Embedded Profiles Зберігати вбудовані профілі
Convert to Working Space Перетворювати в робочий простір
Preserve Numbers Зберігати числа
Texture Текстура
Craquelure… Кракелюри…
Grain… Зерно…
Can't render this file because it is too large.
+5
View File
@@ -608,6 +608,11 @@ font-technology Variable Biến thiên
CMYK Equivalents Tương đương CMYK
CMYK profile in effect Hồ sơ CMYK đang có hiệu lực
CMYK: CMYK:
RGB Policy: Chính sách RGB:
CMYK Policy: Chính sách CMYK:
Preserve Embedded Profiles Giữ cấu hình nhúng
Convert to Working Space Chuyển sang không gian làm việc
Preserve Numbers Giữ giá trị số
Coarse Dots Chấm thô
Collapse Panels to Icons Thu gọn bảng thành biểu tượng
Collapse to Icons Thu gọn thành biểu tượng
Can't render this file because it contains an unexpected character in line 205 and column 5.
+5
View File
@@ -5335,6 +5335,11 @@ imageTrace Outline 轮廓
None (Color Preview) 无 (颜色预览)
Preserve 保留
CMYK: CMYK:
RGB Policy: RGB 策略:
CMYK Policy: CMYK 策略:
Preserve Embedded Profiles 保留嵌入的配置文件
Convert to Working Space 转换为工作空间
Preserve Numbers 保留数值
Art & Illustration 艺术和插画
Branding 品牌
Film & Video 影片和视频
Can't render this file because it is too large.
+5
View File
@@ -5567,6 +5567,11 @@ axis Both 兩者
Interpolation 內插
RGB: RGB:
CMYK: CMYK:
RGB Policy: RGB 原則:
CMYK Policy: CMYK 原則:
Preserve Embedded Profiles 保留內嵌描述檔
Convert to Working Space 轉換為工作空間
Preserve Numbers 保留數值
Texture 紋理
Craquelure… 龜裂紋…
Grain… 顆粒…
Can't render this file because it is too large.
+31 -4
View File
@@ -1,11 +1,12 @@
//! Separations Preview panel: process plates C/M/Y/K plus spot plates with eye toggles (one visible
//! plate renders as greyscale ink coverage; Alt-click an eye to show only that plate), and the
//! colour-management controls behind Edit → Color Settings… / Assign Profile… and View → Proof
//! Setup (working spaces, intent, black-point compensation, proof target, gamut check).
//! Setup (working spaces, color management policies, intent, black-point compensation, proof
//! target, gamut check).
use egui::{Sense, Ui, vec2};
use serde_json::{Value, json};
use vectorcraft_color::cms::{self, Intent, ProfileKind};
use vectorcraft_color::cms::{self, Intent, Policy, ProfileKind};
use vectorcraft_render::proof;
use crate::VectorcraftApp;
@@ -100,8 +101,9 @@ fn settings_section(app: &mut VectorcraftApp, ui: &mut Ui) {
}
}
/// The section's rows: the RGB and CMYK working spaces, intent, black point compensation, Load
/// Profile… and Gamut Check, and the profile the active document is assigned.
/// The section's rows: the RGB and CMYK working spaces, intent, black point compensation, the RGB
/// and CMYK color management policies, Load Profile… and Gamut Check, and the profile the active
/// document is assigned.
fn settings_rows(app: &mut VectorcraftApp, ui: &mut Ui) {
let st = cms::working_settings();
widgets::section_header(ui, tl!("Color Settings"));
@@ -127,6 +129,21 @@ fn settings_rows(app: &mut VectorcraftApp, ui: &mut Ui) {
if widgets::check(ui, tl!("Use Black Point Compensation"), st.bpc, true) {
run(app, "edit.colorSettings", json!({"bpc": !st.bpc}));
}
// Color management policies: what opening a document does with a profile it is tagged with
// that is not the working space.
let policies: [(&str, &str, Policy, &[Policy]); 2] =
[("rgbPolicy", tl!("RGB Policy:"), st.rgb_policy, &Policy::RGB), ("cmykPolicy", tl!("CMYK Policy:"), st.cmyk_policy, &Policy::CMYK)];
for (key, label, current, choices) in policies {
ui.horizontal(|ui| {
ui.label(label);
let labels: Vec<&str> = choices.iter().map(|p| policy_label(*p)).collect();
if let Some(i) = widgets::dropdown_names(ui, key, policy_label(current), &labels, 210.0)
&& let Some(p) = choices.get(i)
{
run(app, "edit.colorSettings", json!({ key: p.id() }));
}
});
}
ui.horizontal(|ui| {
if widgets::flat_button(ui, tl!("Load Profile…"), 110.0).clicked() {
// Its file dialog, shown off the UI thread, loads the profile when it answers.
@@ -243,6 +260,16 @@ fn assign_rows(app: &mut VectorcraftApp, ui: &mut Ui, row: &AssignRow) {
}
}
/// The name of color management policy `p` in the UI language.
fn policy_label(p: Policy) -> &'static str {
match p {
Policy::Off => tl!("Off"),
Policy::PreserveEmbedded => tl!("Preserve Embedded Profiles"),
Policy::Convert => tl!("Convert to Working Space"),
Policy::PreserveNumbers => tl!("Preserve Numbers"),
}
}
/// The status bar's line after Gamut Check finds `n` colors outside the CMYK gamut.
pub(crate) fn gamut_check_status(n: usize) -> String {
if n == 0 {
+12 -4
View File
@@ -1666,10 +1666,18 @@ 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.
- **Color Settings** (`edit.colorSettings`): the RGB and CMYK working spaces, intent, black point
compensation and color management policies 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; settings saved without policies take the default, `preserveEmbedded`. `rgbPolicy` (`off`,
`preserveEmbedded`, `convert`) and `cmykPolicy` (the same and `preserveNumbers`) decide what `document.open`,
`file.newFromTemplate` and `file.revert` do with an RGB or CMYK profile the file is tagged with that is not the
working space: keep it, take it off and keep the color numbers (`off`, `preserveNumbers`), or convert the colors
of that model into the working space with the intent and black point compensation and take it off (`convert`; a
profile that is not available stays). The reply's `warnings` name each profile, and a document a policy changed
opens with unsaved changes. A profile that is the working space stays whatever the policy. `document.open`'s
`colorMode` converts the colors after the policies, through the profiles the document keeps.
`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 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
+1
View File
@@ -758,6 +758,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Translucent art blended before the display profile:** with Use Monitor Profile for Canvas converting, translucent art and the anti-aliased edges of art over an artboard's paper, a colored artboard or the transparency grid are blended with it in sRGB and the blend is converted, so the monitor's profile translates the document's colors as they are composited ([the help page](https://help.adobe.com/archive/en_US/illustrator/cs4/illustrator_cs4_help.pdf#page=151)). The frame rendered whole, the part an edit renders again, the render worker's frame, the pan fill and the other windows' panes do the same; over the pasteboard and in Outline the art is converted on its own, and with no profile to convert into nothing changes. Tests `display_profile::tests::translucent_pixels_over_the_paper_are_blended_before_they_are_converted`, `the_transparency_grid_is_blended_cell_by_cell`, `the_backdrop_is_found_through_the_view` and `canvas::tests::translucent_art_over_the_paper_is_blended_before_the_display_profile`.
- **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. With Window › Arrange › Tile or floating document windows, each document behind the front one draws its art, paper and transparency grid through its own assigned profiles, and loading or removing a profile redraws the documents assigned it; placed documents inside it still draw through the front document's profiles. How documents behind the front one draw is VectorCraft's own rule. 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`.
- **Color management policies:** Edit › Color Settings… (the Separations panel's Color Settings section and `edit.colorSettings`) has an RGB and a CMYK policy: Off, Preserve Embedded Profiles and Convert to Working Space, and Preserve Numbers for CMYK. When a document tagged with a profile other than the working space is opened, reverted or opened as a template, the policy keeps the profile, takes it off with the color numbers kept, or converts that model's colors into the working space, with black point compensation as Color Settings set it, and takes it off. The policies are kept in `Color Settings.json`. Preserve Embedded Profiles as the default for both, and the per-model policy for a document tagged with both an RGB and a CMYK profile, are VectorCraft's own rules; placed images and the Profile Mismatches and Missing Profiles prompts are not covered. Public reference: [Color management policy options](https://help.adobe.com/archive/en_US/illustrator/cs4/illustrator_cs4_help.pdf#page=157). Tests in `engine/tests/color_settings.rs`, `color/src/cms/tests.rs` and `color/src/cms/tests_icc.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`.
- **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).