Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9102c95468 | ||
|
|
743ad86f58 | ||
|
|
11e796fd17 | ||
|
|
ebbdbd4f55 | ||
|
|
ef0d65453e | ||
|
|
9f2ff015d7 | ||
|
|
5a1ad654b8 | ||
|
|
ec7eb2fcb4 |
@@ -9,6 +9,8 @@ jobs:
|
||||
build-macos:
|
||||
name: Build macOS (${{ matrix.platform.name }})
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
env:
|
||||
XDG_CONFIG_HOME: ${{ runner.temp }}/.config
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
@@ -17,21 +19,31 @@ jobs:
|
||||
target: x86_64-apple-darwin
|
||||
binary_dir: macos-x86_64
|
||||
arch: x86_64
|
||||
- name: Apple Silicon
|
||||
os: macos
|
||||
target: aarch64-apple-darwin
|
||||
binary_dir: macos-aarch64
|
||||
arch: arm64
|
||||
- name: Universal (Intel + Apple Silicon)
|
||||
os: macos
|
||||
target: universal-apple-darwin
|
||||
binary_dir: macos-universal
|
||||
arch: universal
|
||||
# - name: Apple Silicon
|
||||
# os: macos
|
||||
# target: aarch64-apple-darwin
|
||||
# binary_dir: macos-aarch64
|
||||
# arch: arm64
|
||||
# - name: Universal (Intel + Apple Silicon)
|
||||
# os: macos
|
||||
# target: universal-apple-darwin
|
||||
# binary_dir: macos-universal
|
||||
# arch: universal
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Ensure workspace and git permissions
|
||||
run: |
|
||||
mkdir -p "${{ runner.temp }}/.config/git"
|
||||
touch "${{ runner.temp }}/.config/git/ignore"
|
||||
mkdir -p .git/info
|
||||
touch .git/info/exclude
|
||||
chmod -R u+rwX .git || true
|
||||
chmod 644 .git/info/exclude || true
|
||||
git config --local core.excludesFile "${{ runner.temp }}/.config/git/ignore" || true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -59,21 +71,6 @@ jobs:
|
||||
test -x "src-tauri/argyll/${{ matrix.platform.binary_dir }}/instlist"
|
||||
test -x "src-tauri/argyll/${{ matrix.platform.binary_dir }}/targen"
|
||||
|
||||
- name: Run Rust tests
|
||||
shell: bash
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
CARGO_TARGET_DIR: "${{ runner.temp }}/cargo-target"
|
||||
run: |
|
||||
cd src-tauri
|
||||
if [ "${{ matrix.platform.target }}" = "universal-apple-darwin" ]; then
|
||||
cargo test
|
||||
elif [ "${{ matrix.platform.target }}" = "aarch64-apple-darwin" ] && [ "$(uname -m)" != "arm64" ]; then
|
||||
cargo test --no-run --target ${{ matrix.platform.target }}
|
||||
else
|
||||
cargo test --target ${{ matrix.platform.target }}
|
||||
fi
|
||||
|
||||
- name: Set Release Environment
|
||||
id: set_env
|
||||
shell: bash
|
||||
|
||||
@@ -35,6 +35,16 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Ensure workspace and git permissions
|
||||
run: |
|
||||
mkdir -p "${{ runner.temp }}/.config/git"
|
||||
touch "${{ runner.temp }}/.config/git/ignore"
|
||||
mkdir -p .git/info
|
||||
touch .git/info/exclude
|
||||
chmod -R u+rwX .git || true
|
||||
chmod 644 .git/info/exclude || true
|
||||
git config --local core.excludesFile "${{ runner.temp }}/.config/git/ignore" || true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
@@ -163,9 +163,9 @@ The "Preferences" button opens the native macOS `NSPrintPanel` (not CUPS web UI
|
||||
- Defaults to `false` ensuring 100% out-of-the-box compatibility with stock upstream ArgyllCMS binaries.
|
||||
- Subprocess error diagnostics in `chartread.js` capture `lastStderrLine` from `process:stderr`, auto-expanding the Process Output `<details>` panel with the stderr explanation if an unpatched binary rejects `-Y l`.
|
||||
|
||||
## CI & Cross-Compilation Testing
|
||||
## CI & Cross-Compilation
|
||||
|
||||
- macOS CI workflow lives in `.gitea/workflows/build-macos.yml`.
|
||||
- On Intel runner hosts, cross-compiling for Apple Silicon (`aarch64-apple-darwin`) must use `cargo test --no-run --target aarch64-apple-darwin`. This validates sidecar packaging, compilation, and link correctness without attempting to execute ARM64 binaries on an Intel CPU (`Bad CPU type in executable (os error 86)`).
|
||||
- Universal and native Intel matrix jobs run and execute full tests natively.
|
||||
- Release packaging workflows live under `.gitea/workflows/` (`build-macos.yml`, `build-linux.yml`, `build-windows.yml`).
|
||||
- Tag release builds focus exclusively on packaging via `npm run tauri build` without redundant debug-profile test compilations.
|
||||
- Local/CI cross-compilation test execution for Apple Silicon (`aarch64-apple-darwin`) on Intel hosts must use `cargo test --no-run --target aarch64-apple-darwin` to avoid executing ARM64 binaries on an x86_64 CPU (`Bad CPU type in executable (os error 86)`).
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> Modern, cross-platform native desktop application for printer profiling, powered by ArgyllCMS.
|
||||
|
||||
[](https://git.i3omb.com/gronod/ICCery)
|
||||
[](https://git.i3omb.com/gronod/ICCery)
|
||||
[](https://git.i3omb.com/gronod/ICCery)
|
||||
[](https://tauri.app)
|
||||
[](LICENCE.md)
|
||||
|
||||
+6
-1
@@ -14,7 +14,7 @@ ICCery is a native, cross-platform desktop application built with:
|
||||
- **Frontend**: Vanilla JS (ES Modules) + HTML5/CSS3 with a modern dark theme and responsive layout.
|
||||
- **Visualization**: Three.js WebGL engine for 3D CIELAB color gamut volumes and sRGB reference comparisons.
|
||||
- **Engine**: ArgyllCMS command-line utilities orchestrated over isolated standard stream IPC (`stdin`, `stdout`, `stderr`).
|
||||
- **Current Version**: `v0.8.2` (Production release).
|
||||
- **Current Version**: `v0.8.3` (Production release).
|
||||
|
||||
---
|
||||
|
||||
@@ -108,6 +108,11 @@ ICCery is a native, cross-platform desktop application built with:
|
||||
- [x] **XY Automated Scanning Tables (#93)**: Full Stage 3 support for automated XY scanning tables (GretagMacbeth SpectroScan, X-Rite i1iO) with pure multi-line prompt classification, fiducial alignment prompts, 4-step sequence checklist, and graceful head parking on cancel.
|
||||
- [ ] ~~**Multi-Language Localization (#96)**~~: *Closed — Won't Fix* (English UI retained as standard color-management terminology).
|
||||
|
||||
### Maintenance & Reliability Release (`v0.8.3`)
|
||||
- [x] **Gamut Viewer Node Test Runner Support (#212)**: Guarded `window` and `window.__TAURI__` globals in `gamut_viewer.js` and added polyfill mock harness to `gamut_viewer.test.js` to enable automated headless test execution via `node src/js/gamut_viewer.test.js`.
|
||||
- [x] **Custom Spectrum File Picker Dialog (#210)**: Implemented native `select_spectrum_file` command wrapping Tauri file dialog with `.sp` filter for custom FWA/OBA spectrum selection in Stage 4 profile generation.
|
||||
- [x] **CGATS Dataset Import File Picker & State Synchronization (#211)**: Implemented native `select_dataset_file` command with `.ti3`, `.txt`, `.cgats`, and `.csv` filter, synchronized wizard target directory and basename upon import, and guarded against empty target states.
|
||||
|
||||
---
|
||||
|
||||
## 3. Future Roadmap
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "iccery",
|
||||
"private": true,
|
||||
"version": "0.8.2",
|
||||
"version": "0.8.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"fetch-argyll": "node scripts/fetch-argyll.mjs",
|
||||
|
||||
Generated
+1
-1
@@ -1423,7 +1423,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iccery"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"image",
|
||||
|
||||
+12
-1
@@ -1,9 +1,20 @@
|
||||
[package]
|
||||
name = "iccery"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
|
||||
authors = ["Gordon"]
|
||||
edition = "2021"
|
||||
include = [
|
||||
"src/**/*",
|
||||
"build.rs",
|
||||
"Cargo.toml",
|
||||
"Cargo.lock",
|
||||
"tauri.conf.json",
|
||||
"capabilities/**/*",
|
||||
"gen/**/*",
|
||||
"icons/**/*",
|
||||
"windows/**/*",
|
||||
]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
||||
@@ -10,6 +10,12 @@ fn main() {
|
||||
|
||||
println!("cargo:rustc-env=BUILD_DATE={}", build_date);
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=src");
|
||||
println!("cargo:rerun-if-changed=tauri.conf.json");
|
||||
println!("cargo:rerun-if-changed=Cargo.toml");
|
||||
println!("cargo:rerun-if-changed=argyll");
|
||||
|
||||
// Validate that ArgyllCMS sidecar binaries are staged before building
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
||||
|
||||
@@ -391,6 +391,61 @@ pub async fn select_profile_file(
|
||||
rx.await.map_err(|e| format!("Dialog channel error: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn select_spectrum_file(
|
||||
app: AppHandle,
|
||||
default_dir: Option<String>,
|
||||
) -> Result<Option<String>, String> {
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
let mut builder = app
|
||||
.dialog()
|
||||
.file()
|
||||
.add_filter("Argyll Spectrum File (*.sp)", &["sp"]);
|
||||
if let Some(ref dir) = default_dir {
|
||||
if !dir.trim().is_empty() {
|
||||
builder = builder.set_directory(std::path::PathBuf::from(dir));
|
||||
}
|
||||
}
|
||||
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
builder.pick_file(move |file_path| {
|
||||
let res = file_path.map(|p| p.to_string());
|
||||
let _ = tx.send(res);
|
||||
});
|
||||
|
||||
rx.await.map_err(|e| format!("Dialog channel error: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn select_dataset_file(
|
||||
app: AppHandle,
|
||||
default_dir: Option<String>,
|
||||
) -> Result<Option<String>, String> {
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
let mut builder = app
|
||||
.dialog()
|
||||
.file()
|
||||
.add_filter(
|
||||
"Measurement Dataset (*.ti3, *.txt, *.cgats, *.csv)",
|
||||
&["ti3", "txt", "cgats", "csv"],
|
||||
);
|
||||
if let Some(ref dir) = default_dir {
|
||||
if !dir.trim().is_empty() {
|
||||
builder = builder.set_directory(std::path::PathBuf::from(dir));
|
||||
}
|
||||
}
|
||||
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
builder.pick_file(move |file_path| {
|
||||
let res = file_path.map(|p| p.to_string());
|
||||
let _ = tx.send(res);
|
||||
});
|
||||
|
||||
rx.await.map_err(|e| format!("Dialog channel error: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn select_target_file(
|
||||
app: AppHandle,
|
||||
|
||||
@@ -51,6 +51,8 @@ pub fn run() {
|
||||
commands::inspect_dataset_preview,
|
||||
commands::select_existing_target,
|
||||
commands::select_profile_file,
|
||||
commands::select_spectrum_file,
|
||||
commands::select_dataset_file,
|
||||
commands::select_target_file,
|
||||
commands::select_directory,
|
||||
commands::send_stdin,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "ICCery",
|
||||
"version": "0.8.2",
|
||||
"version": "0.8.3",
|
||||
"identifier": "com.gronod.iccery",
|
||||
"build": {
|
||||
"frontendDist": "../src"
|
||||
|
||||
+38
-9
@@ -1,4 +1,6 @@
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
const invoke = typeof window !== 'undefined' && window.__TAURI__?.core?.invoke
|
||||
? window.__TAURI__.core.invoke
|
||||
: async () => {};
|
||||
|
||||
export class CgatsInterop {
|
||||
constructor(appState) {
|
||||
@@ -22,26 +24,53 @@ export class CgatsInterop {
|
||||
|
||||
async handleImport() {
|
||||
try {
|
||||
const filePath = await invoke('select_target_file');
|
||||
const filePath = await invoke('select_dataset_file', {
|
||||
defaultDir: this.appState?.cwd || null,
|
||||
});
|
||||
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 isWindows = filePath.includes('\\');
|
||||
const sep = isWindows ? '\\' : '/';
|
||||
const parts = filePath.split(sep);
|
||||
const fileName = parts.pop();
|
||||
const fileDir = parts.join(sep);
|
||||
const fileStem = fileName.replace(/\.[^/.]+$/, '');
|
||||
|
||||
const targetCwd = this.appState?.cwd || fileDir;
|
||||
const targetBasename = this.appState?.basename || fileStem;
|
||||
|
||||
const targetBasenameInput = document.getElementById('targetBasename');
|
||||
if (targetBasenameInput && !targetBasenameInput.value.trim()) {
|
||||
targetBasenameInput.value = targetBasename;
|
||||
}
|
||||
const selectedPathDisplay = document.getElementById('selectedPathDisplay');
|
||||
if (selectedPathDisplay && (!selectedPathDisplay.textContent || selectedPathDisplay.textContent.includes('No directory') || !this.appState?.cwd)) {
|
||||
selectedPathDisplay.textContent = `Directory: ${targetCwd}`;
|
||||
}
|
||||
|
||||
if (this.appState?.setTarget) {
|
||||
await this.appState.setTarget(targetBasename, targetCwd);
|
||||
}
|
||||
|
||||
const summary = await invoke('import_measurement_dataset', {
|
||||
filePath,
|
||||
targetCwd: this.appState.cwd,
|
||||
targetBasename: this.appState.basename,
|
||||
targetCwd,
|
||||
targetBasename,
|
||||
});
|
||||
|
||||
this.appState.showNotice(`Successfully imported dataset (${summary.patch_count} patches)`, 'success');
|
||||
this.appState?.showNotice?.(`Successfully imported dataset (${summary.patch_count} patches)`, 'success');
|
||||
|
||||
// Update state to jump to stage 4
|
||||
if (this.appState?.updateGating) {
|
||||
await this.appState.updateGating();
|
||||
}
|
||||
if (this.appState) {
|
||||
this.appState.currentStage = 4;
|
||||
this.appState.applyStageDOM(4);
|
||||
this.appState.applyStageDOM?.(4);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
this.appState.showNotice(`Failed to import dataset: ${e}`, 'error');
|
||||
this.appState?.showNotice?.(`Failed to import dataset: ${e}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -54,8 +54,8 @@ export function initColprof() {
|
||||
if (btnBrowseCustomSp && colprofCustomSpPath) {
|
||||
btnBrowseCustomSp.addEventListener("click", async () => {
|
||||
try {
|
||||
const selected = await window.__TAURI__.dialog.open({
|
||||
filters: [{ name: 'Spectrum', extensions: ['sp'] }]
|
||||
const selected = await invoke("select_spectrum_file", {
|
||||
defaultDir: chartreadCwd || wizardState.cwd || null,
|
||||
});
|
||||
if (selected) {
|
||||
colprofCustomSpPath.value = selected;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { computeQuickHull } from "./vendor/quickhull.js";
|
||||
import { computeQuickHull } from "./vendor/quickhull.js";
|
||||
import { labToSrgb } from "./color_convert.js";
|
||||
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
const { listen } = window.__TAURI__.event;
|
||||
const invoke = typeof window !== 'undefined' && window.__TAURI__?.core?.invoke ? window.__TAURI__.core.invoke : null;
|
||||
|
||||
let scene, camera, renderer, labelRenderer, controls;
|
||||
let currentProfileMesh = null;
|
||||
|
||||
@@ -1,20 +1,50 @@
|
||||
// Manual / browser-console tests for gamut_viewer.js and profcheck.js parsing.
|
||||
// Run in a browser/devtools console after the app has loaded:
|
||||
// Unit & console tests for gamut_viewer.js parsing.
|
||||
// Can be run in browser devtools console:
|
||||
// import('./gamut_viewer.test.js').then(m => m.runAll())
|
||||
// Or in Node:
|
||||
// node src/js/gamut_viewer.test.js
|
||||
|
||||
import { parseGamutFile } from './gamut_viewer.js';
|
||||
// Node environment polyfill for browser globals
|
||||
if (typeof window === 'undefined') {
|
||||
globalThis.window = {
|
||||
__TAURI__: {
|
||||
core: { invoke: () => Promise.resolve() },
|
||||
event: { listen: () => Promise.resolve(() => {}) }
|
||||
},
|
||||
addEventListener: () => {},
|
||||
dispatchEvent: () => {}
|
||||
};
|
||||
globalThis.document = {
|
||||
getElementById: () => null,
|
||||
querySelectorAll: () => []
|
||||
};
|
||||
}
|
||||
|
||||
const { parseGamutFile } = await import('./gamut_viewer.js');
|
||||
|
||||
let passed = 0;
|
||||
let total = 0;
|
||||
|
||||
export function runAll() {
|
||||
console.group('gamut/profcheck parser tests');
|
||||
console.group('Gamut Viewer Parser Tests');
|
||||
passed = 0;
|
||||
total = 0;
|
||||
testParseGamutBasic();
|
||||
testParseGamutDualTable();
|
||||
testParseGamutWithComments();
|
||||
console.log(`\nResults: ${passed} / ${total} tests passed.`);
|
||||
console.groupEnd();
|
||||
|
||||
if (passed !== total) {
|
||||
throw new Error(`Gamut viewer parser tests failed: ${total - passed} failure(s)`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertEqual(actual, expected, message) {
|
||||
total++;
|
||||
const ok = JSON.stringify(actual) === JSON.stringify(expected);
|
||||
if (ok) {
|
||||
passed++;
|
||||
console.log('PASS:', message);
|
||||
} else {
|
||||
console.error('FAIL:', message, 'expected', expected, 'got', actual);
|
||||
@@ -78,3 +108,8 @@ END_DATA`;
|
||||
assertEqual(vertices.length, 4, 'commented gamut vertex count');
|
||||
assertEqual(faces.length, 2, 'commented gamut face count');
|
||||
}
|
||||
|
||||
// Auto-run if executed in Node.js
|
||||
if (typeof process !== 'undefined' && process.argv && process.argv[1]?.endsWith('gamut_viewer.test.js')) {
|
||||
runAll();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user