diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 856512c..5c9b3c3 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iccery" -version = "0.2.1" +version = "0.3.0" description = "Modern Printer Profiling UI frontend for ArgyllCMS" authors = ["Gordon"] edition = "2021" diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 7d3bdfc..ec7b5c9 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -30,14 +30,27 @@ pub async fn kill_process( state.kill(&id).await } +pub fn get_binary_candidates(binary_name: &str) -> Vec { + if cfg!(windows) && !binary_name.to_lowercase().ends_with(".exe") { + vec![format!("{}.exe", binary_name), binary_name.to_string()] + } else { + vec![binary_name.to_string()] + } +} + #[tauri::command] pub async fn resolve_binary(app: AppHandle, binary_name: String) -> Result { let settings = crate::settings::load_settings(app.clone()).unwrap_or_default(); + let candidates = get_binary_candidates(&binary_name); + if let Some(dir) = settings.argyll_binary_dir { if !dir.trim().is_empty() { - let custom_path = std::path::Path::new(&dir).join(&binary_name); - if custom_path.exists() { - return Ok(custom_path.to_string_lossy().to_string()); + let base_dir = std::path::Path::new(&dir); + for name in &candidates { + let custom_path = base_dir.join(name); + if custom_path.exists() { + return Ok(custom_path.to_string_lossy().to_string()); + } } } } @@ -50,10 +63,22 @@ pub async fn resolve_binary(app: AppHandle, binary_name: String) -> Result "linux-x86_64", }; + for name in &candidates { + if let Ok(resource_path) = app.path().resolve( + format!("argyll/{}/{}", platform, name), + tauri::path::BaseDirectory::Resource, + ) { + if resource_path.exists() { + return Ok(resource_path.to_string_lossy().to_string()); + } + } + } + + let primary_name = &candidates[0]; let resource_path = app .path() .resolve( - format!("argyll/{}/{}", platform, binary_name), + format!("argyll/{}/{}", platform, primary_name), tauri::path::BaseDirectory::Resource, ) .map_err(|e| e.to_string())?; @@ -361,14 +386,24 @@ pub async fn read_tiff_preview_png(path: String) -> Result { pub struct ChartreadConfig { pub basename: String, pub cwd: String, + pub port: Option, } pub fn build_chartread_args(config: &ChartreadConfig) -> Vec { - vec![ + let mut args = vec![ "-v".to_string(), "-u".to_string(), - config.basename.clone(), - ] + ]; + + if let Some(ref port) = config.port { + if !port.trim().is_empty() { + args.push("-c".to_string()); + args.push(port.trim().to_string()); + } + } + + args.push(config.basename.clone()); + args } #[tauri::command] @@ -385,6 +420,36 @@ pub async fn run_chartread( state.spawn(app, id, binary, args, cwd).await } +#[derive(Debug, Deserialize, Serialize)] +pub struct AverageConfig { + pub inputs: Vec, + pub output: String, + pub cwd: String, +} + +pub fn build_average_args(config: &AverageConfig) -> Vec { + let mut args = vec!["-v".to_string()]; + for input in &config.inputs { + args.push(input.clone()); + } + args.push(config.output.clone()); + args +} + +#[tauri::command] +pub async fn run_average( + app: AppHandle, + state: State<'_, ProcessManager>, + config: AverageConfig, +) -> Result<(), String> { + let binary = resolve_binary(app.clone(), "average".to_string()).await?; + let args = build_average_args(&config); + let id = format!("average_{}", config.output); + let cwd = Some(resolve_safe_cwd(&app, &config.cwd)?); + + state.spawn(app, id, binary, args, cwd).await +} + #[derive(Debug, Deserialize, Serialize)] pub struct ColprofConfig { pub algorithm: String, @@ -674,11 +739,34 @@ mod tests { let config = ChartreadConfig { basename: "my_profile".to_string(), cwd: "/home/user".to_string(), + port: None, }; let args = build_chartread_args(&config); assert_eq!(args, vec!["-v", "-u", "my_profile"]); } + #[test] + fn test_build_chartread_args_with_port() { + let config = ChartreadConfig { + basename: "my_profile".to_string(), + cwd: "/home/user".to_string(), + port: Some("1".to_string()), + }; + let args = build_chartread_args(&config); + assert_eq!(args, vec!["-v", "-u", "-c", "1", "my_profile"]); + } + + #[test] + fn test_build_average_args() { + let config = AverageConfig { + inputs: vec!["pass1.ti3".to_string(), "pass2.ti3".to_string()], + output: "avg.ti3".to_string(), + cwd: "/home/user".to_string(), + }; + let args = build_average_args(&config); + assert_eq!(args, vec!["-v", "pass1.ti3", "pass2.ti3", "avg.ti3"]); + } + #[test] fn test_build_colprof_args() { let config = ColprofConfig { @@ -707,4 +795,17 @@ mod tests { let args = build_profcheck_args(&config); assert_eq!(args, vec!["-v", "-k", "-s", "my_profile.ti3", "my_profile.icc"]); } -} + + #[test] + fn test_get_binary_candidates() { + let candidates = get_binary_candidates("targen"); + if cfg!(windows) { + assert_eq!(candidates, vec!["targen.exe", "targen"]); + } else { + assert_eq!(candidates, vec!["targen"]); + } + + let candidates_exe = get_binary_candidates("targen.exe"); + assert_eq!(candidates_exe, vec!["targen.exe"]); + } +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 31cda44..8eb4dee 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -26,6 +26,7 @@ pub fn run() { commands::read_file_base64, commands::read_tiff_preview_png, commands::run_chartread, + commands::run_average, commands::run_colprof, commands::run_profcheck, commands::get_printers, @@ -34,6 +35,11 @@ pub fn run() { commands::print_target_native, settings::load_settings, settings::save_settings, + settings::get_all_presets, + settings::save_preset, + settings::delete_preset, + settings::export_preset_json, + settings::import_preset_json, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 974d911..c444aec 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -2,10 +2,99 @@ use serde::{Deserialize, Serialize}; use std::fs; use tauri::{AppHandle, Manager}; +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct ProfilingPreset { + pub id: String, + pub name: String, + pub description: Option, + pub colour_space: String, + pub patch_count: u32, + pub white_patches: Option, + pub black_patches: Option, + pub instrument: String, + pub page_size: String, + pub bit_depth: u8, + pub dpi: u32, + pub colprof_algorithm: String, + pub colprof_quality: String, + pub colprof_intent: Option, +} + #[derive(Debug, Deserialize, Serialize, Default, Clone)] pub struct AppSettings { pub argyll_binary_dir: Option, pub default_instrument: Option, + #[serde(default)] + pub custom_presets: Vec, +} + +pub fn get_default_presets() -> Vec { + vec![ + ProfilingPreset { + id: "preset-std-rgb".to_string(), + name: "Standard RGB Photo (800 patches)".to_string(), + description: Some("Recommended for standard fine-art and photographic RGB printing on inkjet printers.".to_string()), + colour_space: "rgb".to_string(), + patch_count: 800, + white_patches: Some(4), + black_patches: None, + instrument: "i1".to_string(), + page_size: "A4".to_string(), + bit_depth: 8, + dpi: 300, + colprof_algorithm: "l".to_string(), + colprof_quality: "m".to_string(), + colprof_intent: None, + }, + ProfilingPreset { + id: "preset-hq-cmyk".to_string(), + name: "High-Gamut CMYK Proofing (1500 patches)".to_string(), + description: Some("High-precision CMYK press and RIP proofing profiling with deep shadow neutral boost.".to_string()), + colour_space: "cmyk".to_string(), + patch_count: 1500, + white_patches: None, + black_patches: Some(8), + instrument: "i1".to_string(), + page_size: "A3".to_string(), + bit_depth: 16, + dpi: 300, + colprof_algorithm: "l".to_string(), + colprof_quality: "h".to_string(), + colprof_intent: None, + }, + ProfilingPreset { + id: "preset-draft-rgb".to_string(), + name: "Fast RGB Draft (400 patches)".to_string(), + description: Some("Quick turn-around draft profiling for testing new media.".to_string()), + colour_space: "rgb".to_string(), + patch_count: 400, + white_patches: None, + black_patches: None, + instrument: "i1".to_string(), + page_size: "A4".to_string(), + bit_depth: 8, + dpi: 150, + colprof_algorithm: "l".to_string(), + colprof_quality: "l".to_string(), + colprof_intent: None, + }, + ProfilingPreset { + id: "preset-ultra-rgb".to_string(), + name: "Ultra Precision RGB (2500 patches)".to_string(), + description: Some("Maximum precision lookup table profile for exhibition and master printmaking.".to_string()), + colour_space: "rgb".to_string(), + patch_count: 2500, + white_patches: Some(6), + black_patches: Some(6), + instrument: "i1".to_string(), + page_size: "A3".to_string(), + bit_depth: 16, + dpi: 300, + colprof_algorithm: "l".to_string(), + colprof_quality: "u".to_string(), + colprof_intent: None, + }, + ] } #[tauri::command] @@ -25,3 +114,81 @@ pub fn save_settings(app: AppHandle, settings: AppSettings) -> Result<(), String let json = serde_json::to_string_pretty(&settings).unwrap(); fs::write(path.join("settings.json"), json).map_err(|e| e.to_string()) } + +#[tauri::command] +pub fn get_all_presets(app: AppHandle) -> Vec { + let mut presets = get_default_presets(); + let settings = load_settings(app).unwrap_or_default(); + presets.extend(settings.custom_presets); + presets +} + +#[tauri::command] +pub fn save_preset(app: AppHandle, preset: ProfilingPreset) -> Result, String> { + let mut settings = load_settings(app.clone()).unwrap_or_default(); + if let Some(pos) = settings.custom_presets.iter().position(|p| p.id == preset.id) { + settings.custom_presets[pos] = preset; + } else { + settings.custom_presets.push(preset); + } + save_settings(app.clone(), settings)?; + Ok(get_all_presets(app)) +} + +#[tauri::command] +pub fn delete_preset(app: AppHandle, id: String) -> Result, String> { + let mut settings = load_settings(app.clone()).unwrap_or_default(); + settings.custom_presets.retain(|p| p.id != id); + save_settings(app.clone(), settings)?; + Ok(get_all_presets(app)) +} + +#[tauri::command] +pub fn export_preset_json(preset: ProfilingPreset) -> Result { + serde_json::to_string_pretty(&preset).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn import_preset_json(json: String) -> Result { + serde_json::from_str::(&json).map_err(|e| format!("Invalid preset format: {}", e)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_default_presets() { + let defaults = get_default_presets(); + assert_eq!(defaults.len(), 4); + assert_eq!(defaults[0].id, "preset-std-rgb"); + assert_eq!(defaults[0].colour_space, "rgb"); + assert_eq!(defaults[0].patch_count, 800); + assert_eq!(defaults[1].colour_space, "cmyk"); + assert_eq!(defaults[1].patch_count, 1500); + } + + #[test] + fn test_preset_json_export_and_import() { + let preset = ProfilingPreset { + id: "custom-test".to_string(), + name: "Custom Test Preset".to_string(), + description: Some("Test description".to_string()), + colour_space: "rgb".to_string(), + patch_count: 1200, + white_patches: Some(2), + black_patches: Some(4), + instrument: "i1".to_string(), + page_size: "A4".to_string(), + bit_depth: 16, + dpi: 300, + colprof_algorithm: "l".to_string(), + colprof_quality: "h".to_string(), + colprof_intent: None, + }; + + let json = export_preset_json(preset.clone()).expect("Export failed"); + let imported = import_preset_json(json).expect("Import failed"); + assert_eq!(preset, imported); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 087b01f..2db0bff 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ICCery", - "version": "0.2.1", + "version": "0.3.0", "identifier": "com.gronod.iccery", "build": { "frontendDist": "../src" diff --git a/src/index.html b/src/index.html index 4af00bd..76be7ca 100644 --- a/src/index.html +++ b/src/index.html @@ -8,7 +8,6 @@ - @@ -32,6 +31,21 @@ + + +
+
+ +
+ + +
+
+ +
+