mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 16:33:22 -06:00
M3.7: neutral paint, effect and stroke labels
The wide-gamut RGB working space is now "Wide Gamut RGB (1998 primaries)"; its old name still resolves everywhere (cms::profile / canonical_name, Color Settings, Assign Profile, documents and settings saved earlier). The Effect menu section is "Vector Effects", stroke width profiles are Lens / Taper Start / Taper End, dash, arrowhead-tip and gradient-on-stroke tooltips are reworded, the Window library submenus hold neutral disabled placeholders until the generated libraries land, and the Swatches menu offers "Save Swatch Library…" (disabled). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d99853ed2a
commit
016fcbf34e
@@ -125,7 +125,7 @@ impl IccProfile {
|
||||
/// Built-in RGB working spaces from `moxcms`'s standard primaries.
|
||||
pub fn builtin_rgb(name: &str) -> Option<IccProfile> {
|
||||
let p = match name {
|
||||
super::ADOBE_RGB => ColorProfile::new_adobe_rgb(),
|
||||
super::WIDE_GAMUT_RGB => ColorProfile::new_adobe_rgb(),
|
||||
super::DISPLAY_P3 => ColorProfile::new_display_p3(),
|
||||
super::PROPHOTO_RGB => ColorProfile::new_pro_photo_rgb(),
|
||||
super::SRGB => ColorProfile::new_srgb(),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Colour management: working spaces, RGB ↔ CMYK ↔ Lab conversions with rendering intents,
|
||||
//! gamut checks and soft-proof transforms.
|
||||
//!
|
||||
//! * **RGB working spaces** are ICC matrix/shaper profiles via [`moxcms`] (sRGB, Adobe RGB (1998)
|
||||
//! compatible, Display P3, ProPhoto RGB) or user `.icc` files. Display output is sRGB.
|
||||
//! * **RGB working spaces** are ICC matrix/shaper profiles via [`moxcms`] (sRGB, Wide Gamut RGB
|
||||
//! (1998 primaries), Display P3, ProPhoto RGB) or user `.icc` files. Display output is sRGB.
|
||||
//! * **CMYK spaces:** [`GENERIC_CMYK`] is our own documented parametric press model
|
||||
//! ([`generic`]); [`DEVICE_CMYK`] is the old profile-free formula (kept for exact legacy numbers);
|
||||
//! any CMYK `.icc` the user loads goes through `moxcms` (LUT-based profiles).
|
||||
@@ -25,7 +25,8 @@ pub use lab::{Lab, delta_e76, delta_e2000};
|
||||
use crate::Color;
|
||||
|
||||
pub const SRGB: &str = "sRGB IEC61966-2.1";
|
||||
pub const ADOBE_RGB: &str = "Adobe RGB (1998) compatible";
|
||||
/// The wide-gamut RGB space with the 1998 primaries (γ 2.2, D65).
|
||||
pub const WIDE_GAMUT_RGB: &str = "Wide Gamut RGB (1998 primaries)";
|
||||
pub const DISPLAY_P3: &str = "Display P3";
|
||||
pub const PROPHOTO_RGB: &str = "ProPhoto RGB";
|
||||
/// Our parametric press model (see [`generic`]). Not an Adobe/ECI profile.
|
||||
@@ -33,6 +34,14 @@ pub const GENERIC_CMYK: &str = "VectorCraft Generic CMYK (SWOP-like)";
|
||||
/// Profile-free CMYK (`rgb = (1−c)(1−k)` …): uncalibrated, kept for legacy numbers.
|
||||
pub const DEVICE_CMYK: &str = "Device CMYK (uncalibrated)";
|
||||
|
||||
/// Built-in RGB and CMYK working spaces, in menu order.
|
||||
const BUILTIN_RGB: [&str; 4] = [SRGB, WIDE_GAMUT_RGB, DISPLAY_P3, PROPHOTO_RGB];
|
||||
const BUILTIN_CMYK: [&str; 2] = [GENERIC_CMYK, DEVICE_CMYK];
|
||||
|
||||
/// Names earlier versions gave built-in profiles: (old name, current name). Settings, documents
|
||||
/// and commands that still use an old name get the current profile.
|
||||
const LEGACY_NAMES: &[(&str, &str)] = &[("Adobe RGB (1998) compatible", WIDE_GAMUT_RGB)]; // brand-ok: legacy alias
|
||||
|
||||
/// ΔE2000 above which a colour counts as out of the CMYK gamut (the gamut warning).
|
||||
pub const GAMUT_THRESHOLD: f32 = 2.0;
|
||||
|
||||
@@ -169,7 +178,7 @@ fn user_profile(name: &str) -> Option<Arc<IccProfile>> {
|
||||
}
|
||||
|
||||
fn cmyk_space(name: &str) -> Result<CmykSpace, CmsError> {
|
||||
match name {
|
||||
match canonical_name(name) {
|
||||
GENERIC_CMYK => Ok(CmykSpace::Generic(generic())),
|
||||
DEVICE_CMYK => Ok(CmykSpace::Device),
|
||||
_ => match user_profile(name) {
|
||||
@@ -194,12 +203,23 @@ fn rgb_space(name: &str) -> Result<RgbSpace, CmsError> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The current name of a profile: names earlier versions used resolve to today's name.
|
||||
pub fn canonical_name(name: &str) -> &str {
|
||||
LEGACY_NAMES.iter().find(|(old, _)| *old == name).map_or(name, |(_, new)| new)
|
||||
}
|
||||
|
||||
/// An available profile by its current or legacy name.
|
||||
pub fn profile(name: &str) -> Option<ProfileInfo> {
|
||||
let name = canonical_name(name);
|
||||
profiles().into_iter().find(|p| p.name == name)
|
||||
}
|
||||
|
||||
/// Every profile available (built-in first, then user-loaded).
|
||||
pub fn profiles() -> Vec<ProfileInfo> {
|
||||
let mut v: Vec<ProfileInfo> = [SRGB, ADOBE_RGB, DISPLAY_P3, PROPHOTO_RGB]
|
||||
let mut v: Vec<ProfileInfo> = BUILTIN_RGB
|
||||
.iter()
|
||||
.map(|n| ProfileInfo { name: (*n).into(), kind: ProfileKind::Rgb, builtin: true })
|
||||
.chain([GENERIC_CMYK, DEVICE_CMYK].iter().map(|n| ProfileInfo { name: (*n).into(), kind: ProfileKind::Cmyk, builtin: true }))
|
||||
.chain(BUILTIN_CMYK.iter().map(|n| ProfileInfo { name: (*n).into(), kind: ProfileKind::Cmyk, builtin: true }))
|
||||
.collect();
|
||||
for p in USER.read().unwrap_or_else(|e| e.into_inner()).iter() {
|
||||
v.push(ProfileInfo { name: p.name.clone(), kind: p.kind, builtin: false });
|
||||
@@ -214,7 +234,8 @@ pub fn register_icc(bytes: &[u8], name: Option<String>) -> Result<ProfileInfo, C
|
||||
if p.kind == ProfileKind::Gray {
|
||||
return Err(CmsError::Unsupported("grayscale profiles can't be working spaces yet".into()));
|
||||
}
|
||||
if [SRGB, ADOBE_RGB, DISPLAY_P3, PROPHOTO_RGB, GENERIC_CMYK, DEVICE_CMYK].contains(&p.name.as_str()) {
|
||||
let builtin = canonical_name(&p.name);
|
||||
if BUILTIN_RGB.contains(&builtin) || BUILTIN_CMYK.contains(&builtin) {
|
||||
return Err(CmsError::Unsupported(format!("{} clashes with a built-in profile name", p.name)));
|
||||
}
|
||||
if !p.usable() {
|
||||
@@ -257,8 +278,10 @@ impl Default for Cms {
|
||||
}
|
||||
|
||||
impl Cms {
|
||||
/// Profiles named by a legacy name are stored under their current name.
|
||||
pub fn new(settings: &ColorSettings) -> Result<Self, CmsError> {
|
||||
Ok(Self { settings: settings.clone(), rgb: rgb_space(&settings.rgb)?, cmyk: cmyk_space(&settings.cmyk)? })
|
||||
let settings = ColorSettings { rgb: canonical_name(&settings.rgb).into(), cmyk: canonical_name(&settings.cmyk).into(), ..settings.clone() };
|
||||
Ok(Self { rgb: rgb_space(&settings.rgb)?, cmyk: cmyk_space(&settings.cmyk)?, settings })
|
||||
}
|
||||
pub fn settings(&self) -> &ColorSettings {
|
||||
&self.settings
|
||||
|
||||
@@ -138,11 +138,11 @@ fn convert_between_models() {
|
||||
|
||||
#[test]
|
||||
fn wide_gamut_working_space() {
|
||||
let c = Cms::new(&ColorSettings { rgb: ADOBE_RGB.into(), ..Default::default() }).unwrap();
|
||||
let c = Cms::new(&ColorSettings { rgb: WIDE_GAMUT_RGB.into(), ..Default::default() }).unwrap();
|
||||
assert!(!c.rgb_is_srgb());
|
||||
let grey = c.rgb_to_srgb([0.5, 0.5, 0.5]);
|
||||
assert!(de(grey, [0.5, 0.5, 0.5]) < 1.5, "{grey:?}");
|
||||
// Adobe RGB green is outside sRGB: it clips to (nearly) full sRGB green.
|
||||
// Wide-gamut green is outside sRGB: it clips to (nearly) full sRGB green.
|
||||
let g = c.rgb_to_srgb([0.0, 1.0, 0.0]);
|
||||
assert!(g[1] > 0.95 && g[0] < 0.1, "{g:?}");
|
||||
let back = c.srgb_to_rgb(c.rgb_to_srgb([0.4, 0.5, 0.3]));
|
||||
@@ -150,6 +150,21 @@ fn wide_gamut_working_space() {
|
||||
assert!(matches!(Cms::new(&ColorSettings { rgb: "Nope".into(), ..Default::default() }), Err(CmsError::UnknownProfile(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_profile_name_resolves() {
|
||||
const OLD: &str = "Adobe RGB (1998) compatible"; // brand-ok: legacy alias under test
|
||||
assert_eq!(canonical_name(OLD), WIDE_GAMUT_RGB);
|
||||
assert_eq!(canonical_name(SRGB), SRGB);
|
||||
assert_eq!(profile(OLD).map(|p| (p.name, p.kind, p.builtin)), Some((WIDE_GAMUT_RGB.to_string(), ProfileKind::Rgb, true)));
|
||||
assert!(profile("Nope").is_none());
|
||||
assert!(profiles().iter().all(|p| p.name != OLD), "only the current name is listed");
|
||||
// Settings saved with the old name load as the current profile, under its current name.
|
||||
let c = Cms::new(&ColorSettings { rgb: OLD.into(), ..Default::default() }).unwrap();
|
||||
assert_eq!(c.settings().rgb, WIDE_GAMUT_RGB);
|
||||
let same = Cms::new(&ColorSettings { rgb: WIDE_GAMUT_RGB.into(), ..Default::default() }).unwrap();
|
||||
assert_eq!(c.rgb_to_srgb([0.0, 1.0, 0.0]), same.rgb_to_srgb([0.0, 1.0, 0.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_lut_matches_direct_proof() {
|
||||
let c = cms();
|
||||
|
||||
Reference in New Issue
Block a user