Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
4a79fa80ba | |||
8949d056d6 | |||
59dcb9f4a8 | |||
45276945dc | |||
bba2e2c416 |
256
index.html
256
index.html
@@ -1,70 +1,186 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>ZIR - Live Preview</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0; padding: 0;
|
||||
background-color: #f4f4f7; color: #333;
|
||||
display: flex; flex-direction: column; align-items: center; min-height: 100vh;
|
||||
}
|
||||
header {
|
||||
background-color: #0078d7; color: white; width: 100%;
|
||||
padding: 16px 24px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
text-align: center; font-size: 1.2rem; font-weight: 600;
|
||||
}
|
||||
main { flex: 1; display:flex; align-items:center; justify-content:center; width:100%; padding:20px; box-sizing:border-box; }
|
||||
.preview-container {
|
||||
background-color: white; padding: 16px; border-radius: 12px;
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.08); max-width: 1000px; width: 100%; text-align: center; position: relative;
|
||||
}
|
||||
.preview-container img { border: 1px solid #e0e0e0; max-width: 100%; height: auto; border-radius: 8px; display: none; }
|
||||
.info { margin-top: 12px; font-size: 0.95rem; color: #555; }
|
||||
footer { padding: 12px 24px; text-align:center; font-size:0.8rem; color:#888; }
|
||||
.spinner { border:6px solid #f3f3f3; border-top:6px solid #0078d7; border-radius:50%; width:48px; height:48px; animation:spin 1s linear infinite; margin: 26px auto; }
|
||||
@keyframes spin { 0%{transform:rotate(0deg)} 100%{transform:rotate(360deg)} }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>ZIR – Live Preview</header>
|
||||
<main>
|
||||
<div class="preview-container">
|
||||
<div class="spinner" id="spinner"></div>
|
||||
<img id="labelImage" src="" alt="Label preview" />
|
||||
<div class="info"><code id="inputPath"></code> Neue Labels werden automatisch angezeigt.</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer>© 2025 ZIR Viewer</footer>
|
||||
|
||||
<script>
|
||||
const { ipcRenderer, remote } = require('electron');
|
||||
const img = document.getElementById('labelImage');
|
||||
const spinner = document.getElementById('spinner');
|
||||
const inputPath = document.getElementById('inputPath');
|
||||
|
||||
// Zeige Input-Pfad (wird per ipc vom Main ggf. gesetzt)
|
||||
ipcRenderer.on('show-input-path', (ev, p) => {
|
||||
inputPath.textContent = p;
|
||||
});
|
||||
|
||||
ipcRenderer.on('show-png', (ev, filePath) => {
|
||||
spinner.style.display = 'block';
|
||||
img.style.display = 'none';
|
||||
img.src = filePath + "?t=" + Date.now();
|
||||
img.onload = () => {
|
||||
spinner.style.display = 'none';
|
||||
img.style.display = 'block';
|
||||
};
|
||||
img.onerror = () => {
|
||||
spinner.style.display = 'none';
|
||||
img.style.display = 'none';
|
||||
};
|
||||
});
|
||||
|
||||
// Request main to send input path (in case main wants to show it)
|
||||
ipcRenderer.send('request-input-path');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>ZIR - Live Preview</title>
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", Roboto, Arial, sans-serif;
|
||||
background: #f0f2f5;
|
||||
color: #222;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #0078d7;
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #0078d7;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 14px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #005fa1;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: #e5e7eb;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: #d1d5db;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.06);
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.labels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.label-card {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
background: #fafafa;
|
||||
padding: 16px;
|
||||
align-items: flex-start;
|
||||
box-shadow: 0 3px 10px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
|
||||
.label-card img {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ddd;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
|
||||
.meta {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
footer {
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
padding: 10px 0;
|
||||
background: #f0f2f5;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>ZIR - Live Preview</header>
|
||||
<main>
|
||||
<div class="controls">
|
||||
<button id="clearBtn" class="secondary">Verlauf löschen</button>
|
||||
<div style="flex:1"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div id="labels" class="labels"></div>
|
||||
</div>
|
||||
</main>
|
||||
<footer>Neue Labels werden automatisch angezeigt. Neueste oben.</footer>
|
||||
|
||||
<script>
|
||||
const { ipcRenderer } = require('electron');
|
||||
const container = document.getElementById('labels');
|
||||
const clearBtn = document.getElementById('clearBtn');
|
||||
|
||||
ipcRenderer.on('show-png', (ev, filePath) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'label-card';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = filePath + '?t=' + Date.now();
|
||||
img.alt = 'Label';
|
||||
|
||||
const info = document.createElement('div');
|
||||
info.innerHTML = `<div class="meta">Erstellt: ${new Date().toLocaleString()}</div>`;
|
||||
|
||||
card.appendChild(img);
|
||||
card.appendChild(info);
|
||||
|
||||
container.prepend(card);
|
||||
container.scrollTop = 0;
|
||||
});
|
||||
|
||||
clearBtn.addEventListener('click', () => {
|
||||
while (container.firstChild) container.removeChild(container.firstChild);
|
||||
ipcRenderer.send('clear-history');
|
||||
});
|
||||
|
||||
ipcRenderer.send('request-existing');
|
||||
ipcRenderer.on('existing-files', (ev, files) => {
|
||||
files.reverse().forEach(fp => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'label-card';
|
||||
const img = document.createElement('img');
|
||||
img.src = fp + '?t=' + Date.now();
|
||||
const info = document.createElement('div');
|
||||
info.innerHTML = `<div class="meta">Vorhanden</div>`;
|
||||
card.appendChild(img);
|
||||
card.appendChild(info);
|
||||
container.appendChild(card);
|
||||
});
|
||||
container.scrollTop = 0;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
477
main.js
477
main.js
@@ -1,244 +1,233 @@
|
||||
// main.js
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const chokidar = require("chokidar");
|
||||
const axios = require("axios");
|
||||
|
||||
let OUT_DIR; // endgültiger Eingangsordner (wird in app.whenReady gesetzt)
|
||||
let IMG_DIR; // Ausgabeordner für latest.png
|
||||
|
||||
let win;
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 800,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
win.loadFile(path.join(__dirname, "index.html"));
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
// Versuche zuerst C:\ZIR_Output (praktisch für Windows Local Port)
|
||||
const preferedRoot = "C:\\ZIR_Output";
|
||||
let dataDir = app.getPath("userData"); // fallback
|
||||
|
||||
try {
|
||||
// Versuch: C:\ZIR_Output anlegen (falls möglich)
|
||||
if (!fs.existsSync(preferedRoot)) {
|
||||
fs.mkdirSync(preferedRoot, { recursive: true });
|
||||
}
|
||||
OUT_DIR = preferedRoot;
|
||||
} catch (e) {
|
||||
// Falls es schiefgeht (Rechte), fallback in userData
|
||||
OUT_DIR = path.join(dataDir, "ZIR_Input");
|
||||
}
|
||||
|
||||
// IMG_DIR im userData (schützt vor Schreibrechten)
|
||||
IMG_DIR = path.join(dataDir, "zpl_out");
|
||||
|
||||
// Ordner anlegen, falls nicht existierend
|
||||
try { if (!fs.existsSync(OUT_DIR)) fs.mkdirSync(OUT_DIR, { recursive: true }); } catch(e) {}
|
||||
try { if (!fs.existsSync(IMG_DIR)) fs.mkdirSync(IMG_DIR, { recursive: true }); } catch(e) {}
|
||||
|
||||
console.log("ZIR Input (Eingangsordner):", OUT_DIR);
|
||||
console.log("ZIR Output (PNG-Ordner):", IMG_DIR);
|
||||
|
||||
createWindow();
|
||||
|
||||
const watcher = chokidar.watch(OUT_DIR, { ignoreInitial: true, awaitWriteFinish: {stabilityThreshold: 500, pollInterval: 100} });
|
||||
watcher.on("add", file => {
|
||||
if (!file.toLowerCase().endsWith(".zpl")) return;
|
||||
processFile(file);
|
||||
});
|
||||
watcher.on("change", file => {
|
||||
if (!file.toLowerCase().endsWith(".zpl")) return;
|
||||
processFile(file);
|
||||
});
|
||||
|
||||
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
|
||||
|
||||
/* --- Helferfunktionen --- */
|
||||
|
||||
function extractZPL(raw) {
|
||||
const start = raw.indexOf("^XA");
|
||||
const end = raw.lastIndexOf("^XZ");
|
||||
if (start >= 0 && end >= 0 && end > start) return raw.slice(start, end + 3);
|
||||
if (start >= 0) return raw.slice(start) + (raw.includes("^XZ") ? "" : "^XZ");
|
||||
return "^XA\n" + raw + "\n^XZ";
|
||||
}
|
||||
|
||||
function cleanControlChars(s) {
|
||||
return s.replace(/[\x00-\x1F]/g, '');
|
||||
}
|
||||
|
||||
function parsePWLL(zpl) {
|
||||
const pwMatch = zpl.match(/\^PW\s*?(\d+)/i) || zpl.match(/\^PW(\d+)/i);
|
||||
const llMatch = zpl.match(/\^LL\s*?(\d+)/i) || zpl.match(/\^LL(\d+)/i);
|
||||
const pw = pwMatch ? parseInt(pwMatch[1], 10) : null;
|
||||
const ll = llMatch ? parseInt(llMatch[1], 10) : null;
|
||||
return { pw, ll };
|
||||
}
|
||||
|
||||
function parseMaxFO(zpl) {
|
||||
let maxX = 0, maxY = 0;
|
||||
const re = /\^FO\s*?(\d+)\s*,\s*(\d+)/g;
|
||||
let m;
|
||||
while ((m = re.exec(zpl)) !== null) {
|
||||
const x = parseInt(m[1], 10);
|
||||
const y = parseInt(m[2], 10);
|
||||
if (!isNaN(x) && x > maxX) maxX = x;
|
||||
if (!isNaN(y) && y > maxY) maxY = y;
|
||||
}
|
||||
return { maxX, maxY };
|
||||
}
|
||||
|
||||
function dotsToInches(dots, dpi) {
|
||||
if (!dots || !dpi) return null;
|
||||
return dots / dpi;
|
||||
}
|
||||
|
||||
function fmtInches(v) {
|
||||
return (Math.round(v * 100) / 100).toString();
|
||||
}
|
||||
|
||||
async function tryLabelaryRender(zpl, printerKey, widthInches, heightInches) {
|
||||
const url = `http://api.labelary.com/v1/printers/${printerKey}/labels/${widthInches}x${heightInches}/0/`;
|
||||
try {
|
||||
const resp = await axios.post(url, zpl, {
|
||||
headers: { "Accept": "image/png" },
|
||||
responseType: "arraybuffer",
|
||||
timeout: 15000
|
||||
});
|
||||
return { ok: true, buffer: resp.data };
|
||||
} catch (err) {
|
||||
const status = err.response ? err.response.status : null;
|
||||
return { ok: false, status, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Größen & Druckeroptionen --- */
|
||||
|
||||
const STANDARD_SIZES = [
|
||||
{ w: 8.27, h: 11.69, name: "A4" },
|
||||
{ w: 8.5, h: 11, name: "Letter" },
|
||||
{ w: 4, h: 6, name: "4x6" },
|
||||
{ w: 3, h: 2, name: "3x2" },
|
||||
{ w: 4, h: 3, name: "4x3" },
|
||||
{ w: 2, h: 6, name: "2x6" },
|
||||
{ w: 1, h: 1, name: "1x1" }
|
||||
];
|
||||
|
||||
const PRINTER_OPTIONS = [
|
||||
{ key: "8dpmm", dpi: 203 },
|
||||
{ key: "12dpmm", dpi: 305 }
|
||||
];
|
||||
|
||||
/* --- Hauptverarbeitung --- */
|
||||
let busy = false;
|
||||
async function processFile(filePath) {
|
||||
if (busy) {
|
||||
setTimeout(() => processFile(filePath), 400);
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
|
||||
try {
|
||||
console.log("Verarbeite:", filePath);
|
||||
let raw = fs.readFileSync(filePath, "utf-8");
|
||||
raw = cleanControlChars(raw);
|
||||
let zpl = extractZPL(raw);
|
||||
|
||||
const { pw, ll } = parsePWLL(zpl);
|
||||
const { maxX, maxY } = parseMaxFO(zpl);
|
||||
console.log("Parsed PW/LL:", pw, ll, "maxFO:", maxX, maxY);
|
||||
|
||||
// sizesToTry aufbauen (Priorität: PW/LL -> geschätzte große Formate -> Standardgrößen)
|
||||
const sizesToTry = [];
|
||||
|
||||
if (pw && ll) {
|
||||
for (const p of PRINTER_OPTIONS) {
|
||||
const wIn = dotsToInches(pw, p.dpi);
|
||||
const hIn = dotsToInches(ll, p.dpi);
|
||||
if (wIn && hIn) {
|
||||
sizesToTry.push({ w: wIn, h: hIn, printer: p });
|
||||
sizesToTry.push({ w: hIn, h: wIn, printer: p, rotated: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const estDefaultDPI = 203;
|
||||
const estWIn = maxX ? dotsToInches(maxX + 100, estDefaultDPI) : null; // margin
|
||||
const estHIn = maxY ? dotsToInches(maxY + 100, estDefaultDPI) : null;
|
||||
|
||||
if (estWIn && estHIn) {
|
||||
if (estWIn > 6 || estHIn > 8 || estWIn > 8 || estHIn > 6) {
|
||||
for (const p of PRINTER_OPTIONS) {
|
||||
sizesToTry.push({ w: 8.27, h: 11.69, printer: p, name: "A4" });
|
||||
sizesToTry.push({ w: 11.69, h: 8.27, printer: p, name: "A4_rot" });
|
||||
sizesToTry.push({ w: 8.5, h: 11, printer: p, name: "Letter" });
|
||||
sizesToTry.push({ w: 11, h: 8.5, printer: p, name: "Letter_rot" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of PRINTER_OPTIONS) {
|
||||
for (const s of STANDARD_SIZES) {
|
||||
const exists = sizesToTry.some(x => Math.abs(x.w - s.w) < 0.01 && Math.abs(x.h - s.h) < 0.01 && x.printer.key === p.key);
|
||||
if (!exists) sizesToTry.push({ w: s.w, h: s.h, printer: p, name: s.name || `${s.w}x${s.h}` });
|
||||
}
|
||||
}
|
||||
|
||||
// Duplikate entfernen & Reihenfolge behalten
|
||||
const uniqSizes = [];
|
||||
const seen = new Set();
|
||||
for (const s of sizesToTry) {
|
||||
const key = `${s.printer.key}-${Math.round(s.w*100)}/${Math.round(s.h*100)}`;
|
||||
if (!seen.has(key)) { seen.add(key); uniqSizes.push(s); }
|
||||
}
|
||||
|
||||
// Versuche rendern
|
||||
let renderResult = null;
|
||||
for (const s of uniqSizes) {
|
||||
const wStr = fmtInches(s.w);
|
||||
const hStr = fmtInches(s.h);
|
||||
console.log(`Versuche rendern: printer=${s.printer.key} size=${wStr}x${hStr} (${s.name || ""})`);
|
||||
renderResult = await tryLabelaryRender(zpl, s.printer.key, wStr, hStr);
|
||||
if (renderResult.ok) { console.log(`Erfolg mit ${s.printer.key} ${wStr}x${hStr}`); break; }
|
||||
else { console.log(`Fehler (${renderResult.status}) mit ${s.printer.key} ${wStr}x${hStr}`); }
|
||||
}
|
||||
|
||||
if (!renderResult || !renderResult.ok) {
|
||||
console.error("Alle Render-Versuche fehlgeschlagen, Abbruch.");
|
||||
busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// PNG speichern (latest.png) — alte PNGs löschen
|
||||
const pngBuffer = renderResult.buffer;
|
||||
try {
|
||||
fs.readdirSync(IMG_DIR).forEach(f => {
|
||||
const fileToDelete = path.join(IMG_DIR, f);
|
||||
try { if (fs.lstatSync(fileToDelete).isFile()) fs.unlinkSync(fileToDelete); } catch(e){}
|
||||
});
|
||||
} catch(e){}
|
||||
|
||||
const outFile = path.join(IMG_DIR, "latest.png");
|
||||
fs.writeFileSync(outFile, pngBuffer);
|
||||
console.log("PNG erstellt:", outFile);
|
||||
|
||||
// verarbeitete ZPL-Datei löschen (optional)
|
||||
try { fs.unlinkSync(filePath); } catch(e){ console.warn("Konnte ZPL nicht löschen:", e.message); }
|
||||
|
||||
// Fenster benachrichtigen
|
||||
win.webContents.send("show-png", outFile);
|
||||
|
||||
} catch (err) {
|
||||
console.error("Fehler beim Rendern:", err && err.message ? err.message : err);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
// main.js
|
||||
const { app, BrowserWindow, Menu } = require('electron');
|
||||
const fs = require("fs");
|
||||
const fsp = require("fs").promises;
|
||||
const path = require("path");
|
||||
const chokidar = require("chokidar");
|
||||
const axios = require("axios");
|
||||
|
||||
let OUT_DIR; // Eingangsordner für ZPL
|
||||
let IMG_DIR; // Ausgabeordner für PNGs (History)
|
||||
let win;
|
||||
let watcher;
|
||||
let busy = false;
|
||||
|
||||
/* --- Fenstererstellung --- */
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 800,
|
||||
icon: path.join(__dirname, "assets/icon.ico"),
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false },
|
||||
show: false // Fenster erst zeigen, wenn fertig
|
||||
});
|
||||
|
||||
win.loadFile(path.join(__dirname, "index.html"));
|
||||
win.once("ready-to-show", () => win.show());
|
||||
|
||||
// Menü entfernen (File, Edit usw.)
|
||||
Menu.setApplicationMenu(null);
|
||||
}
|
||||
|
||||
/* --- App Start --- */
|
||||
app.whenReady().then(async () => {
|
||||
const preferedRoot = "C:\\ZIR_Output";
|
||||
let dataDir = app.getPath("userData"); // fallback
|
||||
|
||||
try {
|
||||
await fsp.mkdir(preferedRoot, { recursive: true });
|
||||
OUT_DIR = preferedRoot;
|
||||
} catch {
|
||||
OUT_DIR = path.join(dataDir, "ZIR_Input");
|
||||
await fsp.mkdir(OUT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
IMG_DIR = path.join(dataDir, "zpl_out");
|
||||
await fsp.mkdir(IMG_DIR, { recursive: true });
|
||||
|
||||
console.log("ZIR Input:", OUT_DIR);
|
||||
console.log("ZIR Output:", IMG_DIR);
|
||||
|
||||
createWindow();
|
||||
|
||||
// Watcher verzögert starten (macht Start schneller)
|
||||
setTimeout(startWatcher, 300);
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
/* --- Watcher --- */
|
||||
function startWatcher() {
|
||||
watcher = chokidar.watch(OUT_DIR, {
|
||||
ignoreInitial: true,
|
||||
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
|
||||
});
|
||||
|
||||
watcher.on("add", file => {
|
||||
if (file.toLowerCase().endsWith(".zpl")) processFile(file);
|
||||
});
|
||||
watcher.on("change", file => {
|
||||
if (file.toLowerCase().endsWith(".zpl")) processFile(file);
|
||||
});
|
||||
}
|
||||
|
||||
/* --- Helferfunktionen --- */
|
||||
function extractZPL(raw) {
|
||||
const start = raw.indexOf("^XA");
|
||||
const end = raw.lastIndexOf("^XZ");
|
||||
if (start >= 0 && end >= 0 && end > start) return raw.slice(start, end + 3);
|
||||
if (start >= 0) return raw.slice(start) + (raw.includes("^XZ") ? "" : "^XZ");
|
||||
return "^XA\n" + raw + "\n^XZ";
|
||||
}
|
||||
|
||||
function cleanControlChars(s) {
|
||||
return s.replace(/[\x00-\x1F]/g, '');
|
||||
}
|
||||
|
||||
function parsePWLL(zpl) {
|
||||
const pwMatch = zpl.match(/\^PW\s*?(\d+)/i) || zpl.match(/\^PW(\d+)/i);
|
||||
const llMatch = zpl.match(/\^LL\s*?(\d+)/i) || zpl.match(/\^LL(\d+)/i);
|
||||
const pw = pwMatch ? parseInt(pwMatch[1], 10) : null;
|
||||
const ll = llMatch ? parseInt(llMatch[1], 10) : null;
|
||||
return { pw, ll };
|
||||
}
|
||||
|
||||
function parseMaxFO(zpl) {
|
||||
let maxX = 0, maxY = 0;
|
||||
const re = /\^FO\s*?(\d+)\s*,\s*(\d+)/g;
|
||||
let m;
|
||||
while ((m = re.exec(zpl)) !== null) {
|
||||
const x = parseInt(m[1], 10);
|
||||
const y = parseInt(m[2], 10);
|
||||
if (!isNaN(x) && x > maxX) maxX = x;
|
||||
if (!isNaN(y) && y > maxY) maxY = y;
|
||||
}
|
||||
return { maxX, maxY };
|
||||
}
|
||||
|
||||
function dotsToInches(dots, dpi) {
|
||||
if (!dots || !dpi) return null;
|
||||
return dots / dpi;
|
||||
}
|
||||
|
||||
function fmtInches(v) {
|
||||
return (Math.round(v * 100) / 100).toString();
|
||||
}
|
||||
|
||||
async function tryLabelaryRender(zpl, printerKey, widthInches, heightInches) {
|
||||
const url = `http://api.labelary.com/v1/printers/${printerKey}/labels/${widthInches}x${heightInches}/0/`;
|
||||
try {
|
||||
const resp = await axios.post(url, zpl, {
|
||||
headers: { "Accept": "image/png" },
|
||||
responseType: "arraybuffer",
|
||||
timeout: 10000
|
||||
});
|
||||
return { ok: true, buffer: resp.data };
|
||||
} catch (err) {
|
||||
return { ok: false, status: err.response?.status, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Größen & Druckeroptionen --- */
|
||||
const STANDARD_SIZES = [
|
||||
{ w: 8.27, h: 11.69, name: "A4" },
|
||||
{ w: 8.5, h: 11, name: "Letter" },
|
||||
{ w: 4, h: 6, name: "4x6" },
|
||||
{ w: 3, h: 2, name: "3x2" },
|
||||
{ w: 4, h: 3, name: "4x3" },
|
||||
{ w: 2, h: 6, name: "2x6" },
|
||||
{ w: 1, h: 1, name: "1x1" }
|
||||
];
|
||||
|
||||
const PRINTER_OPTIONS = [
|
||||
{ key: "8dpmm", dpi: 203 },
|
||||
{ key: "12dpmm", dpi: 305 }
|
||||
];
|
||||
|
||||
/* --- Hauptverarbeitung --- */
|
||||
async function processFile(filePath) {
|
||||
if (busy) {
|
||||
setTimeout(() => processFile(filePath), 400);
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
|
||||
try {
|
||||
let raw = await fsp.readFile(filePath, "utf-8");
|
||||
raw = cleanControlChars(raw);
|
||||
let zpl = extractZPL(raw);
|
||||
|
||||
const { pw, ll } = parsePWLL(zpl);
|
||||
const { maxX, maxY } = parseMaxFO(zpl);
|
||||
|
||||
const sizesToTry = [];
|
||||
|
||||
if (pw && ll) {
|
||||
for (const p of PRINTER_OPTIONS) {
|
||||
const wIn = dotsToInches(pw, p.dpi);
|
||||
const hIn = dotsToInches(ll, p.dpi);
|
||||
if (wIn && hIn) {
|
||||
sizesToTry.push({ w: wIn, h: hIn, printer: p });
|
||||
sizesToTry.push({ w: hIn, h: wIn, printer: p, rotated: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const estDefaultDPI = 203;
|
||||
const estWIn = maxX ? dotsToInches(maxX + 100, estDefaultDPI) : null;
|
||||
const estHIn = maxY ? dotsToInches(maxY + 100, estDefaultDPI) : null;
|
||||
|
||||
if (estWIn && estHIn && (estWIn > 6 || estHIn > 8)) {
|
||||
for (const p of PRINTER_OPTIONS) {
|
||||
sizesToTry.push({ w: 8.27, h: 11.69, printer: p, name: "A4" });
|
||||
sizesToTry.push({ w: 11.69, h: 8.27, printer: p, name: "A4_rot" });
|
||||
sizesToTry.push({ w: 8.5, h: 11, printer: p, name: "Letter" });
|
||||
sizesToTry.push({ w: 11, h: 8.5, printer: p, name: "Letter_rot" });
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of PRINTER_OPTIONS) {
|
||||
for (const s of STANDARD_SIZES) {
|
||||
const exists = sizesToTry.some(x =>
|
||||
Math.abs(x.w - s.w) < 0.01 && Math.abs(x.h - s.h) < 0.01 && x.printer.key === p.key
|
||||
);
|
||||
if (!exists) sizesToTry.push({ w: s.w, h: s.h, printer: p, name: s.name });
|
||||
}
|
||||
}
|
||||
|
||||
const uniqSizes = [];
|
||||
const seen = new Set();
|
||||
for (const s of sizesToTry) {
|
||||
const key = `${s.printer.key}-${Math.round(s.w*100)}/${Math.round(s.h*100)}`;
|
||||
if (!seen.has(key)) { seen.add(key); uniqSizes.push(s); }
|
||||
}
|
||||
|
||||
let renderResult = null;
|
||||
for (const s of uniqSizes) {
|
||||
renderResult = await tryLabelaryRender(zpl, s.printer.key, fmtInches(s.w), fmtInches(s.h));
|
||||
if (renderResult.ok) break;
|
||||
}
|
||||
|
||||
if (!renderResult?.ok) {
|
||||
console.error("Rendering fehlgeschlagen");
|
||||
busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const pngBuffer = renderResult.buffer;
|
||||
const timestamp = Date.now();
|
||||
const outFile = path.join(IMG_DIR, `label-${timestamp}.png`);
|
||||
await fsp.writeFile(outFile, pngBuffer);
|
||||
|
||||
try { await fsp.unlink(filePath); } catch {}
|
||||
|
||||
win.webContents.send("show-png", outFile);
|
||||
|
||||
} catch (err) {
|
||||
console.error("Fehler beim Rendern:", err.message);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
13
package.json
13
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zir-viewer",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.2",
|
||||
"description": "ZIR - Virtueller ZPL Live-Preview",
|
||||
"author": "Viper",
|
||||
"main": "main.js",
|
||||
@@ -19,16 +19,23 @@
|
||||
"build": {
|
||||
"appId": "com.viper.zirviewer",
|
||||
"productName": "ZIR Viewer",
|
||||
"asar": true,
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
"files": [
|
||||
"**/*"
|
||||
"**/*",
|
||||
"!node_modules/.cache",
|
||||
"!dist/*",
|
||||
"!*.log"
|
||||
],
|
||||
"win": {
|
||||
"target": [
|
||||
"portable"
|
||||
]
|
||||
],
|
||||
"icon": "assets/icon.ico",
|
||||
"publisherName": "Viper",
|
||||
"artifactName": "ZIR-Viewer-${version}.exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user