Files
zpl-viewer-clean/main.js
2025-09-26 05:44:13 +00:00

234 lines
7.1 KiB
JavaScript

// 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;
}
}