diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 0ac0b81..4464fa3 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -436,7 +436,7 @@ pub async fn extract_gamut( state.spawn(app, id, binary, args, cwd).await } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct TargenConfig { pub colour_space: String, #[serde(default)] @@ -446,6 +446,19 @@ pub struct TargenConfig { pub total_patches: Option, pub basename: String, pub cwd: String, + + // Advanced Stage 1 options + pub grey_steps: Option, // -g + pub single_channel_steps: Option, // -s + pub neutral_steps: Option, // -n + pub preconditioning_profile: Option, // -c + pub neutral_concentration: Option, // -N + pub ofps_high_quality: Option, // -G + pub ofps_adaptation: Option, // -A + pub full_spread_algorithm: Option, // "ofps"|"t"|"r"|"R"|"q"|"Q"|"i"|"I" + pub total_ink_limit: Option, // -l + pub dark_emphasis: Option, // -V + pub device_power: Option, // -p } fn default_dpi() -> u32 { @@ -470,7 +483,8 @@ pub fn build_targen_args(config: &TargenConfig) -> Vec { "-d".to_string(), ]; - if config.colour_space.to_lowercase() == "cmyk" { + let is_cmyk = config.colour_space.to_lowercase() == "cmyk"; + if is_cmyk { args.push("4".to_string()); } else { args.push("2".to_string()); // Default to RGB @@ -496,6 +510,87 @@ pub fn build_targen_args(config: &TargenConfig) -> Vec { args.push("-B".to_string()); args.push(black.to_string()); } + + if let Some(grey) = config.grey_steps { + if grey > 0 { + args.push("-g".to_string()); + args.push(grey.to_string()); + } + } + + if let Some(single) = config.single_channel_steps { + if single > 0 { + args.push("-s".to_string()); + args.push(single.to_string()); + } + } + + if let Some(ref precond) = config.preconditioning_profile { + let trimmed = precond.trim(); + if !trimmed.is_empty() { + args.push("-c".to_string()); + args.push(trimmed.to_string()); + } + } + + if let Some(neutral) = config.neutral_steps { + if neutral > 0 { + args.push("-n".to_string()); + args.push(neutral.to_string()); + } + } + + if let Some(conc) = config.neutral_concentration { + if (conc - 0.50).abs() > 0.001 { + args.push("-N".to_string()); + args.push(format!("{:.2}", conc)); + } + } + + if let Some(true) = config.ofps_high_quality { + args.push("-G".to_string()); + } + + if let Some(adapt) = config.ofps_adaptation { + args.push("-A".to_string()); + args.push(format!("{:.2}", adapt)); + } + + if let Some(ref algo) = config.full_spread_algorithm { + match algo.as_str() { + "t" => args.push("-t".to_string()), + "r" => args.push("-r".to_string()), + "R" => args.push("-R".to_string()), + "q" => args.push("-q".to_string()), + "Q" => args.push("-Q".to_string()), + "i" => args.push("-i".to_string()), + "I" => args.push("-I".to_string()), + _ => {} // default is OFPS (no flag needed) + } + } + + if is_cmyk { + if let Some(limit) = config.total_ink_limit { + if limit > 0 && limit <= 400 { + args.push("-l".to_string()); + args.push(limit.to_string()); + } + } + } + + if let Some(dark) = config.dark_emphasis { + if (dark - 1.0).abs() > 0.001 { + args.push("-V".to_string()); + args.push(format!("{:.2}", dark)); + } + } + + if let Some(power) = config.device_power { + if (power - 1.0).abs() > 0.001 && power > 0.0 { + args.push("-p".to_string()); + args.push(format!("{:.2}", power)); + } + } args.push(config.basename.clone()); args @@ -954,6 +1049,17 @@ mod tests { total_patches: None, basename: "my_profile".to_string(), cwd: "/tmp".to_string(), + grey_steps: None, + single_channel_steps: None, + neutral_steps: None, + preconditioning_profile: None, + neutral_concentration: None, + ofps_high_quality: None, + ofps_adaptation: None, + full_spread_algorithm: None, + total_ink_limit: None, + dark_emphasis: None, + device_power: None, }; let args = build_targen_args(&config); assert_eq!(args, vec!["-v", "-d", "2", "-f", "800", "-e", "4", "my_profile"]); @@ -969,6 +1075,17 @@ mod tests { total_patches: None, basename: "cmyk_profile".to_string(), cwd: "/tmp".to_string(), + grey_steps: None, + single_channel_steps: None, + neutral_steps: None, + preconditioning_profile: None, + neutral_concentration: None, + ofps_high_quality: None, + ofps_adaptation: None, + full_spread_algorithm: None, + total_ink_limit: None, + dark_emphasis: None, + device_power: None, }; let args = build_targen_args(&config); assert_eq!(args, vec!["-v", "-d", "4", "-f", "1500", "-B", "8", "cmyk_profile"]); @@ -984,11 +1101,94 @@ mod tests { total_patches: Some(400), basename: "draft_profile".to_string(), cwd: "/tmp".to_string(), + grey_steps: None, + single_channel_steps: None, + neutral_steps: None, + preconditioning_profile: None, + neutral_concentration: None, + ofps_high_quality: None, + ofps_adaptation: None, + full_spread_algorithm: None, + total_ink_limit: None, + dark_emphasis: None, + device_power: None, }; let args = build_targen_args(&config); assert_eq!(args, vec!["-v", "-d", "2", "-f", "400", "draft_profile"]); } + #[test] + fn test_build_targen_args_all_advanced_flags() { + let config = TargenConfig { + colour_space: "cmyk".to_string(), + patch_count: 1200, + white_patches: Some(4), + black_patches: Some(4), + total_patches: None, + basename: "adv_target".to_string(), + cwd: "/home/user".to_string(), + grey_steps: Some(16), + single_channel_steps: Some(8), + neutral_steps: Some(10), + preconditioning_profile: Some("/profiles/precond.icc".to_string()), + neutral_concentration: Some(0.75), + ofps_high_quality: Some(true), + ofps_adaptation: Some(0.80), + full_spread_algorithm: Some("t".to_string()), + total_ink_limit: Some(320), + dark_emphasis: Some(1.5), + device_power: Some(1.2), + }; + let args = build_targen_args(&config); + assert_eq!( + args, + vec![ + "-v", "-d", "4", + "-f", "1200", + "-e", "4", + "-B", "4", + "-g", "16", + "-s", "8", + "-c", "/profiles/precond.icc", + "-n", "10", + "-N", "0.75", + "-G", + "-A", "0.80", + "-t", + "-l", "320", + "-V", "1.50", + "-p", "1.20", + "adv_target" + ] + ); + } + + #[test] + fn test_build_targen_args_rgb_ignores_ink_limit() { + let config = TargenConfig { + colour_space: "rgb".to_string(), + patch_count: 800, + white_patches: None, + black_patches: None, + total_patches: None, + basename: "rgb_target".to_string(), + cwd: "/tmp".to_string(), + grey_steps: None, + single_channel_steps: None, + neutral_steps: None, + preconditioning_profile: None, + neutral_concentration: None, + ofps_high_quality: None, + ofps_adaptation: None, + full_spread_algorithm: None, + total_ink_limit: Some(300), // Should be ignored for RGB + dark_emphasis: None, + device_power: None, + }; + let args = build_targen_args(&config); + assert!(!args.contains(&"-l".to_string())); + } + #[test] fn test_build_printtarg_args_i1_a4_8bit() { let config = PrinttargConfig { diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 3bbf72b..c554997 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -18,6 +18,30 @@ pub struct ProfilingPreset { pub colprof_algorithm: String, pub colprof_quality: String, pub colprof_intent: Option, + + // Advanced Stage 1 fields + #[serde(default)] + pub grey_steps: Option, + #[serde(default)] + pub single_channel_steps: Option, + #[serde(default)] + pub neutral_steps: Option, + #[serde(default)] + pub preconditioning_profile: Option, + #[serde(default)] + pub neutral_concentration: Option, + #[serde(default)] + pub ofps_high_quality: Option, + #[serde(default)] + pub ofps_adaptation: Option, + #[serde(default)] + pub full_spread_algorithm: Option, + #[serde(default)] + pub total_ink_limit: Option, + #[serde(default)] + pub dark_emphasis: Option, + #[serde(default)] + pub device_power: Option, } #[derive(Debug, Deserialize, Serialize, Default, Clone)] @@ -46,6 +70,17 @@ pub fn get_default_presets() -> Vec { colprof_algorithm: "l".to_string(), colprof_quality: "m".to_string(), colprof_intent: None, + grey_steps: None, + single_channel_steps: None, + neutral_steps: None, + preconditioning_profile: None, + neutral_concentration: None, + ofps_high_quality: None, + ofps_adaptation: None, + full_spread_algorithm: None, + total_ink_limit: None, + dark_emphasis: None, + device_power: None, }, ProfilingPreset { id: "preset-hq-cmyk".to_string(), @@ -62,6 +97,17 @@ pub fn get_default_presets() -> Vec { colprof_algorithm: "l".to_string(), colprof_quality: "h".to_string(), colprof_intent: None, + grey_steps: None, + single_channel_steps: None, + neutral_steps: None, + preconditioning_profile: None, + neutral_concentration: None, + ofps_high_quality: None, + ofps_adaptation: None, + full_spread_algorithm: None, + total_ink_limit: Some(320), + dark_emphasis: None, + device_power: None, }, ProfilingPreset { id: "preset-draft-rgb".to_string(), @@ -78,6 +124,17 @@ pub fn get_default_presets() -> Vec { colprof_algorithm: "l".to_string(), colprof_quality: "l".to_string(), colprof_intent: None, + grey_steps: None, + single_channel_steps: None, + neutral_steps: None, + preconditioning_profile: None, + neutral_concentration: None, + ofps_high_quality: None, + ofps_adaptation: None, + full_spread_algorithm: None, + total_ink_limit: None, + dark_emphasis: None, + device_power: None, }, ProfilingPreset { id: "preset-ultra-rgb".to_string(), @@ -94,6 +151,17 @@ pub fn get_default_presets() -> Vec { colprof_algorithm: "l".to_string(), colprof_quality: "u".to_string(), colprof_intent: None, + grey_steps: None, + single_channel_steps: None, + neutral_steps: None, + preconditioning_profile: None, + neutral_concentration: None, + ofps_high_quality: Some(true), + ofps_adaptation: None, + full_spread_algorithm: None, + total_ink_limit: None, + dark_emphasis: None, + device_power: None, }, ] } @@ -219,6 +287,17 @@ mod tests { colprof_algorithm: "l".to_string(), colprof_quality: "h".to_string(), colprof_intent: None, + grey_steps: Some(16), + single_channel_steps: Some(8), + neutral_steps: Some(10), + preconditioning_profile: Some("/profiles/precond.icc".to_string()), + neutral_concentration: Some(0.75), + ofps_high_quality: Some(true), + ofps_adaptation: Some(0.80), + full_spread_algorithm: Some("t".to_string()), + total_ink_limit: Some(320), + dark_emphasis: Some(1.5), + device_power: Some(1.2), }; let json = export_preset_json(preset.clone()).expect("Export failed"); diff --git a/src/index.html b/src/index.html index aca845f..c7adcdd 100644 --- a/src/index.html +++ b/src/index.html @@ -58,19 +58,31 @@
-

Generate Target

-

Configure the colour patches for your profiling target.

+
+
+

Generate Target

+

Configure the colour patches for your profiling target.

+
+ +
-
-
+
+ +
+
+ Choose the device colour model for the test chart.
+ • RGB (Printer Driver) – for printers driven by an RGB driver or host colour management.
+ • CMYK (RIP) – for printers controlled by a RIP or when you need explicit CMYK separation.
+ This sets the targen -d colourant combination (2 = Print RGB, 4 = CMYK). +
-
+
+
+ Total number of colour patches that will appear in the final chart.
+ Higher counts improve profile accuracy (especially in smooth gradients and near the neutrals) but increase print time, paper usage and measurement time.
+ Typical guidance: 400 (draft), 800 (standard), 1500 (photo/proof), 2500+ (maximum quality). Passed to targen -f. +
-
- +
+ +
+ Number of pure-white patches included in the chart.
+ White defines the paper white point that the ICC profile is made relative to. Measuring several white patches improves robustness against noise and paper variation. Default is 4. +
-
- - +
+ + +
+ Number of pure-black patches included in the chart.
+ Black is especially important for additive (RGB) devices; measuring multiple black patches improves black-point accuracy. Default is 4 for RGB and 0 for CMYK. +
-
+
@@ -106,10 +131,156 @@
No directory selected (using default). +
+ Base name used for all generated files (.ti1, later .ti2, .ti3, .icc/.icm) and folder where artefacts will be created. +
+ + +
+ + ⚙️ Advanced Target Generation Options + Click to expand + + +
+ +
+
+ + +
+ Number of evenly-spaced steps along the combined grey axis (equal R=G=B or C=M=Y).
+ These patches improve neutral-axis rendering and are useful when device will later be used as source space. 0 = none. +
+
+
+ + +
+ Number of steps for each individual colorant wedge (R, G, B or C, M, Y, K).
+ Useful for diagnosing per-channel non-linearity and source-space characterisation. 0 = none. +
+
+
+ + +
+ +
+ + +
+
+ Optional previous ICC/ICM (or MPP) profile of this or a similar device.
+ Used by targen to estimate perceptual distances, colourspace curvature and the true neutral axis. Enables adaptive OFPS and Neutral Axis Steps. +
+
+ +
+
+ + +
+ Number of patches placed along the true neutral axis as estimated by preconditioning profile.
+ Requires a Preconditioning Profile. Improves neutral rendering of final profile. +
+
+
+ +
+ + 0.50 +
+
+ How strongly the patch distribution should favour the neutral axis (0 = none, 1 = maximum).
+ Default 0.5. Most effective with preconditioning profile and high OFPS Adaptation. +
+
+
+ + +
+
+ + +
+ Algorithm used to place remaining “full-spread” patches.
+ • OFPS (default) – best evenness in most cases.
+ • Incremental Far Point – used automatically for >4 channels.
+ • Device/Perceptual random or BCC – alternative space-filling methods. +
+
+
+ +
+ When checked, targen runs the OFPS algorithm in “good” mode (more iterations).
+ Produces a more evenly distributed set of patches at cost of longer generation time. Recommended for final production charts. +
+
+
+ + +
+
+ +
+ + 0.10 +
+
+ Degree to which OFPS adapts to device behaviour from preconditioning profile (0 = ignore, 1 = fully adapt).
+ Default 0.1; automatically rises to 1.0 when preconditioning profile is supplied. +
+
+
+ + +
+ Maximum total ink coverage (Total Area Coverage) in percent.
+ Essential for CMYK to avoid over-inking. Set at least 10% higher than final profile TAC. Disabled in RGB mode. +
+
+
+ +
+
+ +
+ + 1.00 +
+
+ Concentrates more patches toward dark tones (1.0 = none, up to 4.0 = strong).
+ Useful for devices with non-linearity or important shadow detail. Default 1.0. +
+
+
+ + +
+ Optional power-like remapping applied to device values after generation.
+ Values > 1 tighten spacing near 0; values < 1 tighten spacing near 1. Leave empty for linear spacing. +
+
+
+
+
- +