feat(print): integrate native raw printing engine into Stage 2 UI (#28) #31
@@ -346,7 +346,7 @@ pub async fn run_profcheck(
|
|||||||
pub async fn get_windows_printers() -> Result<Vec<String>, String> {
|
pub async fn get_windows_printers() -> Result<Vec<String>, String> {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
crate::print::windows::get_printers()
|
crate::print::windows::get_printers().map(|list| list.into_iter().map(|p| p.name).collect())
|
||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
@@ -400,6 +400,48 @@ pub async fn print_target_cups(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_printers() -> Result<Vec<crate::print::Printer>, String> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
crate::print::windows::get_printers()
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
crate::print::unix::get_printers()
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
|
{
|
||||||
|
Err("Native printer enumeration is not supported on this platform.".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn print_target_native(
|
||||||
|
printer_name: String,
|
||||||
|
tiff_path: String,
|
||||||
|
ppd_uncorrected_passthrough: Option<bool>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let _ = ppd_uncorrected_passthrough;
|
||||||
|
crate::print::windows::print_target(&printer_name, &tiff_path)
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
crate::print::unix::print_target(
|
||||||
|
&printer_name,
|
||||||
|
&tiff_path,
|
||||||
|
ppd_uncorrected_passthrough.unwrap_or(false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
|
{
|
||||||
|
let _ = (printer_name, tiff_path, ppd_uncorrected_passthrough);
|
||||||
|
Err("Native raw printing is not supported on this platform.".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ pub fn run() {
|
|||||||
commands::print_target_windows,
|
commands::print_target_windows,
|
||||||
commands::get_cups_printers,
|
commands::get_cups_printers,
|
||||||
commands::print_target_cups,
|
commands::print_target_cups,
|
||||||
|
commands::get_printers,
|
||||||
|
commands::print_target_native,
|
||||||
settings::load_settings,
|
settings::load_settings,
|
||||||
settings::save_settings,
|
settings::save_settings,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ use windows::Win32::Graphics::Printing::{
|
|||||||
PRINTER_ENUM_LOCAL, PRINTER_INFO_2W, PRINTER_INFO_4W,
|
PRINTER_ENUM_LOCAL, PRINTER_INFO_2W, PRINTER_INFO_4W,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::print::Printer;
|
||||||
|
|
||||||
fn to_wide(s: &str) -> Vec<u16> {
|
fn to_wide(s: &str) -> Vec<u16> {
|
||||||
OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect()
|
OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enumerate available printers on the system
|
/// Enumerate available printers on the system
|
||||||
pub fn get_printers() -> Result<Vec<String>, String> {
|
pub fn get_printers() -> Result<Vec<Printer>, String> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let flags = PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS;
|
let flags = PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS;
|
||||||
let mut bytes_needed = 0u32;
|
let mut bytes_needed = 0u32;
|
||||||
@@ -74,7 +76,11 @@ pub fn get_printers() -> Result<Vec<String>, String> {
|
|||||||
let info = *p_info.add(i);
|
let info = *p_info.add(i);
|
||||||
if !info.pPrinterName.is_null() {
|
if !info.pPrinterName.is_null() {
|
||||||
let name = info.pPrinterName.to_string().map_err(|e| e.to_string())?;
|
let name = info.pPrinterName.to_string().map_err(|e| e.to_string())?;
|
||||||
printers.push(name);
|
printers.push(Printer {
|
||||||
|
name,
|
||||||
|
status: "Ready".to_string(),
|
||||||
|
is_default: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Ok(printers);
|
return Ok(printers);
|
||||||
@@ -101,7 +107,11 @@ pub fn get_printers() -> Result<Vec<String>, String> {
|
|||||||
let info = *p_info.add(i);
|
let info = *p_info.add(i);
|
||||||
if !info.pPrinterName.is_null() {
|
if !info.pPrinterName.is_null() {
|
||||||
let name = info.pPrinterName.to_string().map_err(|e| e.to_string())?;
|
let name = info.pPrinterName.to_string().map_err(|e| e.to_string())?;
|
||||||
printers.push(name);
|
printers.push(Printer {
|
||||||
|
name,
|
||||||
|
status: "Ready".to_string(),
|
||||||
|
is_default: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(printers)
|
Ok(printers)
|
||||||
|
|||||||
@@ -190,6 +190,56 @@
|
|||||||
<div class="gallery-info" id="galleryInfo"></div>
|
<div class="gallery-info" id="galleryInfo"></div>
|
||||||
<div class="gallery-grid" id="galleryGrid"></div>
|
<div class="gallery-grid" id="galleryGrid"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Native Raw Printing Engine (Bypass Colour Management) -->
|
||||||
|
<div class="print-panel hidden" id="rawPrintPanel">
|
||||||
|
<div class="print-panel-header">
|
||||||
|
<div class="print-panel-title">
|
||||||
|
<span class="print-icon">🖨️</span>
|
||||||
|
<h3>Direct Native Raw Printing</h3>
|
||||||
|
</div>
|
||||||
|
<span class="acpu-badge" title="Bypasses OS ICM / ColorSync & CUPS color management, replicating Adobe Color Print Utility">ACPU-Equivalent Mode</span>
|
||||||
|
</div>
|
||||||
|
<p class="print-panel-desc">
|
||||||
|
Send the generated profiling target directly to the printer nozzles with all OS and driver color management strictly bypassed.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Notification / Feedback Banner -->
|
||||||
|
<div id="printNotification" class="notification-banner hidden">
|
||||||
|
<span class="notification-icon" id="printNotificationIcon">ℹ️</span>
|
||||||
|
<div class="notification-text" id="printNotificationText"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="printer-controls-row">
|
||||||
|
<div class="form-group printer-select-group">
|
||||||
|
<label for="printerSelect">Destination Printer</label>
|
||||||
|
<div class="printer-select-wrapper">
|
||||||
|
<select id="printerSelect">
|
||||||
|
<option value="" disabled selected>Detecting printers...</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" class="btn-icon" id="btnRefreshPrinters" title="Refresh printer list">↻</button>
|
||||||
|
<span id="printerStatusBadge" class="status-badge badge-idle hidden">Idle</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group print-options-group" id="cupsOptionsGroup">
|
||||||
|
<label class="checkbox-label" title="Enable for printers that do not accept raw TIFF streams (uses uncorrected CUPS passthrough)">
|
||||||
|
<input type="checkbox" id="chkPpdFallback">
|
||||||
|
<span>PPD Uncorrected Passthrough (Fallback)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="print-actions-row">
|
||||||
|
<button class="primary btn-print" id="btnPrintAll">
|
||||||
|
<span class="btn-text">🖨️ Print All Pages (Bypass CM)</span>
|
||||||
|
<span class="btn-spinner hidden"></span>
|
||||||
|
</button>
|
||||||
|
<button class="secondary" id="btnAdvanceToStage3">
|
||||||
|
<span>Proceed to Measurement (Stage 3) →</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Stage 3: chartread -->
|
<!-- Stage 3: chartread -->
|
||||||
|
|||||||
+261
-12
@@ -5,6 +5,8 @@ import { setStage2Result } from './chartread.js';
|
|||||||
// Module-level state: set by Stage 1 when it completes
|
// Module-level state: set by Stage 1 when it completes
|
||||||
let stage1Basename = "";
|
let stage1Basename = "";
|
||||||
let stage1Cwd = "";
|
let stage1Cwd = "";
|
||||||
|
let currentManifest = null;
|
||||||
|
let discoveredPrinters = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called by targen.js (or app.js) after Stage 1 completes.
|
* Called by targen.js (or app.js) after Stage 1 completes.
|
||||||
@@ -30,6 +32,18 @@ export function initPrinttarg() {
|
|||||||
const galleryInfo = document.getElementById("galleryInfo");
|
const galleryInfo = document.getElementById("galleryInfo");
|
||||||
const galleryGrid = document.getElementById("galleryGrid");
|
const galleryGrid = document.getElementById("galleryGrid");
|
||||||
|
|
||||||
|
// Raw Printing UI elements
|
||||||
|
const rawPrintPanel = document.getElementById("rawPrintPanel");
|
||||||
|
const printerSelect = document.getElementById("printerSelect");
|
||||||
|
const btnRefreshPrinters = document.getElementById("btnRefreshPrinters");
|
||||||
|
const printerStatusBadge = document.getElementById("printerStatusBadge");
|
||||||
|
const chkPpdFallback = document.getElementById("chkPpdFallback");
|
||||||
|
const btnPrintAll = document.getElementById("btnPrintAll");
|
||||||
|
const btnAdvanceToStage3 = document.getElementById("btnAdvanceToStage3");
|
||||||
|
const printNotification = document.getElementById("printNotification");
|
||||||
|
const printNotificationIcon = document.getElementById("printNotificationIcon");
|
||||||
|
const printNotificationText = document.getElementById("printNotificationText");
|
||||||
|
|
||||||
// Show/hide custom page size inputs
|
// Show/hide custom page size inputs
|
||||||
pageSizeSelect.addEventListener("change", (e) => {
|
pageSizeSelect.addEventListener("change", (e) => {
|
||||||
if (e.target.value === "custom") {
|
if (e.target.value === "custom") {
|
||||||
@@ -39,7 +53,126 @@ export function initPrinttarg() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create Layout button
|
/**
|
||||||
|
* Display status feedback banners in the raw printing panel.
|
||||||
|
*/
|
||||||
|
function showNotification(type, message) {
|
||||||
|
if (!printNotification) return;
|
||||||
|
printNotification.className = `notification-banner ${type}`;
|
||||||
|
printNotification.classList.remove("hidden");
|
||||||
|
|
||||||
|
if (printNotificationIcon) {
|
||||||
|
if (type === "success") printNotificationIcon.textContent = "✓";
|
||||||
|
else if (type === "error") printNotificationIcon.textContent = "✕";
|
||||||
|
else printNotificationIcon.textContent = "ℹ️";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (printNotificationText) {
|
||||||
|
printNotificationText.textContent = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideNotification() {
|
||||||
|
if (printNotification) {
|
||||||
|
printNotification.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the badge reflecting current printer's status.
|
||||||
|
*/
|
||||||
|
function updatePrinterStatusBadge() {
|
||||||
|
const selectedName = printerSelect.value;
|
||||||
|
const printer = discoveredPrinters.find(p => p.name === selectedName);
|
||||||
|
|
||||||
|
if (!printer || !printerStatusBadge) {
|
||||||
|
if (printerStatusBadge) printerStatusBadge.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printerStatusBadge.classList.remove("hidden", "badge-idle", "badge-ready", "badge-printing", "badge-stopped", "badge-error", "badge-default");
|
||||||
|
|
||||||
|
const statusLower = (printer.status || "").toLowerCase();
|
||||||
|
printerStatusBadge.textContent = printer.status || "Ready";
|
||||||
|
|
||||||
|
if (statusLower.includes("print")) {
|
||||||
|
printerStatusBadge.classList.add("badge-printing");
|
||||||
|
} else if (statusLower.includes("stop") || statusLower.includes("disable") || statusLower.includes("error")) {
|
||||||
|
printerStatusBadge.classList.add("badge-stopped");
|
||||||
|
} else if (statusLower.includes("idle") || statusLower.includes("ready")) {
|
||||||
|
printerStatusBadge.classList.add("badge-idle");
|
||||||
|
} else {
|
||||||
|
printerStatusBadge.classList.add("badge-default");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover and enumerate all OS-installed printers via Tauri IPC.
|
||||||
|
*/
|
||||||
|
async function loadPrinters() {
|
||||||
|
if (!printerSelect) return;
|
||||||
|
printerSelect.disabled = true;
|
||||||
|
printerSelect.innerHTML = '<option value="" disabled selected>Enumerating printers...</option>';
|
||||||
|
if (printerStatusBadge) printerStatusBadge.classList.add("hidden");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const printers = await invoke("get_printers");
|
||||||
|
discoveredPrinters = Array.isArray(printers) ? printers : [];
|
||||||
|
printerSelect.innerHTML = "";
|
||||||
|
|
||||||
|
if (discoveredPrinters.length === 0) {
|
||||||
|
printerSelect.innerHTML = '<option value="" disabled selected>No printers found</option>';
|
||||||
|
printerSelect.disabled = true;
|
||||||
|
if (btnPrintAll) btnPrintAll.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printerSelect.disabled = false;
|
||||||
|
let defaultSelected = false;
|
||||||
|
|
||||||
|
discoveredPrinters.forEach((p, idx) => {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = p.name;
|
||||||
|
opt.textContent = `${p.name}${p.is_default ? ' (Default)' : ''} [${p.status || 'Ready'}]`;
|
||||||
|
if (p.is_default && !defaultSelected) {
|
||||||
|
opt.selected = true;
|
||||||
|
defaultSelected = true;
|
||||||
|
}
|
||||||
|
printerSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!defaultSelected && discoveredPrinters.length > 0) {
|
||||||
|
printerSelect.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnPrintAll && currentManifest) btnPrintAll.disabled = false;
|
||||||
|
updatePrinterStatusBadge();
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[ICCery Print] Failed to query printers:", err);
|
||||||
|
printerSelect.innerHTML = `<option value="" disabled selected>Failed to load printers</option>`;
|
||||||
|
printerSelect.disabled = true;
|
||||||
|
if (btnPrintAll) btnPrintAll.disabled = true;
|
||||||
|
showNotification("error", `Could not enumerate system printers: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (printerSelect) {
|
||||||
|
printerSelect.addEventListener("change", () => {
|
||||||
|
updatePrinterStatusBadge();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnRefreshPrinters) {
|
||||||
|
btnRefreshPrinters.addEventListener("click", () => {
|
||||||
|
loadPrinters();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load initial printer list on startup
|
||||||
|
loadPrinters();
|
||||||
|
|
||||||
|
// Create Layout button handler
|
||||||
btnCreateLayout.addEventListener("click", async () => {
|
btnCreateLayout.addEventListener("click", async () => {
|
||||||
// Validate that Stage 1 has been completed
|
// Validate that Stage 1 has been completed
|
||||||
if (!stage1Basename) {
|
if (!stage1Basename) {
|
||||||
@@ -51,6 +184,8 @@ export function initPrinttarg() {
|
|||||||
logPre.textContent = "";
|
logPre.textContent = "";
|
||||||
logContainer.classList.remove("hidden");
|
logContainer.classList.remove("hidden");
|
||||||
tiffGallery.classList.add("hidden");
|
tiffGallery.classList.add("hidden");
|
||||||
|
if (rawPrintPanel) rawPrintPanel.classList.add("hidden");
|
||||||
|
hideNotification();
|
||||||
galleryGrid.innerHTML = "";
|
galleryGrid.innerHTML = "";
|
||||||
btnCreateLayout.disabled = true;
|
btnCreateLayout.disabled = true;
|
||||||
|
|
||||||
@@ -83,8 +218,6 @@ export function initPrinttarg() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const processId = `printtarg_${stage1Basename}`;
|
const processId = `printtarg_${stage1Basename}`;
|
||||||
|
|
||||||
// Accumulate all stdout lines to extract JSON manifest at the end
|
|
||||||
let stdoutAccumulator = "";
|
let stdoutAccumulator = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -116,11 +249,14 @@ export function initPrinttarg() {
|
|||||||
// Parse the JSON manifest from stdout
|
// Parse the JSON manifest from stdout
|
||||||
const manifest = extractManifest(stdoutAccumulator);
|
const manifest = extractManifest(stdoutAccumulator);
|
||||||
if (manifest && manifest.pages && manifest.pages.length > 0) {
|
if (manifest && manifest.pages && manifest.pages.length > 0) {
|
||||||
|
currentManifest = manifest;
|
||||||
renderTiffGallery(manifest, config.cwd);
|
renderTiffGallery(manifest, config.cwd);
|
||||||
|
if (rawPrintPanel) rawPrintPanel.classList.remove("hidden");
|
||||||
|
if (discoveredPrinters.length > 0 && btnPrintAll) btnPrintAll.disabled = false;
|
||||||
|
showNotification("info", "Target pages generated. Select your destination printer below and print with color management strictly bypassed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
setStage2Result(stage1Basename, stage1Cwd);
|
setStage2Result(stage1Basename, stage1Cwd);
|
||||||
advanceToStage3();
|
|
||||||
} else {
|
} else {
|
||||||
logPre.textContent += `\n[ERROR] printtarg exited with code ${event.payload.code}.\n`;
|
logPre.textContent += `\n[ERROR] printtarg exited with code ${event.payload.code}.\n`;
|
||||||
btnCreateLayout.disabled = false;
|
btnCreateLayout.disabled = false;
|
||||||
@@ -138,13 +274,115 @@ export function initPrinttarg() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the JSON manifest object from the accumulated stdout.
|
* Spool a single TIFF target file with native color management bypass.
|
||||||
* The manifest is emitted by printtarg -u as a JSON block starting with { and ending with }.
|
*/
|
||||||
* It always appears at the end of stdout, after all "Creating file..." lines.
|
async function printTargetFile(filePath, label, triggeringButton) {
|
||||||
|
const printerName = printerSelect ? printerSelect.value : "";
|
||||||
|
if (!printerName) {
|
||||||
|
showNotification("error", "Please select a destination printer first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ppdFallback = chkPpdFallback ? chkPpdFallback.checked : false;
|
||||||
|
const origBtnContent = triggeringButton ? triggeringButton.innerHTML : "";
|
||||||
|
|
||||||
|
if (triggeringButton) {
|
||||||
|
triggeringButton.disabled = true;
|
||||||
|
triggeringButton.innerHTML = '<span class="btn-spinner"></span> Spooling...';
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification("info", `Sending ${label} to '${printerName}' (bypassing color management)...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke("print_target_native", {
|
||||||
|
printerName,
|
||||||
|
tiffPath: filePath,
|
||||||
|
ppdUncorrectedPassthrough: ppdFallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
showNotification("success", `✓ Successfully spooled ${label} to '${printerName}' with raw color management bypass.`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[ICCery Print Error]:", err);
|
||||||
|
showNotification("error", `Print job failed for ${label}: ${err}`);
|
||||||
|
} finally {
|
||||||
|
if (triggeringButton) {
|
||||||
|
triggeringButton.disabled = false;
|
||||||
|
triggeringButton.innerHTML = origBtnContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print all generated TIFF target pages in sequence.
|
||||||
|
*/
|
||||||
|
if (btnPrintAll) {
|
||||||
|
btnPrintAll.addEventListener("click", async () => {
|
||||||
|
if (!currentManifest || !currentManifest.pages || currentManifest.pages.length === 0) {
|
||||||
|
showNotification("error", "No generated target pages available to print.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const printerName = printerSelect ? printerSelect.value : "";
|
||||||
|
if (!printerName) {
|
||||||
|
showNotification("error", "Please select a destination printer first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ppdFallback = chkPpdFallback ? chkPpdFallback.checked : false;
|
||||||
|
const cwd = stage1Cwd;
|
||||||
|
const sep = cwd.includes('\\') ? '\\' : '/';
|
||||||
|
const pages = currentManifest.pages;
|
||||||
|
|
||||||
|
btnPrintAll.disabled = true;
|
||||||
|
const btnText = btnPrintAll.querySelector(".btn-text");
|
||||||
|
const btnSpinner = btnPrintAll.querySelector(".btn-spinner");
|
||||||
|
if (btnSpinner) btnSpinner.classList.remove("hidden");
|
||||||
|
if (btnText) btnText.textContent = "Spooling Targets...";
|
||||||
|
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < pages.length; i++) {
|
||||||
|
const page = pages[i];
|
||||||
|
const filePath = cwd ? `${cwd}${sep}${page.filename}` : page.filename;
|
||||||
|
showNotification("info", `Spooling page ${i + 1} of ${pages.length} (${page.filename}) to '${printerName}'...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke("print_target_native", {
|
||||||
|
printerName,
|
||||||
|
tiffPath: filePath,
|
||||||
|
ppdUncorrectedPassthrough: ppdFallback,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[ICCery Print Error] Page ${page.filename}:`, err);
|
||||||
|
showNotification("error", `Failed on page ${page.filename}: ${err}`);
|
||||||
|
errorCount++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorCount === 0) {
|
||||||
|
showNotification("success", `✓ All ${pages.length} target page(s) successfully spooled to '${printerName}'!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
btnPrintAll.disabled = false;
|
||||||
|
if (btnSpinner) btnSpinner.classList.add("hidden");
|
||||||
|
if (btnText) btnText.textContent = "🖨️ Print All Pages (Bypass CM)";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed to Stage 3
|
||||||
|
if (btnAdvanceToStage3) {
|
||||||
|
btnAdvanceToStage3.addEventListener("click", () => {
|
||||||
|
setStage2Result(stage1Basename, stage1Cwd);
|
||||||
|
advanceToStage3();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract JSON manifest emitted by printtarg -u.
|
||||||
*/
|
*/
|
||||||
function extractManifest(stdout) {
|
function extractManifest(stdout) {
|
||||||
try {
|
try {
|
||||||
// Find the last JSON object in the output
|
|
||||||
const jsonStart = stdout.lastIndexOf('{\n "event": "manifest"');
|
const jsonStart = stdout.lastIndexOf('{\n "event": "manifest"');
|
||||||
if (jsonStart === -1) return null;
|
if (jsonStart === -1) return null;
|
||||||
const jsonEnd = stdout.indexOf('\n}', jsonStart);
|
const jsonEnd = stdout.indexOf('\n}', jsonStart);
|
||||||
@@ -158,9 +396,7 @@ export function initPrinttarg() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render TIFF preview gallery using base64-encoded images from the backend.
|
* Render TIFF preview gallery with individual card print actions.
|
||||||
* @param {Object} manifest - The parsed JSON manifest from printtarg -u
|
|
||||||
* @param {string} cwd - The working directory where TIFFs were generated
|
|
||||||
*/
|
*/
|
||||||
async function renderTiffGallery(manifest, cwd) {
|
async function renderTiffGallery(manifest, cwd) {
|
||||||
tiffGallery.classList.remove("hidden");
|
tiffGallery.classList.remove("hidden");
|
||||||
@@ -169,6 +405,7 @@ export function initPrinttarg() {
|
|||||||
const totalPatches = manifest.pages.reduce((sum, p) => sum + p.patches, 0);
|
const totalPatches = manifest.pages.reduce((sum, p) => sum + p.patches, 0);
|
||||||
const dims = manifest.pages[0];
|
const dims = manifest.pages[0];
|
||||||
galleryInfo.textContent = `${pageCount} page(s) · ${totalPatches} patches · ${dims.width_mm} × ${dims.height_mm} mm per page`;
|
galleryInfo.textContent = `${pageCount} page(s) · ${totalPatches} patches · ${dims.width_mm} × ${dims.height_mm} mm per page`;
|
||||||
|
galleryGrid.innerHTML = "";
|
||||||
|
|
||||||
for (const page of manifest.pages) {
|
for (const page of manifest.pages) {
|
||||||
const sep = cwd.includes('\\') ? '\\' : '/';
|
const sep = cwd.includes('\\') ? '\\' : '/';
|
||||||
@@ -187,7 +424,6 @@ export function initPrinttarg() {
|
|||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.src = `data:image/tiff;base64,${base64Data}`;
|
img.src = `data:image/tiff;base64,${base64Data}`;
|
||||||
img.alt = page.filename;
|
img.alt = page.filename;
|
||||||
// TIFF may not render natively in all browsers — provide a fallback
|
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
img.remove();
|
img.remove();
|
||||||
const fallback = document.createElement("div");
|
const fallback = document.createElement("div");
|
||||||
@@ -203,6 +439,19 @@ export function initPrinttarg() {
|
|||||||
card.appendChild(fallback);
|
card.appendChild(fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Individual Card Print Button
|
||||||
|
const cardActions = document.createElement("div");
|
||||||
|
cardActions.className = "gallery-card-actions";
|
||||||
|
const btnPrintCard = document.createElement("button");
|
||||||
|
btnPrintCard.type = "button";
|
||||||
|
btnPrintCard.className = "btn-print-page";
|
||||||
|
btnPrintCard.innerHTML = "<span>🖨️ Print Page</span>";
|
||||||
|
btnPrintCard.addEventListener("click", () => {
|
||||||
|
printTargetFile(filePath, page.filename, btnPrintCard);
|
||||||
|
});
|
||||||
|
cardActions.appendChild(btnPrintCard);
|
||||||
|
card.appendChild(cardActions);
|
||||||
|
|
||||||
galleryGrid.appendChild(card);
|
galleryGrid.appendChild(card);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -847,3 +847,229 @@ button.danger:hover {
|
|||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Stage 2: Native Raw Printing Engine Panel
|
||||||
|
======================================== */
|
||||||
|
.print-panel {
|
||||||
|
margin-top: 24px;
|
||||||
|
background: var(--panel-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-panel-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-panel-title h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.acpu-badge {
|
||||||
|
background: rgba(0, 122, 204, 0.15);
|
||||||
|
border: 1px solid rgba(0, 122, 204, 0.4);
|
||||||
|
color: #4fc3f7;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-panel-desc {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #aaa;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.printer-controls-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.printer-select-group {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 280px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.printer-select-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.printer-select-wrapper select {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:hover {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-idle, .badge-ready {
|
||||||
|
background: rgba(76, 175, 80, 0.15);
|
||||||
|
color: #81c784;
|
||||||
|
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-printing {
|
||||||
|
background: rgba(33, 150, 243, 0.15);
|
||||||
|
color: #64b5f6;
|
||||||
|
border: 1px solid rgba(33, 150, 243, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-stopped, .badge-error {
|
||||||
|
background: rgba(244, 67, 54, 0.15);
|
||||||
|
color: #e57373;
|
||||||
|
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-default {
|
||||||
|
background: rgba(255, 193, 7, 0.15);
|
||||||
|
color: #ffd54f;
|
||||||
|
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-options-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-banner.info {
|
||||||
|
background: rgba(33, 150, 243, 0.12);
|
||||||
|
border: 1px solid rgba(33, 150, 243, 0.3);
|
||||||
|
color: #90caf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-banner.success {
|
||||||
|
background: rgba(76, 175, 80, 0.12);
|
||||||
|
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||||
|
color: #a5d6a7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-banner.error {
|
||||||
|
background: rgba(244, 67, 54, 0.12);
|
||||||
|
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||||
|
color: #ef9a9a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-actions-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-card-actions {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.15);
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-print-page {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-print-page:hover {
|
||||||
|
background: var(--accent-color);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: #fff;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user