mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-28 10:11:58 +01:00
New Document: Color Profile, the working space or another profile of the mode (#321)
Photoshop's New Document has Advanced Options › Color Profile; PhotoCraft's always opened in the working space, so a document in another space had to be made and then assigned a profile. `file.new` takes `profile` (working, a built-in id or an .icc path), checked against the colour mode before the document opens, and the New Document dialog lists the working space (by its profile) and the built-in and installed profiles of the mode's colour space. Changing the mode drops a profile of the old space. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
af54f4aed1
commit
5f9c7d307e
@@ -467,7 +467,7 @@ fn build() -> Vec<CommandSpec> {
|
||||
"New…",
|
||||
["File"],
|
||||
Some("Cmd+N"),
|
||||
r##"{"width":1..300000=1920,"height":1..300000=1080 (pixel counts are rounded),"mode":"rgb|gray|grayscale|cmyk|lab"="rgb","depth":8|16|32=8,"background":"white|black|backgroundColor|transparent|#rrggbb"="white","backgroundColor":[r,g,b]? (0..1; defaults to toolbox colour),"resolution":1..30000=72 (ppi),"name":str}"##,
|
||||
r##"{"width":1..300000=1920,"height":1..300000=1080 (pixel counts are rounded),"mode":"rgb|gray|grayscale|cmyk|lab"="rgb","depth":8|16|32=8,"background":"white|black|backgroundColor|transparent|#rrggbb"="white","backgroundColor":[r,g,b]? (0..1; defaults to toolbox colour),"resolution":1..30000=72 (ppi),"name":str,"profile":"working"|built-in id|path to an .icc file="working" (the colour profile it is tagged with; it must suit the mode)}"##,
|
||||
always,
|
||||
|s, p| {
|
||||
use crate::document_preset_cmds::{MAX_DIMENSION, MAX_RESOLUTION, background_color, color_mode, sample_type};
|
||||
@@ -503,6 +503,18 @@ fn build() -> Vec<CommandSpec> {
|
||||
}
|
||||
};
|
||||
doc.resolution_dpi = res;
|
||||
// Photoshop's New Document › Color Profile: tag the document with another profile
|
||||
// than the working space (#321). Checked before the document opens.
|
||||
if let Some(spec) = p.get("profile").and_then(Value::as_str).filter(|spec| !matches!(*spec, "working" | "none")) {
|
||||
let prof = s.color.resolve(spec, Some(&doc), None)?;
|
||||
if prof.color_space != crate::color_cmds::mode_space(doc.mode) {
|
||||
return Err(EngineError::BadParams {
|
||||
cmd: "file.new".into(),
|
||||
msg: format!("profile `{}` is {:?}; the document is {:?}", prof.description, prof.color_space, doc.mode),
|
||||
});
|
||||
}
|
||||
doc.icc_profile = Some(prof.to_bytes());
|
||||
}
|
||||
let i = s.add_document(doc, None);
|
||||
Ok(json!({ "document": i }))
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ pub(crate) fn validate_new_params(s: &Session, p: &Value) -> Result<()> {
|
||||
continue;
|
||||
}
|
||||
"name" => (value.is_string(), "a string".into()),
|
||||
"profile" => (value.as_str().is_some_and(|v| !v.is_empty()), "working, a built-in profile id or an .icc path".into()),
|
||||
_ => return Err(bad(cmd, format!("unknown parameter `{key}`"))),
|
||||
};
|
||||
if !valid {
|
||||
|
||||
@@ -158,6 +158,31 @@ fn file_new_keeps_defaults_and_supported_modes_and_depths() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_new_tags_the_document_with_the_chosen_profile() {
|
||||
// #321: New Document › Color Profile, as in Photoshop: another profile than the working space.
|
||||
let desc = |s: &Session| {
|
||||
let bytes = s.active().unwrap().doc.icc_profile.clone();
|
||||
bytes.map(|b| photocraft_cms::Profile::parse(&b).unwrap().description)
|
||||
};
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", json!({"width": 4, "height": 4, "profile": "adobe-rgb-compat"})).unwrap();
|
||||
assert_eq!(desc(&s).as_deref(), Some(photocraft_cms::Builtin::AdobeRgbCompat.description()));
|
||||
s.execute("file.new", json!({"width": 4, "height": 4, "mode": "cmyk", "profile": "coated-cmyk"})).unwrap();
|
||||
assert!(desc(&s).is_some());
|
||||
// The working space (the default, or asked for) leaves it as before.
|
||||
for p in [json!({"width": 4, "height": 4}), json!({"width": 4, "height": 4, "profile": "working"})] {
|
||||
s.execute("file.new", p).unwrap();
|
||||
assert_eq!(desc(&s), None);
|
||||
}
|
||||
// A profile that doesn't suit the mode, an unknown one or a wrong type opens nothing.
|
||||
let open = s.documents().len();
|
||||
for bad in [json!("coated-cmyk"), json!("no-such-profile"), json!(""), json!(7), json!(null)] {
|
||||
assert!(s.execute("file.new", json!({"width": 4, "height": 4, "profile": bad})).is_err(), "{bad}");
|
||||
}
|
||||
assert_eq!(s.documents().len(), open);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_new_variants() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -231,6 +231,33 @@ pub fn apply_preset(f: &mut Map<String, Value>, p: &Preset, units: &UnitsAndRule
|
||||
f.insert("__unit".into(), json!(unit));
|
||||
}
|
||||
|
||||
/// The colour mode of the dialog's `mode` value.
|
||||
fn color_mode_of(mode: &str) -> Option<photocraft_color::ColorMode> {
|
||||
use photocraft_color::ColorMode;
|
||||
match mode {
|
||||
"rgb" => Some(ColorMode::Rgb),
|
||||
"gray" | "grayscale" => Some(ColorMode::Grayscale),
|
||||
"cmyk" => Some(ColorMode::Cmyk),
|
||||
"lab" => Some(ColorMode::Lab),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The Color Profile menu for a document of colour `space` (#321): the working space (shown with
|
||||
/// its profile's description), then the built-in and installed profiles of that space, as
|
||||
/// `file.new`'s `profile` values (an id or a path) with their labels.
|
||||
fn profile_options(
|
||||
space: Option<photocraft_cms::ColorSpace>,
|
||||
working: &str,
|
||||
installed: &[photocraft_engine::installed_profiles::InstalledProfile],
|
||||
) -> Vec<(String, String)> {
|
||||
let mut v = vec![("working".to_string(), crate::i18n::fmt(tl!("Working: {working}"), &[("working", working)]))];
|
||||
let Some(space) = space else { return v };
|
||||
v.extend(photocraft_cms::Builtin::ALL.iter().filter(|b| b.profile().color_space == space).map(|b| (b.id().to_string(), b.description().to_string())));
|
||||
v.extend(installed.iter().filter(|i| i.is_destination() && i.color_space == space).map(|i| (i.path.clone(), i.description.clone())));
|
||||
v
|
||||
}
|
||||
|
||||
/// Fields `file.new` takes (drops the dialog's `__` UI keys).
|
||||
pub fn command_params(f: &Map<String, Value>) -> Value {
|
||||
Value::Object(f.iter().filter(|(k, _)| !k.starts_with("__")).map(|(k, v)| (k.clone(), v.clone())).collect())
|
||||
@@ -695,6 +722,8 @@ pub fn body(app: &mut crate::PhotocraftApp, ui: &mut egui::Ui, f: &mut Map<Strin
|
||||
110.0,
|
||||
) {
|
||||
f.insert("mode".into(), json!(mode));
|
||||
// A profile of the old mode's colour space doesn't suit the new one.
|
||||
f.remove("profile");
|
||||
f.remove("__savedPreset");
|
||||
}
|
||||
let mut depth = f.get("depth").and_then(Value::as_u64).unwrap_or(8);
|
||||
@@ -705,6 +734,30 @@ pub fn body(app: &mut crate::PhotocraftApp, ui: &mut egui::Ui, f: &mut Map<Strin
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
// Photoshop's Advanced Options › Color Profile (#321): the working space by default,
|
||||
// or another profile of the mode's colour space, built in or installed in the system.
|
||||
small_label(ui, tl!("Color Profile"));
|
||||
{
|
||||
let mode = get_s(f, "mode", "rgb");
|
||||
let space = color_mode_of(&mode).map(photocraft_engine::color_cmds::mode_space);
|
||||
let working = color_mode_of(&mode).map(|m| app.session.color.working(m).description.clone()).unwrap_or_default();
|
||||
let installed = photocraft_engine::installed_profiles::installed();
|
||||
let opts = profile_options(space, &working, &installed);
|
||||
let mut profile = get_s(f, "profile", "working");
|
||||
if !opts.iter().any(|(v, _)| *v == profile) {
|
||||
profile = "working".into();
|
||||
}
|
||||
let shown: Vec<(String, &str)> = opts.iter().map(|(v, l)| (v.clone(), l.as_str())).collect();
|
||||
if widgets::dropdown(ui, "nd-profile", &mut profile, &shown, 240.0) {
|
||||
if profile == "working" {
|
||||
f.remove("profile");
|
||||
} else {
|
||||
f.insert("profile".into(), json!(profile));
|
||||
}
|
||||
f.remove("__savedPreset");
|
||||
}
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
small_label(ui, tl!("Background Contents"));
|
||||
// A custom colour is kept as its "#rrggbb", which `file.new` takes as is (#4789).
|
||||
let current = get_s(f, "background", "white");
|
||||
@@ -768,6 +821,35 @@ pub fn body(app: &mut crate::PhotocraftApp, ui: &mut egui::Ui, f: &mut Map<Strin
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// #321: New Document offers the working space and the other profiles of the mode's space.
|
||||
#[test]
|
||||
fn color_profile_menu_lists_the_profiles_of_the_modes_space() {
|
||||
use photocraft_cms::{Builtin, ColorSpace, ProfileClass};
|
||||
use photocraft_engine::installed_profiles::InstalledProfile;
|
||||
let installed = [
|
||||
InstalledProfile { path: "press.icc".into(), description: "Press".into(), class: ProfileClass::Output, color_space: ColorSpace::Cmyk },
|
||||
InstalledProfile { path: "panel.icc".into(), description: "Panel".into(), class: ProfileClass::Display, color_space: ColorSpace::Rgb },
|
||||
];
|
||||
let values = |mode: &str| profile_options(color_mode_of(mode).map(photocraft_engine::color_cmds::mode_space), "sRGB", &installed)
|
||||
.into_iter()
|
||||
.map(|(v, _)| v)
|
||||
.collect::<Vec<_>>();
|
||||
let rgb = values("rgb");
|
||||
assert_eq!(rgb[0], "working");
|
||||
assert!(rgb.contains(&Builtin::AdobeRgbCompat.id().to_string()) && rgb.contains(&"panel.icc".to_string()));
|
||||
assert!(!rgb.contains(&Builtin::CoatedCmyk.id().to_string()) && !rgb.contains(&"press.icc".to_string()));
|
||||
let cmyk = values("cmyk");
|
||||
assert!(cmyk.contains(&"press.icc".to_string()) && !cmyk.contains(&Builtin::Srgb.id().to_string()));
|
||||
assert!(profile_options(None, "sRGB", &installed).len() == 1, "an unknown mode offers only the working space");
|
||||
// Every entry is one `file.new` takes for that mode.
|
||||
for mode in ["rgb", "gray", "cmyk", "lab"] {
|
||||
for v in values(mode).into_iter().filter(|v| !v.ends_with(".icc")) {
|
||||
let mut s = photocraft_engine::Session::new();
|
||||
s.execute("file.new", json!({"width": 2, "height": 2, "mode": mode, "profile": v})).unwrap_or_else(|e| panic!("{mode} {v}: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn units_round_trip_through_pixels() {
|
||||
assert_eq!(to_unit(2100.0, "in", 300.0), 7.0);
|
||||
|
||||
Reference in New Issue
Block a user