feat: Stage 4 & 5 - Profile Calculation (colprof) & Verification (profcheck) #14
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Mock script for colprof
|
||||||
|
# Simulates colprof execution and outputs progress log
|
||||||
|
|
||||||
|
basename="$1"
|
||||||
|
# Find last argument if -D or other flags are used
|
||||||
|
for arg in "$@"; do
|
||||||
|
basename="$arg"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "colprof: Starting profile calculation for $basename"
|
||||||
|
sleep 1
|
||||||
|
echo "Gamut mapping calculation..."
|
||||||
|
sleep 1
|
||||||
|
echo "Fitting cLUT grid points..."
|
||||||
|
sleep 1
|
||||||
|
echo "Writing ICC profile $basename.icc..."
|
||||||
|
touch "$basename.icc"
|
||||||
|
echo "Done."
|
||||||
|
exit 0
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Mock script for profcheck
|
||||||
|
# Simulates profcheck verification output
|
||||||
|
|
||||||
|
echo "profcheck: Checking profile accuracy..."
|
||||||
|
sleep 1
|
||||||
|
cat << 'EOF'
|
||||||
|
{"event": "profcheck_complete", "avg_de": 0.85, "max_de": 2.41, "rms_de": 1.02}
|
||||||
|
EOF
|
||||||
|
echo "Summary:"
|
||||||
|
echo " avg. dE = 0.85"
|
||||||
|
echo " max. dE = 2.41"
|
||||||
|
echo " rms. dE = 1.02"
|
||||||
|
exit 0
|
||||||
@@ -204,6 +204,94 @@ pub async fn run_chartread(
|
|||||||
state.spawn(app, id, binary, args, cwd).await
|
state.spawn(app, id, binary, args, cwd).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct ColprofConfig {
|
||||||
|
pub quality: String,
|
||||||
|
pub algorithm: String,
|
||||||
|
pub description: String,
|
||||||
|
pub copyright: Option<String>,
|
||||||
|
pub basename: String,
|
||||||
|
pub cwd: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_colprof_args(config: &ColprofConfig) -> Vec<String> {
|
||||||
|
let mut args = vec![
|
||||||
|
"-v".to_string(),
|
||||||
|
"-u".to_string(),
|
||||||
|
"-q".to_string(),
|
||||||
|
config.quality.clone(),
|
||||||
|
"-a".to_string(),
|
||||||
|
config.algorithm.clone(),
|
||||||
|
"-D".to_string(),
|
||||||
|
config.description.clone(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if let Some(copyright) = &config.copyright {
|
||||||
|
if !copyright.trim().is_empty() {
|
||||||
|
args.push("-C".to_string());
|
||||||
|
args.push(copyright.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(config.basename.clone());
|
||||||
|
args
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn run_colprof(
|
||||||
|
app: AppHandle,
|
||||||
|
state: State<'_, ProcessManager>,
|
||||||
|
config: ColprofConfig,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let binary = resolve_binary(app.clone(), "colprof".to_string()).await?;
|
||||||
|
let args = build_colprof_args(&config);
|
||||||
|
let id = format!("colprof_{}", config.basename);
|
||||||
|
|
||||||
|
let cwd = if config.cwd.trim().is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(config.cwd.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
state.spawn(app, id, binary, args, cwd).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct ProfcheckConfig {
|
||||||
|
pub ti3_path: String,
|
||||||
|
pub icc_path: String,
|
||||||
|
pub cwd: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_profcheck_args(config: &ProfcheckConfig) -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"-v".to_string(),
|
||||||
|
"-k".to_string(),
|
||||||
|
"-u".to_string(),
|
||||||
|
config.ti3_path.clone(),
|
||||||
|
config.icc_path.clone(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn run_profcheck(
|
||||||
|
app: AppHandle,
|
||||||
|
state: State<'_, ProcessManager>,
|
||||||
|
config: ProfcheckConfig,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let binary = resolve_binary(app.clone(), "profcheck".to_string()).await?;
|
||||||
|
let args = build_profcheck_args(&config);
|
||||||
|
let id = format!("profcheck_{}", config.ti3_path);
|
||||||
|
|
||||||
|
let cwd = if config.cwd.trim().is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(config.cwd.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
state.spawn(app, id, binary, args, cwd).await
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -287,4 +375,32 @@ mod tests {
|
|||||||
let args = build_chartread_args(&config);
|
let args = build_chartread_args(&config);
|
||||||
assert_eq!(args, vec!["-v", "-u", "my_profile"]);
|
assert_eq!(args, vec!["-v", "-u", "my_profile"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_colprof_args() {
|
||||||
|
let config = ColprofConfig {
|
||||||
|
quality: "h".to_string(),
|
||||||
|
algorithm: "l".to_string(),
|
||||||
|
description: "My Profile".to_string(),
|
||||||
|
copyright: Some("2026 ACME".to_string()),
|
||||||
|
basename: "my_profile".to_string(),
|
||||||
|
cwd: "/home/user".to_string(),
|
||||||
|
};
|
||||||
|
let args = build_colprof_args(&config);
|
||||||
|
assert_eq!(
|
||||||
|
args,
|
||||||
|
vec!["-v", "-u", "-q", "h", "-a", "l", "-D", "My Profile", "-C", "2026 ACME", "my_profile"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_profcheck_args() {
|
||||||
|
let config = ProfcheckConfig {
|
||||||
|
ti3_path: "my_profile.ti3".to_string(),
|
||||||
|
icc_path: "my_profile.icc".to_string(),
|
||||||
|
cwd: "/home/user".to_string(),
|
||||||
|
};
|
||||||
|
let args = build_profcheck_args(&config);
|
||||||
|
assert_eq!(args, vec!["-v", "-k", "-u", "my_profile.ti3", "my_profile.icc"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ pub fn run() {
|
|||||||
commands::run_printtarg,
|
commands::run_printtarg,
|
||||||
commands::read_file_base64,
|
commands::read_file_base64,
|
||||||
commands::run_chartread,
|
commands::run_chartread,
|
||||||
|
commands::run_colprof,
|
||||||
|
commands::run_profcheck,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
+91
-5
@@ -218,15 +218,101 @@
|
|||||||
<!-- Stage 4: colprof -->
|
<!-- Stage 4: colprof -->
|
||||||
<section id="stage-4" class="stage hidden">
|
<section id="stage-4" class="stage hidden">
|
||||||
<h2>Create Profile</h2>
|
<h2>Create Profile</h2>
|
||||||
<p>Calculate the ICC profile.</p>
|
<p>Calculate the ICC profile from your target measurement data.</p>
|
||||||
<button class="primary" id="btnProfile">Create Profile (.icc)</button>
|
|
||||||
|
<div class="form-container">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Profile Quality</label>
|
||||||
|
<select id="colprofQuality">
|
||||||
|
<option value="l">Low (Fast draft)</option>
|
||||||
|
<option value="m" selected>Medium (Standard)</option>
|
||||||
|
<option value="h">High (Recommended)</option>
|
||||||
|
<option value="u">Ultra (Maximum precision - slow)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Profile Description</label>
|
||||||
|
<input type="text" id="colprofDescription" placeholder="e.g. Epson P600 Lustre Photo">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Copyright / Creator (Optional)</label>
|
||||||
|
<input type="text" id="colprofCopyright" placeholder="e.g. Copyright (c) 2026 My Studio">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Algorithm Type</label>
|
||||||
|
<select id="colprofAlgorithm">
|
||||||
|
<option value="l" selected>Lab cLUT (Standard Lookup Table)</option>
|
||||||
|
<option value="x">XYZ cLUT</option>
|
||||||
|
<option value="X">Display XYZ cLUT + Matrix</option>
|
||||||
|
<option value="m">Matrix Only (Simple)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="primary" id="btnCreateProfile">Calculate Profile (.icc)</button>
|
||||||
|
|
||||||
|
<!-- Spinner and Progress Stage -->
|
||||||
|
<div class="spinner-container hidden" id="colprofSpinnerContainer">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<div class="stage-label" id="colprofStageLabel">Calculating ICC Profile...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Success Card -->
|
||||||
|
<div class="success-card hidden" id="colprofSuccessCard">
|
||||||
|
<h3>🎉 Profile Calculation Complete</h3>
|
||||||
|
<p id="colprofSuccessInfo"></p>
|
||||||
|
<button class="primary" id="btnGoToVerify">Proceed to Verification →</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Process Log -->
|
||||||
|
<div class="log-container hidden" id="colprofLogContainer">
|
||||||
|
<h4>Process Output</h4>
|
||||||
|
<pre id="colprofLog"></pre>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Stage 5: profcheck -->
|
<!-- Stage 5: profcheck -->
|
||||||
<section id="stage-5" class="stage hidden">
|
<section id="stage-5" class="stage hidden">
|
||||||
<h2>Verify</h2>
|
<h2>Verify Profile</h2>
|
||||||
<p>Analyze the generated profile quality.</p>
|
<p>Check the numerical accuracy of your profile against the original measurement data.</p>
|
||||||
<button class="primary" id="btnVerify">Verify Profile</button>
|
|
||||||
|
<button class="primary" id="btnVerify">Run Profile Verification</button>
|
||||||
|
|
||||||
|
<!-- Report Card -->
|
||||||
|
<div class="report-card hidden" id="profcheckReportCard">
|
||||||
|
<div class="report-header">
|
||||||
|
<h3>Profile Quality Report</h3>
|
||||||
|
<div id="profcheckBadge" class="report-badge">GOOD</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="report-metrics">
|
||||||
|
<div class="metric-box">
|
||||||
|
<span class="metric-value" id="profcheckAvgDe">0.00</span>
|
||||||
|
<span class="metric-label">Average ΔE₀₀</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-box">
|
||||||
|
<span class="metric-value" id="profcheckMaxDe">0.00</span>
|
||||||
|
<span class="metric-label">Peak ΔE₀₀</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-box">
|
||||||
|
<span class="metric-value" id="profcheckRmsDe">0.00</span>
|
||||||
|
<span class="metric-label">RMS ΔE₀₀</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="report-footnote">
|
||||||
|
Professional printer profiles typically achieve average ΔE₀₀ < 1.0, and acceptable profiles < 2.0.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Process Log -->
|
||||||
|
<div class="log-container hidden" id="profcheckLogContainer">
|
||||||
|
<h4>Verification Output</h4>
|
||||||
|
<pre id="profcheckLog"></pre>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { initTargen } from './targen.js';
|
import { initTargen } from './targen.js';
|
||||||
import { initPrinttarg } from './printtarg.js';
|
import { initPrinttarg } from './printtarg.js';
|
||||||
import { initChartread } from './chartread.js';
|
import { initChartread } from './chartread.js';
|
||||||
|
import { initColprof } from './colprof.js';
|
||||||
|
import { initProfcheck } from './profcheck.js';
|
||||||
|
|
||||||
const { invoke } = window.__TAURI__.core;
|
const { invoke } = window.__TAURI__.core;
|
||||||
|
|
||||||
@@ -35,4 +37,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Initialize Stage 3
|
// Initialize Stage 3
|
||||||
initChartread();
|
initChartread();
|
||||||
|
|
||||||
|
// Initialize Stage 4
|
||||||
|
initColprof();
|
||||||
|
|
||||||
|
// Initialize Stage 5
|
||||||
|
initProfcheck();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const { invoke } = window.__TAURI__.core;
|
const { invoke } = window.__TAURI__.core;
|
||||||
const { listen } = window.__TAURI__.event;
|
const { listen } = window.__TAURI__.event;
|
||||||
import { startSwatchListener, stopSwatchListener } from './swatch_grid.js';
|
import { startSwatchListener, stopSwatchListener } from './swatch_grid.js';
|
||||||
|
import { setStage3Result } from './colprof.js';
|
||||||
|
|
||||||
// Module-level state: set by Stage 2 when it completes
|
// Module-level state: set by Stage 2 when it completes
|
||||||
let stage2Basename = "";
|
let stage2Basename = "";
|
||||||
@@ -150,6 +151,7 @@ export function initChartread() {
|
|||||||
setState(STATE.FINISHED);
|
setState(STATE.FINISHED);
|
||||||
setPrompt("✅ Measurement complete! .ti3 file has been saved.");
|
setPrompt("✅ Measurement complete! .ti3 file has been saved.");
|
||||||
logPre.textContent += "\n[SUCCESS] chartread completed. .ti3 file written.\n";
|
logPre.textContent += "\n[SUCCESS] chartread completed. .ti3 file written.\n";
|
||||||
|
setStage3Result(stage2Basename, stage2Cwd);
|
||||||
advanceToStage4();
|
advanceToStage4();
|
||||||
} else {
|
} else {
|
||||||
setState(STATE.FINISHED);
|
setState(STATE.FINISHED);
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
const { invoke } = window.__TAURI__.core;
|
||||||
|
const { listen } = window.__TAURI__.event;
|
||||||
|
import { setStage4Result } from './profcheck.js';
|
||||||
|
|
||||||
|
let chartreadBasename = "";
|
||||||
|
let chartreadCwd = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by chartread.js after Stage 3 completes.
|
||||||
|
*/
|
||||||
|
export function setStage3Result(basename, cwd) {
|
||||||
|
chartreadBasename = basename;
|
||||||
|
chartreadCwd = cwd;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initColprof() {
|
||||||
|
const qualitySelect = document.getElementById("colprofQuality");
|
||||||
|
const algorithmSelect = document.getElementById("colprofAlgorithm");
|
||||||
|
const descInput = document.getElementById("colprofDescription");
|
||||||
|
const copyrightInput = document.getElementById("colprofCopyright");
|
||||||
|
const btnCreateProfile = document.getElementById("btnCreateProfile");
|
||||||
|
const spinnerContainer = document.getElementById("colprofSpinnerContainer");
|
||||||
|
const stageLabel = document.getElementById("colprofStageLabel");
|
||||||
|
const logContainer = document.getElementById("colprofLogContainer");
|
||||||
|
const logPre = document.getElementById("colprofLog");
|
||||||
|
const successCard = document.getElementById("colprofSuccessCard");
|
||||||
|
const successInfo = document.getElementById("colprofSuccessInfo");
|
||||||
|
const btnGoToVerify = document.getElementById("btnGoToVerify");
|
||||||
|
|
||||||
|
if (!btnCreateProfile) return;
|
||||||
|
|
||||||
|
btnCreateProfile.addEventListener("click", async () => {
|
||||||
|
const basename = chartreadBasename || "test_target";
|
||||||
|
const cwd = chartreadCwd || "";
|
||||||
|
|
||||||
|
const description = descInput.value.trim() || basename;
|
||||||
|
|
||||||
|
logPre.textContent = "";
|
||||||
|
logContainer.classList.remove("hidden");
|
||||||
|
spinnerContainer.classList.remove("hidden");
|
||||||
|
successCard.classList.add("hidden");
|
||||||
|
btnCreateProfile.disabled = true;
|
||||||
|
stageLabel.textContent = "Initializing colprof...";
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
quality: qualitySelect.value,
|
||||||
|
algorithm: algorithmSelect.value,
|
||||||
|
description: description,
|
||||||
|
copyright: copyrightInput.value.trim() || null,
|
||||||
|
basename: basename,
|
||||||
|
cwd: cwd,
|
||||||
|
};
|
||||||
|
|
||||||
|
const processId = `colprof_${basename}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const unlistenStdout = await listen("process:stdout", (event) => {
|
||||||
|
if (event.payload.id === processId && event.payload.line) {
|
||||||
|
const line = event.payload.line;
|
||||||
|
logPre.textContent += line + "\n";
|
||||||
|
logPre.scrollTop = logPre.scrollHeight;
|
||||||
|
|
||||||
|
// Parse coarse progress stages from stdout
|
||||||
|
const lineLower = line.toLowerCase();
|
||||||
|
if (lineLower.includes("gamut mapping")) {
|
||||||
|
stageLabel.textContent = "Gamut mapping calculation in progress...";
|
||||||
|
} else if (lineLower.includes("fitting") || lineLower.includes("clut")) {
|
||||||
|
stageLabel.textContent = "Fitting cLUT grid points...";
|
||||||
|
} else if (lineLower.includes("writing") || lineLower.includes("icc profile")) {
|
||||||
|
stageLabel.textContent = "Writing ICC profile header & tags...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const unlistenStderr = await listen("process:stderr", (event) => {
|
||||||
|
if (event.payload.id === processId && event.payload.line) {
|
||||||
|
logPre.textContent += "ERR: " + event.payload.line + "\n";
|
||||||
|
logPre.scrollTop = logPre.scrollHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const unlistenExit = await listen("process:exit", (event) => {
|
||||||
|
if (event.payload.id === processId) {
|
||||||
|
unlistenStdout();
|
||||||
|
unlistenStderr();
|
||||||
|
unlistenExit();
|
||||||
|
spinnerContainer.classList.add("hidden");
|
||||||
|
btnCreateProfile.disabled = false;
|
||||||
|
|
||||||
|
if (event.payload.code === 0) {
|
||||||
|
logPre.textContent += "\n[SUCCESS] colprof completed. Profile generated.\n";
|
||||||
|
const iccFilename = `${basename}.icc`;
|
||||||
|
successInfo.textContent = `Profile: ${iccFilename} (${description})`;
|
||||||
|
successCard.classList.remove("hidden");
|
||||||
|
|
||||||
|
setStage4Result(basename, cwd);
|
||||||
|
} else {
|
||||||
|
logPre.textContent += `\n[ERROR] colprof exited with code ${event.payload.code}.\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await invoke("run_colprof", { config });
|
||||||
|
} catch (err) {
|
||||||
|
logPre.textContent += `\n[INVOKE ERROR] ${err}\n`;
|
||||||
|
spinnerContainer.classList.add("hidden");
|
||||||
|
btnCreateProfile.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (btnGoToVerify) {
|
||||||
|
btnGoToVerify.addEventListener("click", () => {
|
||||||
|
advanceToStage5();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function advanceToStage5() {
|
||||||
|
const steps = document.querySelectorAll('.step');
|
||||||
|
const stages = document.querySelectorAll('.stage');
|
||||||
|
|
||||||
|
steps.forEach(s => s.classList.remove('active'));
|
||||||
|
if (steps[4]) steps[4].classList.add('active');
|
||||||
|
|
||||||
|
stages.forEach(s => {
|
||||||
|
s.classList.remove('active');
|
||||||
|
s.classList.add('hidden');
|
||||||
|
});
|
||||||
|
if (stages[4]) {
|
||||||
|
stages[4].classList.remove('hidden');
|
||||||
|
stages[4].classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
const { invoke } = window.__TAURI__.core;
|
||||||
|
const { listen } = window.__TAURI__.event;
|
||||||
|
|
||||||
|
let profileBasename = "";
|
||||||
|
let profileCwd = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by colprof.js after Stage 4 completes.
|
||||||
|
*/
|
||||||
|
export function setStage4Result(basename, cwd) {
|
||||||
|
profileBasename = basename;
|
||||||
|
profileCwd = cwd;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initProfcheck() {
|
||||||
|
const btnVerify = document.getElementById("btnVerify");
|
||||||
|
const logContainer = document.getElementById("profcheckLogContainer");
|
||||||
|
const logPre = document.getElementById("profcheckLog");
|
||||||
|
const reportCard = document.getElementById("profcheckReportCard");
|
||||||
|
const avgDeEl = document.getElementById("profcheckAvgDe");
|
||||||
|
const maxDeEl = document.getElementById("profcheckMaxDe");
|
||||||
|
const rmsDeEl = document.getElementById("profcheckRmsDe");
|
||||||
|
const badgeEl = document.getElementById("profcheckBadge");
|
||||||
|
|
||||||
|
if (!btnVerify) return;
|
||||||
|
|
||||||
|
btnVerify.addEventListener("click", async () => {
|
||||||
|
const basename = profileBasename || "test_target";
|
||||||
|
const cwd = profileCwd || "";
|
||||||
|
|
||||||
|
const sep = cwd.includes('\\') ? '\\' : '/';
|
||||||
|
const ti3Path = cwd ? `${cwd}${sep}${basename}.ti3` : `${basename}.ti3`;
|
||||||
|
const iccPath = cwd ? `${cwd}${sep}${basename}.icc` : `${basename}.icc`;
|
||||||
|
|
||||||
|
logPre.textContent = "";
|
||||||
|
logContainer.classList.remove("hidden");
|
||||||
|
reportCard.classList.add("hidden");
|
||||||
|
btnVerify.disabled = true;
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
ti3_path: ti3Path,
|
||||||
|
icc_path: iccPath,
|
||||||
|
cwd: cwd,
|
||||||
|
};
|
||||||
|
|
||||||
|
const processId = `profcheck_${ti3Path}`;
|
||||||
|
let stdoutAccumulator = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const unlistenStdout = await listen("process:stdout", (event) => {
|
||||||
|
if (event.payload.id === processId && event.payload.line) {
|
||||||
|
stdoutAccumulator += event.payload.line + "\n";
|
||||||
|
logPre.textContent += event.payload.line + "\n";
|
||||||
|
logPre.scrollTop = logPre.scrollHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const unlistenStderr = await listen("process:stderr", (event) => {
|
||||||
|
if (event.payload.id === processId && event.payload.line) {
|
||||||
|
logPre.textContent += "ERR: " + event.payload.line + "\n";
|
||||||
|
logPre.scrollTop = logPre.scrollHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const unlistenExit = await listen("process:exit", (event) => {
|
||||||
|
if (event.payload.id === processId) {
|
||||||
|
unlistenStdout();
|
||||||
|
unlistenStderr();
|
||||||
|
unlistenExit();
|
||||||
|
btnVerify.disabled = false;
|
||||||
|
|
||||||
|
if (event.payload.code === 0) {
|
||||||
|
logPre.textContent += "\n[SUCCESS] profcheck verification finished.\n";
|
||||||
|
parseAndRenderReport(stdoutAccumulator);
|
||||||
|
} else {
|
||||||
|
logPre.textContent += `\n[ERROR] profcheck exited with code ${event.payload.code}.\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await invoke("run_profcheck", { config });
|
||||||
|
} catch (err) {
|
||||||
|
logPre.textContent += `\n[INVOKE ERROR] ${err}\n`;
|
||||||
|
btnVerify.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function parseAndRenderReport(stdout) {
|
||||||
|
reportCard.classList.remove("hidden");
|
||||||
|
|
||||||
|
let avgDe = 0.0;
|
||||||
|
let maxDe = 0.0;
|
||||||
|
let rmsDe = 0.0;
|
||||||
|
|
||||||
|
// Check if JSON output is present
|
||||||
|
const jsonMatch = stdout.match(/\{[\s\S]*"avg_de"[\s\S]*\}/);
|
||||||
|
if (jsonMatch) {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(jsonMatch[0]);
|
||||||
|
avgDe = json.avg_de || 0;
|
||||||
|
maxDe = json.max_de || json.peak_de || 0;
|
||||||
|
rmsDe = json.rms_de || 0;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("JSON parse error:", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Regex fallbacks for standard profcheck output
|
||||||
|
const avgMatch = stdout.match(/avg\.\s*dE\s*=\s*([\d\.]+)/i) || stdout.match(/average\s*dE\s*:\s*([\d\.]+)/i);
|
||||||
|
const maxMatch = stdout.match(/max\.\s*dE\s*=\s*([\d\.]+)/i) || stdout.match(/peak\s*dE\s*:\s*([\d\.]+)/i);
|
||||||
|
const rmsMatch = stdout.match(/rms\.\s*dE\s*=\s*([\d\.]+)/i) || stdout.match(/rms\s*dE\s*:\s*([\d\.]+)/i);
|
||||||
|
|
||||||
|
if (avgMatch) avgDe = parseFloat(avgMatch[1]);
|
||||||
|
if (maxMatch) maxDe = parseFloat(maxMatch[1]);
|
||||||
|
if (rmsMatch) rmsDe = parseFloat(rmsMatch[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
avgDeEl.textContent = avgDe.toFixed(2);
|
||||||
|
maxDeEl.textContent = maxDe.toFixed(2);
|
||||||
|
rmsDeEl.textContent = rmsDe.toFixed(2);
|
||||||
|
|
||||||
|
// Quality verdict
|
||||||
|
badgeEl.className = "report-badge";
|
||||||
|
if (avgDe < 1.0) {
|
||||||
|
badgeEl.textContent = "EXCELLENT";
|
||||||
|
badgeEl.classList.add("badge-excellent");
|
||||||
|
} else if (avgDe < 2.0) {
|
||||||
|
badgeEl.textContent = "GOOD";
|
||||||
|
badgeEl.classList.add("badge-good");
|
||||||
|
} else if (avgDe < 4.0) {
|
||||||
|
badgeEl.textContent = "ACCEPTABLE";
|
||||||
|
badgeEl.classList.add("badge-acceptable");
|
||||||
|
} else {
|
||||||
|
badgeEl.textContent = "POOR";
|
||||||
|
badgeEl.classList.add("badge-poor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -499,3 +499,146 @@ button.danger:hover {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Stage 4: Spinner & Success Card
|
||||||
|
======================================== */
|
||||||
|
.spinner-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
background: var(--panel-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-top-color: var(--accent-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-label {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-card {
|
||||||
|
background: rgba(76, 175, 80, 0.1);
|
||||||
|
border: 1px solid #4caf50;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-card h3 {
|
||||||
|
color: #4caf50;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-card p {
|
||||||
|
color: #ccc;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Stage 5: Report Card
|
||||||
|
======================================== */
|
||||||
|
.report-card {
|
||||||
|
background: var(--panel-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-badge {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-excellent {
|
||||||
|
background: rgba(76, 175, 80, 0.2);
|
||||||
|
color: #4caf50;
|
||||||
|
border: 1px solid #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-good {
|
||||||
|
background: rgba(33, 150, 243, 0.2);
|
||||||
|
color: #2196f3;
|
||||||
|
border: 1px solid #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-acceptable {
|
||||||
|
background: rgba(255, 152, 0, 0.2);
|
||||||
|
color: #ff9800;
|
||||||
|
border: 1px solid #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-poor {
|
||||||
|
background: rgba(244, 67, 54, 0.2);
|
||||||
|
color: #f44336;
|
||||||
|
border: 1px solid #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-box {
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-footnote {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
border-top: 1px dashed var(--border-color);
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user