Feature: Custom target label metadata input & generation in Stage 2 (printtarg -d) (resolves #119) #132

Merged
gronod merged 1 commits from feat/119-printtarg-custom-label into development 2026-08-29 12:26:08 +01:00
3 changed files with 176 additions and 0 deletions
+39
View File
@@ -319,6 +319,7 @@ pub struct PrinttargConfig {
pub bit_depth: u8, // 8 or 16
#[serde(default = "default_dpi")]
pub dpi: u32, // TIFF resolution, defaults to 300 DPI
pub custom_label: Option<String>, // Custom chart label / description metadata string (-d)
pub basename: String, // Must match the .ti1 basename from Stage 1
pub cwd: String, // Working directory where the .ti1 file resides
}
@@ -370,6 +371,11 @@ pub fn build_printtarg_args(config: &PrinttargConfig) -> Vec<String> {
config.page_size.clone(),
];
if let Some(ref label) = config.custom_label {
args.push("-d".to_string());
args.push(label.clone());
}
if config.bit_depth == 16 {
args.push("-T".to_string());
} else {
@@ -850,6 +856,7 @@ mod tests {
page_size: "A4".to_string(),
bit_depth: 8,
dpi: 100,
custom_label: None,
basename: "my_profile".to_string(),
cwd: "/tmp".to_string(),
};
@@ -864,6 +871,7 @@ mod tests {
page_size: "Letter".to_string(),
bit_depth: 16,
dpi: 300,
custom_label: None,
basename: "cmyk_profile".to_string(),
cwd: "/home/user".to_string(),
};
@@ -878,6 +886,7 @@ mod tests {
page_size: "200x400".to_string(),
bit_depth: 8,
dpi: 150,
custom_label: None,
basename: "custom_target".to_string(),
cwd: "/tmp".to_string(),
};
@@ -885,6 +894,36 @@ mod tests {
assert_eq!(args, vec!["-v", "-u", "-i", "SS", "-p", "200x400", "-t", "150", "custom_target"]);
}
#[test]
fn test_build_printtarg_args_with_custom_label() {
let config = PrinttargConfig {
instrument: "i1".to_string(),
page_size: "A4".to_string(),
bit_depth: 8,
dpi: 300,
custom_label: Some("ICCery - Pro900 - Luster - 29/08/2026 12:00".to_string()),
basename: "my_profile".to_string(),
cwd: "/tmp".to_string(),
};
let args = build_printtarg_args(&config);
assert_eq!(
args,
vec![
"-v",
"-u",
"-i",
"i1",
"-p",
"A4",
"-d",
"ICCery - Pro900 - Luster - 29/08/2026 12:00",
"-t",
"300",
"my_profile"
]
);
}
#[test]
fn test_build_chartread_args_auto() {
let config = ChartreadConfig {
+37
View File
@@ -181,6 +181,43 @@
<label for="tiffDpi">Target Resolution (DPI)</label>
<input type="number" id="tiffDpi" min="72" max="600" value="300" placeholder="300">
</div>
<!-- Target Label & Metadata (printtarg -d) -->
<div class="form-group" style="margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border-color, #333);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
<label style="margin: 0; font-weight: 600;">Target Label &amp; Metadata (Optional)</label>
<button type="button" class="secondary" id="btnToggleLabelEdit" style="font-size: 0.75rem; padding: 2px 8px;">✏️ Edit Label</button>
</div>
<p class="sub-label" style="margin-bottom: 10px;">Printed below test patches to identify hardware, ink, and media used for this profiling run.</p>
<div class="input-row" style="margin-bottom: 10px;">
<div>
<label for="targetMetadataPrinter" class="sub-label">Printer Model</label>
<input type="text" id="targetMetadataPrinter" placeholder="e.g. Epson SC-P900">
</div>
<div>
<label for="targetMetadataInkSet" class="sub-label">Ink Set</label>
<input type="text" id="targetMetadataInkSet" placeholder="e.g. UltraChrome PRO10">
</div>
</div>
<div class="input-row" style="margin-bottom: 10px;">
<div>
<label for="targetMetadataDriverPaper" class="sub-label">Driver Paper Setting</label>
<input type="text" id="targetMetadataDriverPaper" placeholder="e.g. Premium Luster Photo Paper">
</div>
<div>
<label for="targetMetadataActualPaper" class="sub-label">Actual Paper Substrate</label>
<input type="text" id="targetMetadataActualPaper" placeholder="e.g. Ilford Galerie Smooth Pearl 310">
</div>
</div>
<!-- Live Assembled Label Preview / Manual Override -->
<div>
<label for="targetLabelPreview" class="sub-label">Printed Page Label Preview</label>
<input type="text" id="targetLabelPreview" placeholder="ICCery - Target Label Preview" readonly style="font-family: monospace; font-size: 0.85rem;">
</div>
</div>
</div>
<button class="primary" id="btnCreateLayout">Create Layout (.ti2 + .tif)</button>
+100
View File
@@ -9,6 +9,8 @@ let stage1Cwd = "";
let currentManifest = null;
let discoveredPrinters = [];
let updateLabelPreviewFn = null;
/**
* Called by targen.js (or app.js) after Stage 1 completes.
* Passes the basename and working directory forward.
@@ -17,6 +19,9 @@ export function setStage1Result(basename, cwd) {
stage1Basename = basename || wizardState.basename;
stage1Cwd = cwd || wizardState.cwd;
wizardState.setTarget(stage1Basename, stage1Cwd);
if (updateLabelPreviewFn) {
updateLabelPreviewFn();
}
}
export function initPrinttarg() {
@@ -34,6 +39,93 @@ export function initPrinttarg() {
const galleryInfo = document.getElementById("galleryInfo");
const galleryGrid = document.getElementById("galleryGrid");
// Target Label & Metadata elements (printtarg -d)
const targetMetadataPrinter = document.getElementById("targetMetadataPrinter");
const targetMetadataInkSet = document.getElementById("targetMetadataInkSet");
const targetMetadataDriverPaper = document.getElementById("targetMetadataDriverPaper");
const targetMetadataActualPaper = document.getElementById("targetMetadataActualPaper");
const targetLabelPreview = document.getElementById("targetLabelPreview");
const btnToggleLabelEdit = document.getElementById("btnToggleLabelEdit");
let isManualLabelOverride = false;
/**
* Format current local date & time into standard DD/MM/YYYY HH:MM format.
*/
function formatTimestamp(d = new Date()) {
const pad = (n) => String(n).padStart(2, "0");
const day = pad(d.getDate());
const month = pad(d.getMonth() + 1);
const year = d.getFullYear();
const hours = pad(d.getHours());
const mins = pad(d.getMinutes());
return `${day}/${month}/${year} ${hours}:${mins}`;
}
/**
* Assemble dynamic label string from Stage 1 run name and metadata inputs.
*/
function assembleTargetLabel() {
const parts = ["ICCery"];
const runName = stage1Basename || wizardState.basename || "";
if (runName.trim()) {
parts.push(runName.trim());
}
const printer = targetMetadataPrinter ? targetMetadataPrinter.value.trim() : "";
if (printer) parts.push(printer);
const inkSet = targetMetadataInkSet ? targetMetadataInkSet.value.trim() : "";
if (inkSet) parts.push(inkSet);
const driverPaper = targetMetadataDriverPaper ? targetMetadataDriverPaper.value.trim() : "";
if (driverPaper) parts.push(driverPaper);
const actualPaper = targetMetadataActualPaper ? targetMetadataActualPaper.value.trim() : "";
if (actualPaper) parts.push(actualPaper);
parts.push(formatTimestamp());
return parts.join(" - ");
}
function updateLabelPreview() {
if (!targetLabelPreview) return;
if (!isManualLabelOverride) {
targetLabelPreview.value = assembleTargetLabel();
}
}
updateLabelPreviewFn = updateLabelPreview;
updateLabelPreview();
// Bind input listeners for dynamic label assembly
[targetMetadataPrinter, targetMetadataInkSet, targetMetadataDriverPaper, targetMetadataActualPaper].forEach(input => {
if (input) {
input.addEventListener("input", () => {
updateLabelPreview();
});
}
});
// Toggle between auto-assembled preview and manual edit
if (btnToggleLabelEdit && targetLabelPreview) {
btnToggleLabelEdit.addEventListener("click", () => {
isManualLabelOverride = !isManualLabelOverride;
if (isManualLabelOverride) {
targetLabelPreview.removeAttribute("readonly");
targetLabelPreview.style.background = "var(--bg-color)";
targetLabelPreview.style.borderColor = "var(--accent-color, #3b82f6)";
btnToggleLabelEdit.textContent = "↺ Reset Auto";
targetLabelPreview.focus();
} else {
targetLabelPreview.setAttribute("readonly", true);
targetLabelPreview.style.background = "var(--bg-color)";
targetLabelPreview.style.borderColor = "var(--border-color, #333)";
btnToggleLabelEdit.textContent = "✏️ Edit Label";
updateLabelPreview();
}
});
}
// Raw Printing UI elements
const rawPrintPanel = document.getElementById("rawPrintPanel");
const printerSelect = document.getElementById("printerSelect");
@@ -302,11 +394,19 @@ export function initPrinttarg() {
});
const dpi = tiffDpi && tiffDpi.value ? parseInt(tiffDpi.value, 10) || 300 : 300;
// Obtain custom label string
let customLabel = null;
if (targetLabelPreview && targetLabelPreview.value.trim()) {
customLabel = targetLabelPreview.value.trim();
}
const config = {
instrument: instrumentSelect.value,
page_size: pageSize,
bit_depth: bitDepth,
dpi: dpi,
custom_label: customLabel,
basename: stage1Basename,
cwd: stage1Cwd,
};