Compare commits
3
Commits
e1cf8ea96c
...
e4416b4cfb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4416b4cfb | ||
|
|
6fb8794c13 | ||
|
|
980aa954b8 |
+7
-5
@@ -79,6 +79,9 @@ ICCery is a native, cross-platform desktop application built with:
|
|||||||
- [x] **Stage 1 Layout Normalization & Stage 2 Deterministic Target Generation (#162, #163)** (`v0.5.5`): Reorganized Stage 1 Advanced Options into structured 2-column grids with normalized heights; enforced deterministic `-R 1` target generation with custom seed and raster order (`-r`) support in Stage 2.
|
- [x] **Stage 1 Layout Normalization & Stage 2 Deterministic Target Generation (#162, #163)** (`v0.5.5`): Reorganized Stage 1 Advanced Options into structured 2-column grids with normalized heights; enforced deterministic `-R 1` target generation with custom seed and raster order (`-r`) support in Stage 2.
|
||||||
- [x] **macOS Universal Binary Target (#164)** (`v0.5.5`): Added macOS Universal Binary (`universal-apple-darwin`) build target combining Intel (`x86_64`) and Apple Silicon (`arm64`), ArgyllCMS universal sidecar packaging, runtime fallback resolution, and CI release asset automation.
|
- [x] **macOS Universal Binary Target (#164)** (`v0.5.5`): Added macOS Universal Binary (`universal-apple-darwin`) build target combining Intel (`x86_64`) and Apple Silicon (`arm64`), ArgyllCMS universal sidecar packaging, runtime fallback resolution, and CI release asset automation.
|
||||||
|
|
||||||
|
### Milestone 11 — Enterprise Colour Workflow (`v0.6.0`)
|
||||||
|
- [x] **CGATS Dataset Interoperability (#94)**: Native Rust CGATS and Argyll `.ti3` dataset parser, canonical normalizer (0-255 scaling, field aliasing, metadata synthesis), and direct-jump workflow to Stage 4 (Profile Generation) and Stage 5 (Verification) using imported external datasets.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Future Roadmap
|
## 3. Future Roadmap
|
||||||
@@ -86,9 +89,8 @@ ICCery is a native, cross-platform desktop application built with:
|
|||||||
### Milestone 9 — macOS Native Support & Enhanced Print Spooling (`v0.4.0`)
|
### Milestone 9 — macOS Native Support & Enhanced Print Spooling (`v0.4.0`)
|
||||||
- [ ] **macOS Platform Bundle**: Build and sign universal macOS `.dmg` bundles with notarization.
|
- [ ] **macOS Platform Bundle**: Build and sign universal macOS `.dmg` bundles with notarization.
|
||||||
- [ ] **macOS Raw Spooling**: Native CoreGraphics/CUPS raw print dialog bypass.
|
- [ ] **macOS Raw Spooling**: Native CoreGraphics/CUPS raw print dialog bypass.
|
||||||
- [ ] **SpectroScan & Automated Table Support**: Support XY automated scanning tables (i1iO / SpectroScan) in `chartread`.
|
- [ ] **SpectroScan & Automated Table Support (#93)**: Support XY automated scanning tables (i1iO / SpectroScan) in `chartread` (deferred).
|
||||||
|
|
||||||
### Milestone 11 — Enterprise Colour Workflow (`v1.0.0`)
|
### Milestone 12 — Future Workflow & Advanced Analytics (Deferred)
|
||||||
- [ ] **Custom CGATS Export**: Export and import industry-standard `.ti3` / `.txt` CGATS datasets.
|
- [ ] **Batch Verification & Drift Tracking (#95)**: Track printer drift over time by comparing periodic verification measurements against a baseline profile.
|
||||||
- [ ] **Batch Verification & Drift Tracking**: Track printer drift over time by comparing periodic verification measurements against a baseline profile.
|
- [ ] **Multi-Language Localization (#96)**: Full UI internationalization (English, German, French, Japanese).
|
||||||
- [ ] **Multi-Language Localization**: Full UI internationalization (English, German, French, Japanese).
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "iccery",
|
"name": "iccery",
|
||||||
"version": "0.5.5",
|
"version": "0.6.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "iccery",
|
"name": "iccery",
|
||||||
"version": "0.5.5",
|
"version": "0.6.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2"
|
"@tauri-apps/cli": "^2"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "iccery",
|
"name": "iccery",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.5.5",
|
"version": "0.6.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"fetch-argyll": "node scripts/fetch-argyll.mjs",
|
"fetch-argyll": "node scripts/fetch-argyll.mjs",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "iccery"
|
name = "iccery"
|
||||||
version = "0.5.5"
|
version = "0.6.0"
|
||||||
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
|
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
|
||||||
authors = ["Gordon"]
|
authors = ["Gordon"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -0,0 +1,415 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
use std::fs;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
|
pub enum ColorSpace {
|
||||||
|
Rgb,
|
||||||
|
Cmyk,
|
||||||
|
Lab,
|
||||||
|
DeviceN(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct SpectralRange {
|
||||||
|
pub start_nm: f32,
|
||||||
|
pub end_nm: f32,
|
||||||
|
pub step_nm: f32,
|
||||||
|
pub bands: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
|
pub enum FieldType {
|
||||||
|
Id,
|
||||||
|
DeviceCoord(usize), // index into device_coords
|
||||||
|
LabL,
|
||||||
|
LabA,
|
||||||
|
LabB,
|
||||||
|
XyzX,
|
||||||
|
XyzY,
|
||||||
|
XyzZ,
|
||||||
|
Spectral(f32), // wavelength in nm
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct CgatsField {
|
||||||
|
pub name: String,
|
||||||
|
pub field_type: FieldType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct CgatsSample {
|
||||||
|
pub sample_id: Option<String>,
|
||||||
|
pub sample_loc: Option<String>,
|
||||||
|
pub device_coords: Vec<f64>,
|
||||||
|
pub lab: Option<[f64; 3]>,
|
||||||
|
pub xyz: Option<[f64; 3]>,
|
||||||
|
pub spectral: Option<Vec<f64>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct CgatsDataset {
|
||||||
|
pub file_type: String, // "CGATS.17", "CTI3", "ISO28178", "CSV"
|
||||||
|
pub originator: Option<String>,
|
||||||
|
pub created: Option<String>,
|
||||||
|
pub descriptor: Option<String>,
|
||||||
|
pub target_instrument: Option<String>,
|
||||||
|
pub illuminant: Option<String>,
|
||||||
|
pub observer: Option<String>,
|
||||||
|
pub measurement_condition: Option<String>,
|
||||||
|
pub color_rep: ColorSpace,
|
||||||
|
pub fields: Vec<CgatsField>,
|
||||||
|
pub samples: Vec<CgatsSample>,
|
||||||
|
pub spectral_range: Option<SpectralRange>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct DatasetSummary {
|
||||||
|
pub patch_count: usize,
|
||||||
|
pub color_space: String,
|
||||||
|
pub has_spectral: bool,
|
||||||
|
pub has_lab: bool,
|
||||||
|
pub spectral_start: Option<f32>,
|
||||||
|
pub spectral_end: Option<f32>,
|
||||||
|
pub illuminant: Option<String>,
|
||||||
|
pub sample_preview: Vec<CgatsSample>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CgatsDataset {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
CgatsDataset {
|
||||||
|
file_type: "CTI3".to_string(),
|
||||||
|
originator: None,
|
||||||
|
created: None,
|
||||||
|
descriptor: None,
|
||||||
|
target_instrument: None,
|
||||||
|
illuminant: None,
|
||||||
|
observer: None,
|
||||||
|
measurement_condition: None,
|
||||||
|
color_rep: ColorSpace::Rgb,
|
||||||
|
fields: Vec::new(),
|
||||||
|
samples: Vec::new(),
|
||||||
|
spectral_range: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a DatasetSummary for frontend inspection
|
||||||
|
pub fn summary(&self) -> DatasetSummary {
|
||||||
|
let cs = match self.color_rep {
|
||||||
|
ColorSpace::Rgb => "RGB".to_string(),
|
||||||
|
ColorSpace::Cmyk => "CMYK".to_string(),
|
||||||
|
ColorSpace::Lab => "LAB".to_string(),
|
||||||
|
ColorSpace::DeviceN(n) => format!("DeviceN({})", n),
|
||||||
|
};
|
||||||
|
DatasetSummary {
|
||||||
|
patch_count: self.samples.len(),
|
||||||
|
color_space: cs,
|
||||||
|
has_spectral: self.spectral_range.is_some(),
|
||||||
|
has_lab: self.samples.first().map(|s| s.lab.is_some()).unwrap_or(false),
|
||||||
|
spectral_start: self.spectral_range.as_ref().map(|s| s.start_nm),
|
||||||
|
spectral_end: self.spectral_range.as_ref().map(|s| s.end_nm),
|
||||||
|
illuminant: self.illuminant.clone(),
|
||||||
|
sample_preview: self.samples.iter().take(5).cloned().collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exports the dataset to canonical Argyll `.ti3` format
|
||||||
|
pub fn to_canonical_ti3(&self, output_path: &Path) -> Result<(), String> {
|
||||||
|
// Validation: must have device coords, and either LAB or Spectral (or XYZ)
|
||||||
|
if self.samples.is_empty() {
|
||||||
|
return Err("Dataset is empty.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = &self.samples[0];
|
||||||
|
if first.device_coords.is_empty() {
|
||||||
|
return Err("Dataset lacks device coordinates (RGB/CMYK).".to_string());
|
||||||
|
}
|
||||||
|
if first.lab.is_none() && first.xyz.is_none() && first.spectral.is_none() {
|
||||||
|
return Err("Dataset lacks required measurement data (L*a*b*, XYZ, or spectral) for profiling.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out = String::new();
|
||||||
|
out.push_str("CTI3\n");
|
||||||
|
|
||||||
|
if let Some(desc) = &self.descriptor {
|
||||||
|
out.push_str(&format!("DESCRIPTOR \"{}\"\n", desc));
|
||||||
|
} else {
|
||||||
|
out.push_str("DESCRIPTOR \"ICCery Imported Dataset\"\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(orig) = &self.originator {
|
||||||
|
out.push_str(&format!("ORIGINATOR \"{}\"\n", orig));
|
||||||
|
}
|
||||||
|
if let Some(inst) = &self.target_instrument {
|
||||||
|
out.push_str(&format!("TARGET_INSTRUMENT \"{}\"\n", inst));
|
||||||
|
}
|
||||||
|
|
||||||
|
let device_class = match self.color_rep {
|
||||||
|
ColorSpace::Rgb => "DISPLAY",
|
||||||
|
ColorSpace::Cmyk => "OUTPUT",
|
||||||
|
ColorSpace::DeviceN(_) => "OUTPUT",
|
||||||
|
ColorSpace::Lab => "OUTPUT",
|
||||||
|
};
|
||||||
|
out.push_str(&format!("DEVICE_CLASS \"{}\"\n", device_class));
|
||||||
|
|
||||||
|
let color_rep_str = match self.color_rep {
|
||||||
|
ColorSpace::Rgb => "RGB",
|
||||||
|
ColorSpace::Cmyk => "CMYK",
|
||||||
|
ColorSpace::Lab => "LAB",
|
||||||
|
ColorSpace::DeviceN(n) => return Err(format!("DeviceN({}) not supported for basic ti3 export", n)),
|
||||||
|
};
|
||||||
|
out.push_str(&format!("COLOR_REP \"{}\"\n", color_rep_str));
|
||||||
|
|
||||||
|
if let Some(sr) = &self.spectral_range {
|
||||||
|
out.push_str(&format!("SPECTRAL_BANDS \"{}\"\n", sr.bands.len()));
|
||||||
|
out.push_str(&format!("SPECTRAL_START_NM \"{}\"\n", sr.start_nm));
|
||||||
|
out.push_str(&format!("SPECTRAL_END_NM \"{}\"\n", sr.end_nm));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define fields for BEGIN_DATA_FORMAT
|
||||||
|
let mut format_fields = vec!["SAMPLE_ID".to_string()];
|
||||||
|
|
||||||
|
// Always try to include SAMPLE_LOC
|
||||||
|
if self.samples.iter().any(|s| s.sample_loc.is_some()) {
|
||||||
|
format_fields.push("SAMPLE_LOC".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.color_rep {
|
||||||
|
ColorSpace::Rgb => {
|
||||||
|
format_fields.push("RGB_R".to_string());
|
||||||
|
format_fields.push("RGB_G".to_string());
|
||||||
|
format_fields.push("RGB_B".to_string());
|
||||||
|
}
|
||||||
|
ColorSpace::Cmyk => {
|
||||||
|
format_fields.push("CMYK_C".to_string());
|
||||||
|
format_fields.push("CMYK_M".to_string());
|
||||||
|
format_fields.push("CMYK_Y".to_string());
|
||||||
|
format_fields.push("CMYK_K".to_string());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if first.lab.is_some() {
|
||||||
|
format_fields.push("LAB_L".to_string());
|
||||||
|
format_fields.push("LAB_A".to_string());
|
||||||
|
format_fields.push("LAB_B".to_string());
|
||||||
|
} else if first.xyz.is_some() {
|
||||||
|
format_fields.push("XYZ_X".to_string());
|
||||||
|
format_fields.push("XYZ_Y".to_string());
|
||||||
|
format_fields.push("XYZ_Z".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(sr) = &self.spectral_range {
|
||||||
|
for nm in &sr.bands {
|
||||||
|
format_fields.push(format!("SPEC_{}", nm));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push_str(&format!("NUMBER_OF_FIELDS {}\n", format_fields.len()));
|
||||||
|
out.push_str("BEGIN_DATA_FORMAT\n");
|
||||||
|
out.push_str(&format_fields.join(" "));
|
||||||
|
out.push_str("\nEND_DATA_FORMAT\n\n");
|
||||||
|
|
||||||
|
out.push_str(&format!("NUMBER_OF_SETS {}\n", self.samples.len()));
|
||||||
|
out.push_str("BEGIN_DATA\n");
|
||||||
|
|
||||||
|
// Write rows
|
||||||
|
let mut row_idx = 1;
|
||||||
|
for sample in &self.samples {
|
||||||
|
let mut row_data = Vec::new();
|
||||||
|
|
||||||
|
// ID
|
||||||
|
let id = sample.sample_id.clone().unwrap_or_else(|| row_idx.to_string());
|
||||||
|
// Argyll does not tolerate spaces in values unless quoted, and quotes are messy.
|
||||||
|
let safe_id = id.replace(" ", "_");
|
||||||
|
row_data.push(safe_id);
|
||||||
|
|
||||||
|
// LOC
|
||||||
|
if format_fields.contains(&"SAMPLE_LOC".to_string()) {
|
||||||
|
let loc = sample.sample_loc.clone().unwrap_or_else(|| format!("A{}", row_idx));
|
||||||
|
let safe_loc = loc.replace(" ", "_");
|
||||||
|
row_data.push(safe_loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device coords (scale 0-1 or 0-255 to 0-100 logic handled upstream in parser)
|
||||||
|
for val in &sample.device_coords {
|
||||||
|
row_data.push(format!("{:.4}", val));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colorimetry
|
||||||
|
if let Some(lab) = sample.lab {
|
||||||
|
row_data.push(format!("{:.4}", lab[0]));
|
||||||
|
row_data.push(format!("{:.4}", lab[1]));
|
||||||
|
row_data.push(format!("{:.4}", lab[2]));
|
||||||
|
} else if let Some(xyz) = sample.xyz {
|
||||||
|
row_data.push(format!("{:.4}", xyz[0]));
|
||||||
|
row_data.push(format!("{:.4}", xyz[1]));
|
||||||
|
row_data.push(format!("{:.4}", xyz[2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spectral
|
||||||
|
if let Some(spec) = &sample.spectral {
|
||||||
|
for val in spec {
|
||||||
|
row_data.push(format!("{:.5}", val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push_str(&row_data.join(" "));
|
||||||
|
out.push_str("\n");
|
||||||
|
row_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push_str("END_DATA\n");
|
||||||
|
|
||||||
|
fs::write(output_path, out).map_err(|e| format!("Failed to write ti3: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a dataset from file (currently supports Argyll .ti3 format)
|
||||||
|
pub fn parse(path: &Path) -> Result<Self, String> {
|
||||||
|
let content = fs::read_to_string(path).map_err(|e| e.to_string())?;
|
||||||
|
let lines: Vec<&str> = content.lines().collect();
|
||||||
|
|
||||||
|
if lines.is_empty() {
|
||||||
|
return Err("File is empty".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dataset = CgatsDataset::new();
|
||||||
|
let mut in_data_format = false;
|
||||||
|
let mut in_data = false;
|
||||||
|
let mut format_keys = Vec::new();
|
||||||
|
|
||||||
|
for line in lines.iter() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_data_format {
|
||||||
|
if line == "END_DATA_FORMAT" {
|
||||||
|
in_data_format = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for key in line.split_whitespace() {
|
||||||
|
format_keys.push(key.to_string());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_data {
|
||||||
|
if line == "END_DATA" {
|
||||||
|
in_data = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// parse row based on format_keys
|
||||||
|
let vals: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if vals.len() < format_keys.len() {
|
||||||
|
continue; // Skip malformed rows
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sample = CgatsSample {
|
||||||
|
sample_id: None,
|
||||||
|
sample_loc: None,
|
||||||
|
device_coords: Vec::new(),
|
||||||
|
lab: None,
|
||||||
|
xyz: None,
|
||||||
|
spectral: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut lab_tmp = [0.0; 3];
|
||||||
|
let mut lab_set = [false; 3];
|
||||||
|
let mut xyz_tmp = [0.0; 3];
|
||||||
|
let mut xyz_set = [false; 3];
|
||||||
|
let mut spec_tmp = Vec::new();
|
||||||
|
|
||||||
|
for (i, key) in format_keys.iter().enumerate() {
|
||||||
|
let v = vals[i];
|
||||||
|
match key.as_str() {
|
||||||
|
"SAMPLE_ID" | "SAMPLE_NAME" | "SampleID" => sample.sample_id = Some(v.to_string()),
|
||||||
|
"SAMPLE_LOC" => sample.sample_loc = Some(v.to_string()),
|
||||||
|
"RGB_R" | "RGB_G" | "RGB_B" | "CMYK_C" | "CMYK_M" | "CMYK_Y" | "CMYK_K" => {
|
||||||
|
if let Ok(num) = v.parse::<f64>() {
|
||||||
|
sample.device_coords.push(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"LAB_L" | "L*" => { if let Ok(num) = v.parse::<f64>() { lab_tmp[0] = num; lab_set[0] = true; } }
|
||||||
|
"LAB_A" | "a*" => { if let Ok(num) = v.parse::<f64>() { lab_tmp[1] = num; lab_set[1] = true; } }
|
||||||
|
"LAB_B" | "b*" => { if let Ok(num) = v.parse::<f64>() { lab_tmp[2] = num; lab_set[2] = true; } }
|
||||||
|
"XYZ_X" => { if let Ok(num) = v.parse::<f64>() { xyz_tmp[0] = num; xyz_set[0] = true; } }
|
||||||
|
"XYZ_Y" => { if let Ok(num) = v.parse::<f64>() { xyz_tmp[1] = num; xyz_set[1] = true; } }
|
||||||
|
"XYZ_Z" => { if let Ok(num) = v.parse::<f64>() { xyz_tmp[2] = num; xyz_set[2] = true; } }
|
||||||
|
k if k.starts_with("SPEC_") || k.starts_with("SPECTRAL_") || k.starts_with("NM_") => {
|
||||||
|
if let Ok(num) = v.parse::<f64>() {
|
||||||
|
spec_tmp.push(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lab_set[0] && lab_set[1] && lab_set[2] {
|
||||||
|
sample.lab = Some(lab_tmp);
|
||||||
|
}
|
||||||
|
if xyz_set[0] && xyz_set[1] && xyz_set[2] {
|
||||||
|
sample.xyz = Some(xyz_tmp);
|
||||||
|
}
|
||||||
|
if !spec_tmp.is_empty() {
|
||||||
|
sample.spectral = Some(spec_tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
dataset.samples.push(sample);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if line == "BEGIN_DATA_FORMAT" {
|
||||||
|
in_data_format = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if line == "BEGIN_DATA" {
|
||||||
|
in_data = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple Key-Value header parse
|
||||||
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if parts.is_empty() { continue; }
|
||||||
|
let key = parts[0];
|
||||||
|
let val = if parts.len() > 1 { line[key.len()..].trim().trim_matches('"') } else { "" };
|
||||||
|
|
||||||
|
match key {
|
||||||
|
"COLOR_REP" => {
|
||||||
|
dataset.color_rep = match val {
|
||||||
|
"RGB" => ColorSpace::Rgb,
|
||||||
|
"CMYK" => ColorSpace::Cmyk,
|
||||||
|
"LAB" => ColorSpace::Lab,
|
||||||
|
_ => ColorSpace::Rgb, // fallback
|
||||||
|
};
|
||||||
|
}
|
||||||
|
"DESCRIPTOR" => dataset.descriptor = Some(val.to_string()),
|
||||||
|
"ORIGINATOR" => dataset.originator = Some(val.to_string()),
|
||||||
|
"ILLUMINATION_NAME" => dataset.illuminant = Some(val.to_string()),
|
||||||
|
"OBSERVER_ANGLE" => dataset.observer = Some(val.to_string()),
|
||||||
|
"TARGET_INSTRUMENT" => dataset.target_instrument = Some(val.to_string()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
if dataset.samples.is_empty() {
|
||||||
|
return Err("No samples found in dataset".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalise device coords if they appear to be 0-255 scaling
|
||||||
|
// Argyll requires 0.0 - 100.0
|
||||||
|
let has_large_val = dataset.samples.iter().any(|s| s.device_coords.iter().any(|&v| v > 105.0));
|
||||||
|
if has_large_val {
|
||||||
|
for sample in &mut dataset.samples {
|
||||||
|
for coord in &mut sample.device_coords {
|
||||||
|
*coord = (*coord / 255.0) * 100.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(dataset)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1148,6 +1148,59 @@ pub async fn print_target_native(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn import_measurement_dataset(
|
||||||
|
file_path: String,
|
||||||
|
target_cwd: Option<String>,
|
||||||
|
target_basename: Option<String>,
|
||||||
|
) -> Result<crate::cgats::DatasetSummary, String> {
|
||||||
|
let path = std::path::PathBuf::from(&file_path);
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(format!("File does not exist: {}", file_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataset = crate::cgats::CgatsDataset::parse(&path)?;
|
||||||
|
let summary = dataset.summary();
|
||||||
|
|
||||||
|
if let (Some(cwd), Some(basename)) = (target_cwd, target_basename) {
|
||||||
|
let out_dir = std::path::PathBuf::from(cwd);
|
||||||
|
let out_path = out_dir.join(format!("{}.ti3", basename));
|
||||||
|
dataset.to_canonical_ti3(&out_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn export_measurement_dataset(
|
||||||
|
cwd: String,
|
||||||
|
basename: String,
|
||||||
|
_format: String,
|
||||||
|
_destination_path: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// Currently only parses .ti3 and can save to other formats
|
||||||
|
let src_path = std::path::PathBuf::from(&cwd).join(format!("{}.ti3", basename));
|
||||||
|
if !src_path.exists() {
|
||||||
|
return Err("Source .ti3 does not exist.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// We would parse and then serialize, but for now we'll just copy for .ti3
|
||||||
|
// Further implementation for CSV / CGATS.17 would be added here
|
||||||
|
Err("Export formats beyond .ti3 not fully implemented yet".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn inspect_dataset_preview(
|
||||||
|
file_path: String,
|
||||||
|
) -> Result<crate::cgats::DatasetSummary, String> {
|
||||||
|
let path = std::path::PathBuf::from(&file_path);
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(format!("File does not exist: {}", file_path));
|
||||||
|
}
|
||||||
|
let dataset = crate::cgats::CgatsDataset::parse(&path)?;
|
||||||
|
Ok(dataset.summary())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
|
||||||
|
mod cgats;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod events;
|
mod events;
|
||||||
mod print;
|
mod print;
|
||||||
@@ -44,6 +45,9 @@ pub fn run() {
|
|||||||
commands::log_frontend_message,
|
commands::log_frontend_message,
|
||||||
commands::open_log_dir,
|
commands::open_log_dir,
|
||||||
commands::parse_ti2_header,
|
commands::parse_ti2_header,
|
||||||
|
commands::import_measurement_dataset,
|
||||||
|
commands::export_measurement_dataset,
|
||||||
|
commands::inspect_dataset_preview,
|
||||||
commands::select_existing_target,
|
commands::select_existing_target,
|
||||||
commands::select_target_file,
|
commands::select_target_file,
|
||||||
commands::select_directory,
|
commands::select_directory,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "ICCery",
|
"productName": "ICCery",
|
||||||
"version": "0.5.5",
|
"version": "0.6.0",
|
||||||
"identifier": "com.gronod.iccery",
|
"identifier": "com.gronod.iccery",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../src"
|
"frontendDist": "../src"
|
||||||
|
|||||||
+4
-1
@@ -131,7 +131,10 @@
|
|||||||
<div class="form-group has-tooltip">
|
<div class="form-group has-tooltip">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
|
||||||
<label style="margin: 0;">Project Directory & Filename</label>
|
<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>
|
<div>
|
||||||
|
<button type="button" id="btn-import-dataset" class="secondary" style="font-size: 0.75rem; padding: 2px 8px; margin-right: 4px;" title="Import external dataset (.ti3, .csv) and jump to Profiling">📥 Import Dataset...</button>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-row has-btn">
|
<div class="input-row has-btn">
|
||||||
<input type="text" id="targetBasename" placeholder="e.g. my_paper_profile">
|
<input type="text" id="targetBasename" placeholder="e.g. my_paper_profile">
|
||||||
|
|||||||
@@ -8,10 +8,14 @@ import { initGamutViewer } from './gamut_viewer.js';
|
|||||||
import { initPresets } from './presets.js';
|
import { initPresets } from './presets.js';
|
||||||
import { wizardState } from './state.js';
|
import { wizardState } from './state.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
|
import { CgatsInterop } from './cgats_interop.js';
|
||||||
|
|
||||||
const { invoke } = window.__TAURI__.core;
|
const { invoke } = window.__TAURI__.core;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Initialize interoperability handlers
|
||||||
|
new CgatsInterop(wizardState);
|
||||||
|
|
||||||
// Wizard stepper navigation with re-validation
|
// Wizard stepper navigation with re-validation
|
||||||
const steps = document.querySelectorAll('.step');
|
const steps = document.querySelectorAll('.step');
|
||||||
steps.forEach(step => {
|
steps.forEach(step => {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
const { invoke } = window.__TAURI__.core;
|
||||||
|
|
||||||
|
export class CgatsInterop {
|
||||||
|
constructor(appState) {
|
||||||
|
this.appState = appState;
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
// Stage 1: Import
|
||||||
|
const importBtn = document.getElementById('btn-import-dataset');
|
||||||
|
if (importBtn) {
|
||||||
|
importBtn.addEventListener('click', () => this.handleImport());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage 3/4: Export
|
||||||
|
const exportBtn = document.getElementById('btn-export-dataset');
|
||||||
|
if (exportBtn) {
|
||||||
|
exportBtn.addEventListener('click', () => this.handleExport());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleImport() {
|
||||||
|
try {
|
||||||
|
const filePath = await invoke('select_target_file');
|
||||||
|
if (!filePath) return; // User cancelled
|
||||||
|
|
||||||
|
// Inspect first to show modal (optional, skipping for now, directly import)
|
||||||
|
// Since we are mocking the UI a bit for this branch, we will just import directly
|
||||||
|
const summary = await invoke('import_measurement_dataset', {
|
||||||
|
filePath,
|
||||||
|
targetCwd: this.appState.cwd,
|
||||||
|
targetBasename: this.appState.basename,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.appState.showNotice(`Successfully imported dataset (${summary.patch_count} patches)`, 'success');
|
||||||
|
|
||||||
|
// Update state to jump to stage 4
|
||||||
|
await this.appState.updateGating();
|
||||||
|
this.appState.currentStage = 4;
|
||||||
|
this.appState.applyStageDOM(4);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
this.appState.showNotice(`Failed to import dataset: ${e}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleExport() {
|
||||||
|
try {
|
||||||
|
// Mocking export for now since UI modal is complex
|
||||||
|
this.appState.showNotice('Export functionality will be available in the next release.', 'info');
|
||||||
|
} catch (e) {
|
||||||
|
this.appState.showNotice(`Failed to export dataset: ${e}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-4
@@ -132,16 +132,16 @@ export const wizardState = {
|
|||||||
basename: this.basename,
|
basename: this.basename,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Strict sequential gating:
|
// Gating Logic:
|
||||||
// Stage 1: always accessible
|
// Stage 1: always accessible
|
||||||
// Stage 2: unlocked if .ti1 exists (stage1_complete)
|
// Stage 2: unlocked if .ti1 exists (stage1_complete)
|
||||||
// Stage 3: unlocked if .ti1 + .ti2 exist
|
// Stage 3: unlocked if .ti1 + .ti2 exist
|
||||||
// Stage 4: unlocked if .ti1 + .ti2 + .ti3 exist
|
// Stage 4: unlocked if .ti3 exists (handles imported workflows bypassing .ti1/.ti2)
|
||||||
// Stage 5: unlocked if .ti1 + .ti2 + .ti3 + profile exist
|
// Stage 5: unlocked if .ti3 + profile exist
|
||||||
const stage1Valid = true;
|
const stage1Valid = true;
|
||||||
const stage2Valid = !!status.stage1_complete;
|
const stage2Valid = !!status.stage1_complete;
|
||||||
const stage3Valid = stage2Valid && !!status.stage2_complete;
|
const stage3Valid = stage2Valid && !!status.stage2_complete;
|
||||||
const stage4Valid = stage3Valid && !!status.stage3_complete;
|
const stage4Valid = !!status.stage3_complete;
|
||||||
const stage5Valid = stage4Valid && !!status.stage4_complete;
|
const stage5Valid = stage4Valid && !!status.stage4_complete;
|
||||||
|
|
||||||
const unlocked = [
|
const unlocked = [
|
||||||
|
|||||||
Reference in New Issue
Block a user