@@ -150,7 +150,24 @@ pub fn export_preset_json(preset: ProfilingPreset) -> Result<String, String> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn import_preset_json(json: String) -> Result<ProfilingPreset, String> {
|
pub fn import_preset_json(json: String) -> Result<ProfilingPreset, String> {
|
||||||
serde_json::from_str::<ProfilingPreset>(&json).map_err(|e| format!("Invalid preset format: {}", e))
|
let preset: ProfilingPreset = serde_json::from_str::<ProfilingPreset>(&json)
|
||||||
|
.map_err(|e| format!("Invalid preset format: {}", e))?;
|
||||||
|
|
||||||
|
if preset.name.trim().is_empty() {
|
||||||
|
return Err("Preset name cannot be empty".to_string());
|
||||||
|
}
|
||||||
|
if preset.name.len() > 200 {
|
||||||
|
return Err("Preset name is too long (max 200 characters)".to_string());
|
||||||
|
}
|
||||||
|
if let Some(ref desc) = preset.description {
|
||||||
|
if desc.len() > 500 {
|
||||||
|
return Err("Preset description is too long (max 500 characters)".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if preset.dpi < 72 || preset.dpi > 2400 {
|
||||||
|
return Err("Preset DPI out of allowed range (72 - 2400)".to_string());
|
||||||
|
}
|
||||||
|
Ok(preset)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -168,6 +185,22 @@ mod tests {
|
|||||||
assert_eq!(defaults[1].patch_count, 1500);
|
assert_eq!(defaults[1].patch_count, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_import_preset_validation_name_length() {
|
||||||
|
let mut preset = get_default_presets()[0].clone();
|
||||||
|
preset.name = "a".repeat(201);
|
||||||
|
let json = serde_json::to_string(&preset).unwrap();
|
||||||
|
assert!(import_preset_json(json).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_import_preset_validation_dpi() {
|
||||||
|
let mut preset = get_default_presets()[0].clone();
|
||||||
|
preset.dpi = 10;
|
||||||
|
let json = serde_json::to_string(&preset).unwrap();
|
||||||
|
assert!(import_preset_json(json).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_preset_json_export_and_import() {
|
fn test_preset_json_export_and_import() {
|
||||||
let preset = ProfilingPreset {
|
let preset = ProfilingPreset {
|
||||||
|
|||||||
@@ -176,6 +176,11 @@
|
|||||||
<label><input type="radio" name="bitDepth" value="16"> 16-bit</label>
|
<label><input type="radio" name="bitDepth" value="16"> 16-bit</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tiffDpi">Target Resolution (DPI)</label>
|
||||||
|
<input type="number" id="tiffDpi" min="72" max="600" value="300" placeholder="300">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="primary" id="btnCreateLayout">Create Layout (.ti2 + .tif)</button>
|
<button class="primary" id="btnCreateLayout">Create Layout (.ti2 + .tif)</button>
|
||||||
|
|||||||
+74
-35
@@ -50,44 +50,75 @@ export async function initPresets() {
|
|||||||
const item = document.createElement("div");
|
const item = document.createElement("div");
|
||||||
item.style.cssText = "display:flex; justify-content:space-between; align-items:center; background:rgba(255,255,255,0.05); padding:8px 12px; border-radius:6px;";
|
item.style.cssText = "display:flex; justify-content:space-between; align-items:center; background:rgba(255,255,255,0.05); padding:8px 12px; border-radius:6px;";
|
||||||
|
|
||||||
item.innerHTML = `
|
const leftCol = document.createElement("div");
|
||||||
<div style="flex:1; margin-right:12px;">
|
leftCol.style.cssText = "flex:1; margin-right:12px;";
|
||||||
<div style="font-weight:600; font-size:0.9rem;">${p.name} ${isBuiltin ? '<span style="font-size:0.7rem; opacity:0.6; border:1px solid #555; padding:1px 4px; border-radius:3px; margin-left:4px;">Built-in</span>' : ''}</div>
|
|
||||||
<div style="font-size:0.75rem; opacity:0.7; margin-top:2px;">${p.description || "No description"}</div>
|
|
||||||
<div style="font-size:0.7rem; opacity:0.5; margin-top:2px;">${p.colour_space.toUpperCase()} • ${p.patch_count} patches • ${p.page_size} • ${p.bit_depth}-bit • Quality ${p.colprof_quality.toUpperCase()}</div>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex; gap:6px;">
|
|
||||||
<button type="button" class="secondary btn-export-one" data-id="${p.id}" style="font-size:0.75rem; padding:4px 8px;">Export</button>
|
|
||||||
${!isBuiltin ? `<button type="button" class="danger btn-delete-one" data-id="${p.id}" style="font-size:0.75rem; padding:4px 8px;">Delete</button>` : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
managePresetsList.appendChild(item);
|
const titleRow = document.createElement("div");
|
||||||
});
|
titleRow.style.cssText = "font-weight:600; font-size:0.9rem; display:flex; align-items:center; gap:6px;";
|
||||||
|
const titleSpan = document.createElement("span");
|
||||||
|
titleSpan.textContent = p.name || "Untitled";
|
||||||
|
titleRow.appendChild(titleSpan);
|
||||||
|
|
||||||
// Attach listeners for export / delete
|
if (isBuiltin) {
|
||||||
managePresetsList.querySelectorAll(".btn-export-one").forEach((btn) => {
|
const badge = document.createElement("span");
|
||||||
btn.addEventListener("click", async () => {
|
badge.style.cssText = "font-size:0.7rem; opacity:0.6; border:1px solid #555; padding:1px 4px; border-radius:3px;";
|
||||||
const id = btn.getAttribute("data-id");
|
badge.textContent = "Built-in";
|
||||||
const preset = currentPresets.find(p => p.id === id);
|
titleRow.appendChild(badge);
|
||||||
if (preset) exportPreset(preset);
|
}
|
||||||
});
|
leftCol.appendChild(titleRow);
|
||||||
});
|
|
||||||
|
|
||||||
managePresetsList.querySelectorAll(".btn-delete-one").forEach((btn) => {
|
const descDiv = document.createElement("div");
|
||||||
btn.addEventListener("click", async () => {
|
descDiv.style.cssText = "font-size:0.8rem; opacity:0.75; margin-top:2px;";
|
||||||
const id = btn.getAttribute("data-id");
|
descDiv.textContent = p.description || "No description";
|
||||||
if (confirm("Delete this custom preset?")) {
|
leftCol.appendChild(descDiv);
|
||||||
try {
|
|
||||||
currentPresets = await invoke("delete_preset", { id });
|
const metaDiv = document.createElement("div");
|
||||||
if (activePresetId === id) activePresetId = currentPresets[0]?.id || "preset-std-rgb";
|
metaDiv.style.cssText = "font-size:0.75rem; opacity:0.5; margin-top:4px;";
|
||||||
renderPresetDropdown();
|
metaDiv.textContent = `${(p.colour_space || "").toUpperCase()} • ${p.patch_count} patches • ${p.page_size} • ${p.dpi || 300} DPI • ${p.bit_depth}-bit`;
|
||||||
renderManagePresetsList();
|
leftCol.appendChild(metaDiv);
|
||||||
} catch (err) {
|
|
||||||
alert("Delete failed: " + err);
|
item.appendChild(leftCol);
|
||||||
|
|
||||||
|
const actionsDiv = document.createElement("div");
|
||||||
|
actionsDiv.style.cssText = "display:flex; gap:6px;";
|
||||||
|
|
||||||
|
const exportBtn = document.createElement("button");
|
||||||
|
exportBtn.type = "button";
|
||||||
|
exportBtn.className = "icon-btn";
|
||||||
|
exportBtn.title = "Export to JSON";
|
||||||
|
exportBtn.style.cssText = "font-size:0.85rem; padding:4px 6px; border:1px solid var(--border-color, #333); border-radius:4px; background:transparent; cursor:pointer;";
|
||||||
|
exportBtn.textContent = "💾";
|
||||||
|
exportBtn.addEventListener("click", () => exportPreset(p));
|
||||||
|
actionsDiv.appendChild(exportBtn);
|
||||||
|
|
||||||
|
if (!isBuiltin) {
|
||||||
|
const deleteBtn = document.createElement("button");
|
||||||
|
deleteBtn.type = "button";
|
||||||
|
deleteBtn.className = "icon-btn";
|
||||||
|
deleteBtn.title = "Delete Preset";
|
||||||
|
deleteBtn.style.cssText = "font-size:0.85rem; padding:4px 6px; border:1px solid var(--border-color, #333); border-radius:4px; background:transparent; cursor:pointer; color:#ff6b6b;";
|
||||||
|
deleteBtn.textContent = "🗑️";
|
||||||
|
deleteBtn.addEventListener("click", async () => {
|
||||||
|
if (confirm(`Delete preset "${p.name}"?`)) {
|
||||||
|
try {
|
||||||
|
currentPresets = await invoke("delete_preset", { id: p.id });
|
||||||
|
if (activePresetId === p.id) {
|
||||||
|
activePresetId = "preset-std-rgb";
|
||||||
|
const fallback = currentPresets.find(x => x.id === activePresetId);
|
||||||
|
if (fallback) applyPreset(fallback);
|
||||||
|
}
|
||||||
|
renderPresetDropdown();
|
||||||
|
renderManagePresetsList();
|
||||||
|
} catch (err) {
|
||||||
|
alert("Delete preset failed: " + err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
actionsDiv.appendChild(deleteBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
item.appendChild(actionsDiv);
|
||||||
|
managePresetsList.appendChild(item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +164,11 @@ export async function initPresets() {
|
|||||||
if (Number(r.value) === preset.bit_depth) r.checked = true;
|
if (Number(r.value) === preset.bit_depth) r.checked = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tiffDpi = document.getElementById("tiffDpi");
|
||||||
|
if (tiffDpi && preset.dpi) {
|
||||||
|
tiffDpi.value = preset.dpi;
|
||||||
|
}
|
||||||
|
|
||||||
// Stage 4 controls
|
// Stage 4 controls
|
||||||
const colprofQuality = document.getElementById("colprofQuality");
|
const colprofQuality = document.getElementById("colprofQuality");
|
||||||
if (colprofQuality && preset.colprof_quality) colprofQuality.value = preset.colprof_quality;
|
if (colprofQuality && preset.colprof_quality) colprofQuality.value = preset.colprof_quality;
|
||||||
@@ -171,6 +207,9 @@ export async function initPresets() {
|
|||||||
const bitDepthRadio = document.querySelector('input[name="bitDepth"]:checked');
|
const bitDepthRadio = document.querySelector('input[name="bitDepth"]:checked');
|
||||||
const bit_depth = bitDepthRadio ? parseInt(bitDepthRadio.value, 10) : 8;
|
const bit_depth = bitDepthRadio ? parseInt(bitDepthRadio.value, 10) : 8;
|
||||||
|
|
||||||
|
const tiffDpi = document.getElementById("tiffDpi");
|
||||||
|
const dpi = tiffDpi && tiffDpi.value ? parseInt(tiffDpi.value, 10) || 300 : 300;
|
||||||
|
|
||||||
const colprofQuality = document.getElementById("colprofQuality");
|
const colprofQuality = document.getElementById("colprofQuality");
|
||||||
const colprof_quality = colprofQuality ? colprofQuality.value : "m";
|
const colprof_quality = colprofQuality ? colprofQuality.value : "m";
|
||||||
|
|
||||||
@@ -188,7 +227,7 @@ export async function initPresets() {
|
|||||||
instrument,
|
instrument,
|
||||||
page_size,
|
page_size,
|
||||||
bit_depth,
|
bit_depth,
|
||||||
dpi: 300,
|
dpi,
|
||||||
colprof_algorithm,
|
colprof_algorithm,
|
||||||
colprof_quality,
|
colprof_quality,
|
||||||
colprof_intent: null,
|
colprof_intent: null,
|
||||||
|
|||||||
+2
-1
@@ -301,11 +301,12 @@ export function initPrinttarg() {
|
|||||||
if (radio.checked) bitDepth = parseInt(radio.value, 10);
|
if (radio.checked) bitDepth = parseInt(radio.value, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dpi = tiffDpi && tiffDpi.value ? parseInt(tiffDpi.value, 10) || 300 : 300;
|
||||||
const config = {
|
const config = {
|
||||||
instrument: instrumentSelect.value,
|
instrument: instrumentSelect.value,
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
bit_depth: bitDepth,
|
bit_depth: bitDepth,
|
||||||
dpi: 300, // Master high-resolution layout scaled automatically to device bounds
|
dpi: dpi,
|
||||||
basename: stage1Basename,
|
basename: stage1Basename,
|
||||||
cwd: stage1Cwd,
|
cwd: stage1Cwd,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user