feat(stage1): add advanced targen options, collapsible section, tooltips and preset integration (resolves #141) #144
+202
-2
@@ -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<u32>,
|
||||
pub basename: String,
|
||||
pub cwd: String,
|
||||
|
||||
// Advanced Stage 1 options
|
||||
pub grey_steps: Option<u32>, // -g
|
||||
pub single_channel_steps: Option<u32>, // -s
|
||||
pub neutral_steps: Option<u32>, // -n
|
||||
pub preconditioning_profile: Option<String>, // -c
|
||||
pub neutral_concentration: Option<f64>, // -N
|
||||
pub ofps_high_quality: Option<bool>, // -G
|
||||
pub ofps_adaptation: Option<f64>, // -A
|
||||
pub full_spread_algorithm: Option<String>, // "ofps"|"t"|"r"|"R"|"q"|"Q"|"i"|"I"
|
||||
pub total_ink_limit: Option<u32>, // -l
|
||||
pub dark_emphasis: Option<f64>, // -V
|
||||
pub device_power: Option<f64>, // -p
|
||||
}
|
||||
|
||||
fn default_dpi() -> u32 {
|
||||
@@ -470,7 +483,8 @@ pub fn build_targen_args(config: &TargenConfig) -> Vec<String> {
|
||||
"-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<String> {
|
||||
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 {
|
||||
|
||||
@@ -18,6 +18,30 @@ pub struct ProfilingPreset {
|
||||
pub colprof_algorithm: String,
|
||||
pub colprof_quality: String,
|
||||
pub colprof_intent: Option<String>,
|
||||
|
||||
// Advanced Stage 1 fields
|
||||
#[serde(default)]
|
||||
pub grey_steps: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub single_channel_steps: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub neutral_steps: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub preconditioning_profile: Option<String>,
|
||||
#[serde(default)]
|
||||
pub neutral_concentration: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub ofps_high_quality: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub ofps_adaptation: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub full_spread_algorithm: Option<String>,
|
||||
#[serde(default)]
|
||||
pub total_ink_limit: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub dark_emphasis: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub device_power: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
||||
@@ -46,6 +70,17 @@ pub fn get_default_presets() -> Vec<ProfilingPreset> {
|
||||
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<ProfilingPreset> {
|
||||
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<ProfilingPreset> {
|
||||
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<ProfilingPreset> {
|
||||
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");
|
||||
|
||||
+183
-12
@@ -58,19 +58,31 @@
|
||||
<main class="content">
|
||||
<!-- Stage 1: targen -->
|
||||
<section id="stage-1" class="stage active">
|
||||
<h2>Generate Target</h2>
|
||||
<p>Configure the colour patches for your profiling target.</p>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<h2>Generate Target</h2>
|
||||
<p>Configure the colour patches for your profiling target.</p>
|
||||
</div>
|
||||
<button type="button" id="btnToggleAllHelp" class="secondary" style="font-size: 0.75rem; padding: 4px 8px;" title="Toggle tooltip hints for all options">💡 Show Tooltip Hints</button>
|
||||
</div>
|
||||
|
||||
<div class="form-container">
|
||||
<div class="form-group">
|
||||
<div class="form-container" id="stage1FormContainer">
|
||||
<!-- Basic Settings -->
|
||||
<div class="form-group has-tooltip">
|
||||
<label>Colour Space</label>
|
||||
<div class="radio-group">
|
||||
<label><input type="radio" name="colourSpace" value="rgb" checked> RGB (Printer Driver)</label>
|
||||
<label><input type="radio" name="colourSpace" value="cmyk"> CMYK (RIP)</label>
|
||||
</div>
|
||||
<div class="tooltip-text">
|
||||
Choose the device colour model for the test chart.<br>
|
||||
• RGB (Printer Driver) – for printers driven by an RGB driver or host colour management.<br>
|
||||
• CMYK (RIP) – for printers controlled by a RIP or when you need explicit CMYK separation.<br>
|
||||
This sets the targen -d colourant combination (2 = Print RGB, 4 = CMYK).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group has-tooltip">
|
||||
<label>Patch Count</label>
|
||||
<select id="patchCountPreset">
|
||||
<option value="400">Draft (400 patches)</option>
|
||||
@@ -80,23 +92,36 @@
|
||||
<option value="custom">Custom...</option>
|
||||
</select>
|
||||
<input type="number" id="patchCountCustom" value="800" min="50" max="10000" class="hidden" style="margin-top: 8px;">
|
||||
<div class="tooltip-text">
|
||||
Total number of colour patches that will appear in the final chart.<br>
|
||||
Higher counts improve profile accuracy (especially in smooth gradients and near the neutrals) but increase print time, paper usage and measurement time.<br>
|
||||
Typical guidance: 400 (draft), 800 (standard), 1500 (photo/proof), 2500+ (maximum quality). Passed to targen -f.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Neutral Axis Boost (Optional)</label>
|
||||
<div class="input-row">
|
||||
<div>
|
||||
<label for="whitePatches" class="sub-label">White Patches</label>
|
||||
<div class="has-tooltip" style="flex: 1;">
|
||||
<label for="whitePatches" class="sub-label">White Patches (-e)</label>
|
||||
<input type="number" id="whitePatches" min="0" max="100" placeholder="e.g. 4">
|
||||
<div class="tooltip-text">
|
||||
Number of pure-white patches included in the chart.<br>
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="blackPatches" class="sub-label">Black Patches</label>
|
||||
<input type="number" id="blackPatches" min="0" max="100" placeholder="e.g. 8">
|
||||
<div class="has-tooltip" style="flex: 1;">
|
||||
<label for="blackPatches" class="sub-label">Black Patches (-B)</label>
|
||||
<input type="number" id="blackPatches" min="0" max="100" placeholder="e.g. 4">
|
||||
<div class="tooltip-text">
|
||||
Number of pure-black patches included in the chart.<br>
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group has-tooltip">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
|
||||
<label style="margin: 0;">Project Directory & Filename</label>
|
||||
<button type="button" id="btnOpenExisting" class="secondary" style="font-size: 0.75rem; padding: 2px 8px;" title="Open existing .ti1 or .ti2 target file">📂 Open Target...</button>
|
||||
@@ -106,10 +131,156 @@
|
||||
<button type="button" id="btnBrowse" class="secondary">Browse...</button>
|
||||
</div>
|
||||
<small id="selectedPathDisplay" class="path-display">No directory selected (using default).</small>
|
||||
<div class="tooltip-text">
|
||||
Base name used for all generated files (.ti1, later .ti2, .ti3, .icc/.icm) and folder where artefacts will be created.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collapsible Advanced Section -->
|
||||
<details id="targenAdvancedDetails" class="advanced-section-details" style="margin-top: 10px; border: 1px solid var(--border-color, #333); border-radius: 8px; padding: 10px 14px; background: rgba(0,0,0,0.15);">
|
||||
<summary style="font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: space-between; user-select: none;">
|
||||
<span>⚙️ Advanced Target Generation Options</span>
|
||||
<span class="summary-badge" style="font-size: 0.75rem; opacity: 0.6;">Click to expand</span>
|
||||
</summary>
|
||||
|
||||
<div class="advanced-content" style="margin-top: 14px; display: flex; flex-direction: column; gap: 12px;">
|
||||
<!-- Grey & Single Channel Steps -->
|
||||
<div class="input-row">
|
||||
<div class="form-group has-tooltip" style="flex: 1;">
|
||||
<label for="targenGreySteps" class="sub-label">Grey / Combined Axis Steps (-g)</label>
|
||||
<input type="number" id="targenGreySteps" min="0" max="256" placeholder="0">
|
||||
<div class="tooltip-text">
|
||||
Number of evenly-spaced steps along the combined grey axis (equal R=G=B or C=M=Y).<br>
|
||||
These patches improve neutral-axis rendering and are useful when device will later be used as source space. 0 = none.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group has-tooltip" style="flex: 1;">
|
||||
<label for="targenSingleChannelSteps" class="sub-label">Single-Channel Steps (-s)</label>
|
||||
<input type="number" id="targenSingleChannelSteps" min="0" max="256" placeholder="0">
|
||||
<div class="tooltip-text">
|
||||
Number of steps for each individual colorant wedge (R, G, B or C, M, Y, K).<br>
|
||||
Useful for diagnosing per-channel non-linearity and source-space characterisation. 0 = none.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preconditioning Profile & Neutral Axis Steps -->
|
||||
<div class="form-group has-tooltip">
|
||||
<label for="targenPrecondProfile" class="sub-label">Preconditioning Profile (-c)</label>
|
||||
<div class="input-row has-btn">
|
||||
<input type="text" id="targenPrecondProfile" placeholder="Optional previous profile (.icc / .icm)">
|
||||
<button type="button" id="btnBrowsePrecondProfile" class="secondary">Browse...</button>
|
||||
</div>
|
||||
<div class="tooltip-text">
|
||||
Optional previous ICC/ICM (or MPP) profile of this or a similar device.<br>
|
||||
Used by targen to estimate perceptual distances, colourspace curvature and the true neutral axis. Enables adaptive OFPS and Neutral Axis Steps.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-row">
|
||||
<div class="form-group has-tooltip" style="flex: 1;">
|
||||
<label for="targenNeutralSteps" class="sub-label">Neutral Axis Steps (-n)</label>
|
||||
<input type="number" id="targenNeutralSteps" min="0" max="256" placeholder="0">
|
||||
<div class="tooltip-text">
|
||||
Number of patches placed along the true neutral axis as estimated by preconditioning profile.<br>
|
||||
Requires a Preconditioning Profile. Improves neutral rendering of final profile.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group has-tooltip" style="flex: 1;">
|
||||
<label for="targenNeutralConcentration" class="sub-label">Neutral Axis Concentration (-N)</label>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<input type="range" id="targenNeutralConcentration" min="0.0" max="1.0" step="0.05" value="0.5" style="flex: 1;">
|
||||
<span id="targenNeutralConcVal" style="font-family: monospace; font-size: 0.8rem; min-width: 35px;">0.50</span>
|
||||
</div>
|
||||
<div class="tooltip-text">
|
||||
How strongly the patch distribution should favour the neutral axis (0 = none, 1 = maximum).<br>
|
||||
Default 0.5. Most effective with preconditioning profile and high OFPS Adaptation.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quality & Algorithm -->
|
||||
<div class="input-row">
|
||||
<div class="form-group has-tooltip" style="flex: 1;">
|
||||
<label for="targenAlgorithm" class="sub-label">Full-spread Algorithm</label>
|
||||
<select id="targenAlgorithm">
|
||||
<option value="ofps" selected>OFPS (Optimised Farthest Point Sampling - Default)</option>
|
||||
<option value="t">Incremental Far Point (-t)</option>
|
||||
<option value="r">Device Random (-r)</option>
|
||||
<option value="R">Perceptual Random (-R)</option>
|
||||
<option value="q">Device Quasi-Random (-q)</option>
|
||||
<option value="Q">Perceptual Quasi-Random (-Q)</option>
|
||||
<option value="i">Device BCC (-i)</option>
|
||||
<option value="I">Perceptual BCC (-I)</option>
|
||||
</select>
|
||||
<div class="tooltip-text">
|
||||
Algorithm used to place remaining “full-spread” patches.<br>
|
||||
• OFPS (default) – best evenness in most cases.<br>
|
||||
• Incremental Far Point – used automatically for >4 channels.<br>
|
||||
• Device/Perceptual random or BCC – alternative space-filling methods.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group has-tooltip" style="flex: 1; display: flex; flex-direction: column; justify-content: flex-end;">
|
||||
<label style="cursor: pointer; display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
|
||||
<input type="checkbox" id="targenHighQuality">
|
||||
<span style="font-size: 0.85rem;">High Quality OFPS (-G)</span>
|
||||
</label>
|
||||
<div class="tooltip-text">
|
||||
When checked, targen runs the OFPS algorithm in “good” mode (more iterations).<br>
|
||||
Produces a more evenly distributed set of patches at cost of longer generation time. Recommended for final production charts.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Adaptation & Ink Limit & Tone Remapping -->
|
||||
<div class="input-row">
|
||||
<div class="form-group has-tooltip" style="flex: 1;">
|
||||
<label for="targenAdaptation" class="sub-label">OFPS Adaptation (-A)</label>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<input type="range" id="targenAdaptation" min="0.0" max="1.0" step="0.05" value="0.1" style="flex: 1;">
|
||||
<span id="targenAdaptationVal" style="font-family: monospace; font-size: 0.8rem; min-width: 35px;">0.10</span>
|
||||
</div>
|
||||
<div class="tooltip-text">
|
||||
Degree to which OFPS adapts to device behaviour from preconditioning profile (0 = ignore, 1 = fully adapt).<br>
|
||||
Default 0.1; automatically rises to 1.0 when preconditioning profile is supplied.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group has-tooltip" id="targenInkLimitGroup" style="flex: 1;">
|
||||
<label for="targenInkLimit" class="sub-label">Total Ink Limit / TAC (-l %)</label>
|
||||
<input type="number" id="targenInkLimit" min="0" max="400" placeholder="e.g. 320" disabled>
|
||||
<div class="tooltip-text">
|
||||
Maximum total ink coverage (Total Area Coverage) in percent.<br>
|
||||
Essential for CMYK to avoid over-inking. Set at least 10% higher than final profile TAC. Disabled in RGB mode.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-row">
|
||||
<div class="form-group has-tooltip" style="flex: 1;">
|
||||
<label for="targenDarkEmphasis" class="sub-label">Dark Region Emphasis (-V)</label>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<input type="range" id="targenDarkEmphasis" min="1.0" max="4.0" step="0.1" value="1.0" style="flex: 1;">
|
||||
<span id="targenDarkEmphasisVal" style="font-family: monospace; font-size: 0.8rem; min-width: 35px;">1.00</span>
|
||||
</div>
|
||||
<div class="tooltip-text">
|
||||
Concentrates more patches toward dark tones (1.0 = none, up to 4.0 = strong).<br>
|
||||
Useful for devices with non-linearity or important shadow detail. Default 1.0.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group has-tooltip" style="flex: 1;">
|
||||
<label for="targenDevicePower" class="sub-label">Device Value Power (-p)</label>
|
||||
<input type="number" id="targenDevicePower" min="0.1" max="4.0" step="0.1" placeholder="Linear (1.0)">
|
||||
<div class="tooltip-text">
|
||||
Optional power-like remapping applied to device values after generation.<br>
|
||||
Values > 1 tighten spacing near 0; values < 1 tighten spacing near 1. Leave empty for linear spacing.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<button class="primary" id="btnGenerate" disabled>Generate (.ti1)</button>
|
||||
<button class="primary" id="btnGenerate" disabled style="margin-top: 14px;">Generate (.ti1)</button>
|
||||
|
||||
<details class="log-container hidden" id="targenLogContainer">
|
||||
<summary>Process Output</summary>
|
||||
|
||||
@@ -152,6 +152,58 @@ export async function initPresets() {
|
||||
const blackPatches = document.getElementById("blackPatches");
|
||||
if (blackPatches) blackPatches.value = preset.black_patches !== null && preset.black_patches !== undefined ? preset.black_patches : "";
|
||||
|
||||
// Advanced Stage 1 controls
|
||||
const targenGreySteps = document.getElementById("targenGreySteps");
|
||||
if (targenGreySteps) targenGreySteps.value = (preset.grey_steps !== null && preset.grey_steps !== undefined) ? preset.grey_steps : "";
|
||||
|
||||
const targenSingleChannelSteps = document.getElementById("targenSingleChannelSteps");
|
||||
if (targenSingleChannelSteps) targenSingleChannelSteps.value = (preset.single_channel_steps !== null && preset.single_channel_steps !== undefined) ? preset.single_channel_steps : "";
|
||||
|
||||
const targenPrecondProfile = document.getElementById("targenPrecondProfile");
|
||||
if (targenPrecondProfile) targenPrecondProfile.value = preset.preconditioning_profile || "";
|
||||
|
||||
const targenNeutralSteps = document.getElementById("targenNeutralSteps");
|
||||
if (targenNeutralSteps) targenNeutralSteps.value = (preset.neutral_steps !== null && preset.neutral_steps !== undefined) ? preset.neutral_steps : "";
|
||||
|
||||
const targenNeutralConcentration = document.getElementById("targenNeutralConcentration");
|
||||
const targenNeutralConcVal = document.getElementById("targenNeutralConcVal");
|
||||
if (targenNeutralConcentration) {
|
||||
const conc = (preset.neutral_concentration !== null && preset.neutral_concentration !== undefined) ? preset.neutral_concentration : 0.50;
|
||||
targenNeutralConcentration.value = conc;
|
||||
if (targenNeutralConcVal) targenNeutralConcVal.textContent = parseFloat(conc).toFixed(2);
|
||||
}
|
||||
|
||||
const targenHighQuality = document.getElementById("targenHighQuality");
|
||||
if (targenHighQuality) targenHighQuality.checked = !!preset.ofps_high_quality;
|
||||
|
||||
const targenAdaptation = document.getElementById("targenAdaptation");
|
||||
const targenAdaptationVal = document.getElementById("targenAdaptationVal");
|
||||
if (targenAdaptation) {
|
||||
const adapt = (preset.ofps_adaptation !== null && preset.ofps_adaptation !== undefined) ? preset.ofps_adaptation : 0.10;
|
||||
targenAdaptation.value = adapt;
|
||||
if (targenAdaptationVal) targenAdaptationVal.textContent = parseFloat(adapt).toFixed(2);
|
||||
}
|
||||
|
||||
const targenAlgorithm = document.getElementById("targenAlgorithm");
|
||||
if (targenAlgorithm) targenAlgorithm.value = preset.full_spread_algorithm || "ofps";
|
||||
|
||||
const targenInkLimit = document.getElementById("targenInkLimit");
|
||||
if (targenInkLimit) {
|
||||
targenInkLimit.disabled = preset.colour_space.toLowerCase() !== "cmyk";
|
||||
targenInkLimit.value = (preset.total_ink_limit !== null && preset.total_ink_limit !== undefined) ? preset.total_ink_limit : "";
|
||||
}
|
||||
|
||||
const targenDarkEmphasis = document.getElementById("targenDarkEmphasis");
|
||||
const targenDarkEmphasisVal = document.getElementById("targenDarkEmphasisVal");
|
||||
if (targenDarkEmphasis) {
|
||||
const dark = (preset.dark_emphasis !== null && preset.dark_emphasis !== undefined) ? preset.dark_emphasis : 1.00;
|
||||
targenDarkEmphasis.value = dark;
|
||||
if (targenDarkEmphasisVal) targenDarkEmphasisVal.textContent = parseFloat(dark).toFixed(2);
|
||||
}
|
||||
|
||||
const targenDevicePower = document.getElementById("targenDevicePower");
|
||||
if (targenDevicePower) targenDevicePower.value = (preset.device_power !== null && preset.device_power !== undefined) ? preset.device_power : "";
|
||||
|
||||
// Stage 2 controls
|
||||
const instrumentSelect = document.getElementById("instrumentSelect");
|
||||
if (instrumentSelect && preset.instrument) instrumentSelect.value = preset.instrument;
|
||||
@@ -198,6 +250,40 @@ export async function initPresets() {
|
||||
const blackInput = document.getElementById("blackPatches");
|
||||
const black_patches = blackInput && blackInput.value ? parseInt(blackInput.value, 10) : null;
|
||||
|
||||
// Advanced Stage 1 inputs
|
||||
const targenGreySteps = document.getElementById("targenGreySteps");
|
||||
const grey_steps = targenGreySteps && targenGreySteps.value ? parseInt(targenGreySteps.value, 10) : null;
|
||||
|
||||
const targenSingleChannelSteps = document.getElementById("targenSingleChannelSteps");
|
||||
const single_channel_steps = targenSingleChannelSteps && targenSingleChannelSteps.value ? parseInt(targenSingleChannelSteps.value, 10) : null;
|
||||
|
||||
const targenPrecondProfile = document.getElementById("targenPrecondProfile");
|
||||
const preconditioning_profile = targenPrecondProfile && targenPrecondProfile.value.trim() ? targenPrecondProfile.value.trim() : null;
|
||||
|
||||
const targenNeutralSteps = document.getElementById("targenNeutralSteps");
|
||||
const neutral_steps = targenNeutralSteps && targenNeutralSteps.value ? parseInt(targenNeutralSteps.value, 10) : null;
|
||||
|
||||
const targenNeutralConcentration = document.getElementById("targenNeutralConcentration");
|
||||
const neutral_concentration = targenNeutralConcentration ? parseFloat(targenNeutralConcentration.value) : null;
|
||||
|
||||
const targenHighQuality = document.getElementById("targenHighQuality");
|
||||
const ofps_high_quality = targenHighQuality ? targenHighQuality.checked : false;
|
||||
|
||||
const targenAdaptation = document.getElementById("targenAdaptation");
|
||||
const ofps_adaptation = targenAdaptation ? parseFloat(targenAdaptation.value) : null;
|
||||
|
||||
const targenAlgorithm = document.getElementById("targenAlgorithm");
|
||||
const full_spread_algorithm = targenAlgorithm && targenAlgorithm.value !== "ofps" ? targenAlgorithm.value : null;
|
||||
|
||||
const targenInkLimit = document.getElementById("targenInkLimit");
|
||||
const total_ink_limit = (colour_space === "cmyk" && targenInkLimit && targenInkLimit.value) ? parseInt(targenInkLimit.value, 10) : null;
|
||||
|
||||
const targenDarkEmphasis = document.getElementById("targenDarkEmphasis");
|
||||
const dark_emphasis = targenDarkEmphasis ? parseFloat(targenDarkEmphasis.value) : null;
|
||||
|
||||
const targenDevicePower = document.getElementById("targenDevicePower");
|
||||
const device_power = targenDevicePower && targenDevicePower.value ? parseFloat(targenDevicePower.value) : null;
|
||||
|
||||
const instrumentSelect = document.getElementById("instrumentSelect");
|
||||
const instrument = instrumentSelect ? instrumentSelect.value : "i1";
|
||||
|
||||
@@ -224,6 +310,17 @@ export async function initPresets() {
|
||||
patch_count,
|
||||
white_patches,
|
||||
black_patches,
|
||||
grey_steps,
|
||||
single_channel_steps,
|
||||
preconditioning_profile,
|
||||
neutral_steps,
|
||||
neutral_concentration,
|
||||
ofps_high_quality,
|
||||
ofps_adaptation,
|
||||
full_spread_algorithm,
|
||||
total_ink_limit,
|
||||
dark_emphasis,
|
||||
device_power,
|
||||
instrument,
|
||||
page_size,
|
||||
bit_depth,
|
||||
|
||||
@@ -18,8 +18,96 @@ export function initTargen() {
|
||||
const logContainer = document.getElementById("targenLogContainer");
|
||||
const logPre = document.getElementById("targenLog");
|
||||
|
||||
// Advanced controls
|
||||
const targenGreySteps = document.getElementById("targenGreySteps");
|
||||
const targenSingleChannelSteps = document.getElementById("targenSingleChannelSteps");
|
||||
const targenPrecondProfile = document.getElementById("targenPrecondProfile");
|
||||
const btnBrowsePrecondProfile = document.getElementById("btnBrowsePrecondProfile");
|
||||
const targenNeutralSteps = document.getElementById("targenNeutralSteps");
|
||||
const targenNeutralConcentration = document.getElementById("targenNeutralConcentration");
|
||||
const targenNeutralConcVal = document.getElementById("targenNeutralConcVal");
|
||||
const targenAlgorithm = document.getElementById("targenAlgorithm");
|
||||
const targenHighQuality = document.getElementById("targenHighQuality");
|
||||
const targenAdaptation = document.getElementById("targenAdaptation");
|
||||
const targenAdaptationVal = document.getElementById("targenAdaptationVal");
|
||||
const targenInkLimit = document.getElementById("targenInkLimit");
|
||||
const targenDarkEmphasis = document.getElementById("targenDarkEmphasis");
|
||||
const targenDarkEmphasisVal = document.getElementById("targenDarkEmphasisVal");
|
||||
const targenDevicePower = document.getElementById("targenDevicePower");
|
||||
const btnToggleAllHelp = document.getElementById("btnToggleAllHelp");
|
||||
const stage1FormContainer = document.getElementById("stage1FormContainer");
|
||||
|
||||
let currentWorkingDir = "";
|
||||
|
||||
// Help hints toggle
|
||||
if (btnToggleAllHelp && stage1FormContainer) {
|
||||
btnToggleAllHelp.addEventListener("click", () => {
|
||||
stage1FormContainer.classList.toggle("show-all-tooltips");
|
||||
if (stage1FormContainer.classList.contains("show-all-tooltips")) {
|
||||
btnToggleAllHelp.textContent = "💡 Hide Tooltip Hints";
|
||||
} else {
|
||||
btnToggleAllHelp.textContent = "💡 Show Tooltip Hints";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sliders dynamic display
|
||||
if (targenNeutralConcentration && targenNeutralConcVal) {
|
||||
targenNeutralConcentration.addEventListener("input", (e) => {
|
||||
targenNeutralConcVal.textContent = parseFloat(e.target.value).toFixed(2);
|
||||
});
|
||||
}
|
||||
if (targenAdaptation && targenAdaptationVal) {
|
||||
targenAdaptation.addEventListener("input", (e) => {
|
||||
targenAdaptationVal.textContent = parseFloat(e.target.value).toFixed(2);
|
||||
});
|
||||
}
|
||||
if (targenDarkEmphasis && targenDarkEmphasisVal) {
|
||||
targenDarkEmphasis.addEventListener("input", (e) => {
|
||||
targenDarkEmphasisVal.textContent = parseFloat(e.target.value).toFixed(2);
|
||||
});
|
||||
}
|
||||
|
||||
// Colour space change listener to enable/disable ink limit
|
||||
colourSpaceRadios.forEach((radio) => {
|
||||
radio.addEventListener("change", (e) => {
|
||||
const isCmyk = e.target.value === "cmyk";
|
||||
if (targenInkLimit) {
|
||||
targenInkLimit.disabled = !isCmyk;
|
||||
if (!isCmyk) {
|
||||
targenInkLimit.value = "";
|
||||
}
|
||||
}
|
||||
if (blackPatches) {
|
||||
if (isCmyk && (!blackPatches.value || blackPatches.value === "4")) {
|
||||
blackPatches.value = "0";
|
||||
} else if (!isCmyk && (!blackPatches.value || blackPatches.value === "0")) {
|
||||
blackPatches.value = "4";
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Browse preconditioning profile
|
||||
if (btnBrowsePrecondProfile && targenPrecondProfile) {
|
||||
btnBrowsePrecondProfile.addEventListener("click", async () => {
|
||||
try {
|
||||
const filePath = await invoke("select_existing_target", {
|
||||
defaultDir: currentWorkingDir || null,
|
||||
});
|
||||
if (filePath) {
|
||||
targenPrecondProfile.value = filePath;
|
||||
if (targenAdaptation && targenAdaptation.value === "0.1") {
|
||||
targenAdaptation.value = "1.0";
|
||||
if (targenAdaptationVal) targenAdaptationVal.textContent = "1.00";
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to select preconditioning profile:", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateGenerateButton() {
|
||||
const hasBasename = targetBasename && targetBasename.value.trim().length > 0;
|
||||
const hasCwd = currentWorkingDir && currentWorkingDir.trim().length > 0;
|
||||
@@ -210,6 +298,17 @@ export function initTargen() {
|
||||
black_patches: (blackPatches && blackPatches.value) ? parseInt(blackPatches.value, 10) : null,
|
||||
basename: basename,
|
||||
cwd: currentWorkingDir,
|
||||
grey_steps: (targenGreySteps && targenGreySteps.value) ? parseInt(targenGreySteps.value, 10) : null,
|
||||
single_channel_steps: (targenSingleChannelSteps && targenSingleChannelSteps.value) ? parseInt(targenSingleChannelSteps.value, 10) : null,
|
||||
preconditioning_profile: (targenPrecondProfile && targenPrecondProfile.value.trim()) ? targenPrecondProfile.value.trim() : null,
|
||||
neutral_steps: (targenNeutralSteps && targenNeutralSteps.value) ? parseInt(targenNeutralSteps.value, 10) : null,
|
||||
neutral_concentration: (targenNeutralConcentration) ? parseFloat(targenNeutralConcentration.value) : null,
|
||||
ofps_high_quality: (targenHighQuality) ? targenHighQuality.checked : false,
|
||||
ofps_adaptation: (targenAdaptation) ? parseFloat(targenAdaptation.value) : null,
|
||||
full_spread_algorithm: (targenAlgorithm && targenAlgorithm.value !== "ofps") ? targenAlgorithm.value : null,
|
||||
total_ink_limit: (colourSpace === "cmyk" && targenInkLimit && targenInkLimit.value) ? parseInt(targenInkLimit.value, 10) : null,
|
||||
dark_emphasis: (targenDarkEmphasis) ? parseFloat(targenDarkEmphasis.value) : null,
|
||||
device_power: (targenDevicePower && targenDevicePower.value) ? parseFloat(targenDevicePower.value) : null,
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -1155,3 +1155,36 @@ button.danger:hover {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Tooltip & Help Styling */
|
||||
.has-tooltip {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip-text {
|
||||
display: none;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
color: #e0e0e0;
|
||||
background: #252530;
|
||||
border: 1px solid #444455;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
margin-top: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.has-tooltip:hover .tooltip-text,
|
||||
.has-tooltip:focus-within .tooltip-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.show-all-tooltips .tooltip-text {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.advanced-section-details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user