diff --git a/cmd/web/main.go b/cmd/web/main.go
index 1da1295..d0b38de 100644
--- a/cmd/web/main.go
+++ b/cmd/web/main.go
@@ -223,6 +223,7 @@ const indexHTML = `
.preview-info { margin-top: 0.75rem; font-size: 0.8rem; color: #a0aec0; min-height: 1.2em; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem; }
+ .grid3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; margin-bottom: 1rem; }
label { display: block; font-size: 0.8rem; font-weight: 600; color: #a0aec0; margin-bottom: 0.35rem; }
input[type=number], select {
width: 100%;
@@ -237,6 +238,22 @@ const indexHTML = `
}
input[type=number]:focus, select:focus { border-color: #6366f1; }
+ /* unit toggle */
+ .unit-row {
+ display: flex; align-items: center; gap: 0.5rem;
+ margin-bottom: 0.75rem;
+ }
+ .unit-row span { font-size: 0.8rem; color: #a0aec0; font-weight: 600; }
+ .toggle-group { display: flex; border: 1px solid #3a4060; border-radius: 6px; overflow: hidden; }
+ .toggle-group button {
+ flex: 1; background: #252a3d; color: #a0aec0;
+ border: none; border-radius: 0; padding: 0.3rem 0.8rem;
+ font-size: 0.8rem; font-weight: 600; cursor: pointer; width: auto;
+ transition: background .15s, color .15s;
+ }
+ .toggle-group button.active { background: #6366f1; color: #fff; }
+ .sub-label { font-size: 0.7rem; color: #718096; font-weight: 400; margin-top: 0.15rem; }
+
.checks { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1.5rem; }
.checks label {
display: flex; align-items: center; gap: 0.4rem;
@@ -264,10 +281,7 @@ const indexHTML = `
button:hover { background: #4f52d0; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
- .output {
- margin-top: 1.5rem;
- display: none;
- }
+ .output { margin-top: 1.5rem; display: none; }
.output-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 0.5rem;
@@ -285,10 +299,7 @@ const indexHTML = `
color: #7dd3fc; font-family: monospace; font-size: 0.75rem;
padding: 0.75rem; resize: vertical; outline: none;
}
- .dl-btn {
- margin-top: 0.75rem;
- background: #2d6a4f;
- }
+ .dl-btn { margin-top: 0.75rem; background: #2d6a4f; }
.dl-btn:hover { background: #215040; }
.error { color: #fc8181; font-size: 0.85rem; margin-top: 0.75rem; display: none; }
.spinner {
@@ -313,14 +324,31 @@ const indexHTML = `
+
+
+
Dimensions in
+
+
+
+
+
Printer DPI
+
+
+
-
-
+
+
+
-
-
+
+
+
@@ -366,7 +394,69 @@ const spinner = document.getElementById('spinner');
const output = document.getElementById('output');
const zplOutput = document.getElementById('zplOutput');
const errorEl = document.getElementById('error');
+
let selectedFile = null;
+let currentUnit = 'px'; // 'px' or 'cm'
+let origPxW = 0, origPxH = 0; // image native pixel dimensions
+
+function getDPI() { return parseInt(document.getElementById('dpi').value, 10); }
+
+// cm ↔ px helpers
+function pxToCm(px) { return +(px * 2.54 / getDPI()).toFixed(3); }
+function cmToPx(cm) { return Math.round(cm * getDPI() / 2.54); }
+
+function setUnit(unit) {
+ const wEl = document.getElementById('width');
+ const hEl = document.getElementById('height');
+ const curW = parseFloat(wEl.value) || 0;
+ const curH = parseFloat(hEl.value) || 0;
+
+ if (unit === 'cm' && currentUnit === 'px') {
+ wEl.step = '0.01';
+ hEl.step = '0.01';
+ wEl.value = curW > 0 ? pxToCm(curW) : 0;
+ hEl.value = curH > 0 ? pxToCm(curH) : 0;
+ } else if (unit === 'px' && currentUnit === 'cm') {
+ wEl.step = '1';
+ hEl.step = '1';
+ wEl.value = curW > 0 ? cmToPx(curW) : 0;
+ hEl.value = curH > 0 ? cmToPx(curH) : 0;
+ }
+ currentUnit = unit;
+ document.getElementById('btnPx').classList.toggle('active', unit === 'px');
+ document.getElementById('btnCm').classList.toggle('active', unit === 'cm');
+ updateLabels();
+}
+
+function updateLabels() {
+ const unit = currentUnit;
+ document.getElementById('labelW').textContent = 'Width (' + unit + ')' + (unit === 'px' ? ' — 0 = auto' : '');
+ document.getElementById('labelH').textContent = 'Height (' + unit + ')' + (unit === 'px' ? ' — 0 = auto' : '');
+ updateSubLabels();
+}
+
+function updateSubLabels() {
+ if (!origPxW) return;
+ if (currentUnit === 'px') {
+ document.getElementById('subW').textContent = pxToCm(origPxW) + ' cm at ' + getDPI() + ' dpi';
+ document.getElementById('subH').textContent = pxToCm(origPxH) + ' cm at ' + getDPI() + ' dpi';
+ } else {
+ document.getElementById('subW').textContent = origPxW + ' px native';
+ document.getElementById('subH').textContent = origPxH + ' px native';
+ }
+}
+
+// Recalc sub-labels when DPI changes
+document.getElementById('dpi').addEventListener('change', () => {
+ if (currentUnit === 'cm') {
+ // re-fill cm fields based on original px
+ if (origPxW) {
+ document.getElementById('width').value = pxToCm(origPxW);
+ document.getElementById('height').value = pxToCm(origPxH);
+ }
+ }
+ updateSubLabels();
+});
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('over'); });
@@ -388,10 +478,25 @@ function setFile(file) {
fetch('/preview', { method: 'POST', body: fd })
.then(r => r.ok ? r.json() : Promise.reject())
.then(info => {
- previewInfo.textContent = file.name + ' — ' + info.width + ' × ' + info.height + ' px';
- // Pre-fill dimensions
- document.getElementById('width').value = info.width;
- document.getElementById('height').value = info.height;
+ origPxW = info.width;
+ origPxH = info.height;
+
+ const wEl = document.getElementById('width');
+ const hEl = document.getElementById('height');
+ if (currentUnit === 'px') {
+ wEl.value = origPxW;
+ hEl.value = origPxH;
+ } else {
+ wEl.value = pxToCm(origPxW);
+ hEl.value = pxToCm(origPxH);
+ }
+
+ const cmW = pxToCm(origPxW), cmH = pxToCm(origPxH);
+ previewInfo.textContent =
+ file.name + ' — ' + origPxW + ' × ' + origPxH + ' px' +
+ ' (' + cmW + ' × ' + cmH + ' cm at ' + getDPI() + ' dpi)';
+
+ updateSubLabels();
convertBtn.disabled = false;
})
.catch(() => {
@@ -400,6 +505,16 @@ function setFile(file) {
});
}
+// Returns pixel values regardless of current unit
+function getPixelDimensions() {
+ const w = parseFloat(document.getElementById('width').value) || 0;
+ const h = parseFloat(document.getElementById('height').value) || 0;
+ if (currentUnit === 'cm') {
+ return { w: cmToPx(w), h: cmToPx(h) };
+ }
+ return { w: Math.round(w), h: Math.round(h) };
+}
+
convertBtn.addEventListener('click', async () => {
if (!selectedFile) return;
errorEl.style.display = 'none';
@@ -407,11 +522,12 @@ convertBtn.addEventListener('click', async () => {
spinner.style.display = 'block';
output.style.display = 'none';
+ const { w, h } = getPixelDimensions();
const edits = [...document.querySelectorAll('input[name=edit]:checked')].map(c => c.value);
const fd = new FormData();
fd.append('file', selectedFile);
- fd.append('width', document.getElementById('width').value);
- fd.append('height', document.getElementById('height').value);
+ fd.append('width', w);
+ fd.append('height', h);
fd.append('type', document.getElementById('enctype').value);
fd.append('edit', edits.join(','));
@@ -446,6 +562,9 @@ document.getElementById('dlBtn').addEventListener('click', () => {
a.download = (selectedFile ? selectedFile.name.replace(/\.[^.]+$/, '') : 'label') + '.zpl';
a.click();
});
+
+// init labels
+updateLabels();