Fix text/image alignment, add dithering, extend Classic BT to Windows

This commit is contained in:
Hamza
2026-02-27 20:06:29 +01:00
parent c1fd34f145
commit b1ff403594
9 changed files with 756 additions and 42 deletions

View File

@@ -310,6 +310,9 @@
<div>Drop an image here, or click to browse</div>
<input type="file" id="fileInput" accept="image/*">
</div>
<label style="font-size:13px;color:var(--text2);display:flex;align-items:center;gap:6px">
<input type="checkbox" id="imgDither" checked> Floyd-Steinberg dithering
</label>
<div class="preview-wrap" id="imgPreviewWrap" style="display:none">
<canvas id="imgPreview"></canvas>
</div>
@@ -393,6 +396,7 @@ let notifyBuf = [];
let notifyResolve = null;
let connected = false;
let uploadedCanvas = null; // prepared 1-bit canvas from image upload
let lastUploadedImg = null; // original Image element for re-processing
// ---- DOM refs ----
const $ = id => document.getElementById(id);
@@ -654,9 +658,11 @@ function textToCanvas(text, fontSize) {
}
function prepareImageCanvas(img) {
// Resize to 96px wide, proportional height
// Resize to 96px wide, proportional height, cap to label length
const scale = PRINTHEAD_PX / img.width;
const newH = Math.round(img.height * scale);
const maxH = getLabelHeight();
let newH = Math.round(img.height * scale);
if (newH > maxH) newH = maxH;
const c = document.createElement('canvas');
c.width = PRINTHEAD_PX;
@@ -664,7 +670,8 @@ function prepareImageCanvas(img) {
const ctx = c.getContext('2d');
ctx.drawImage(img, 0, 0, PRINTHEAD_PX, newH);
return threshold(c);
const useDither = $('imgDither').checked;
return useDither ? floydSteinbergDither(c) : threshold(c);
}
function threshold(canvas) {
@@ -685,6 +692,52 @@ function threshold(canvas) {
return canvas;
}
function floydSteinbergDither(canvas) {
// Floyd-Steinberg error-diffusion dithering.
// Same algorithm as PrinterImageProcessor.ditherFloydSteinberg() in the
// decompiled Fichero APK (7/16, 3/16, 5/16, 1/16 error distribution).
const ctx = canvas.getContext('2d');
const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
const px = data.data;
const w = canvas.width;
const h = canvas.height;
// Build grayscale float buffer
const gray = new Float32Array(w * h);
for (let i = 0; i < w * h; i++) {
const j = i * 4;
gray[i] = 0.299 * px[j] + 0.587 * px[j + 1] + 0.114 * px[j + 2];
}
// Dither
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const idx = y * w + x;
const old = gray[idx];
const val = old < 128 ? 0 : 255;
gray[idx] = val;
const err = old - val;
if (x + 1 < w) gray[idx + 1] += err * 7 / 16;
if (y + 1 < h) {
if (x - 1 >= 0) gray[idx + w - 1] += err * 3 / 16;
gray[idx + w] += err * 5 / 16;
if (x + 1 < w) gray[idx + w + 1] += err * 1 / 16;
}
}
}
// Write back
for (let i = 0; i < w * h; i++) {
const v = gray[i] < 128 ? 0 : 255;
const j = i * 4;
px[j] = px[j + 1] = px[j + 2] = v;
px[j + 3] = 255;
}
ctx.putImageData(data, 0, 0);
return canvas;
}
function canvasToRaster(canvas) {
// Pack pixels into MSB-first bytes. 1 = black, 0 = white.
const ctx = canvas.getContext('2d');
@@ -834,10 +887,8 @@ function handleFile(file) {
reader.onload = () => {
const img = new Image();
img.onload = () => {
uploadedCanvas = prepareImageCanvas(img);
showPreview(uploadedCanvas, 'imgPreview');
$('imgPreviewWrap').style.display = '';
$('imgPrintBtn').disabled = !connected;
lastUploadedImg = img;
reprocessImage();
log(`Image loaded: ${img.width}x${img.height} -> ${uploadedCanvas.width}x${uploadedCanvas.height}`);
};
img.src = reader.result;
@@ -845,6 +896,14 @@ function handleFile(file) {
reader.readAsDataURL(file);
}
function reprocessImage() {
if (!lastUploadedImg) return;
uploadedCanvas = prepareImageCanvas(lastUploadedImg);
showPreview(uploadedCanvas, 'imgPreview');
$('imgPreviewWrap').style.display = '';
$('imgPrintBtn').disabled = !connected;
}
// ---- Event listeners ----
$('textInput').addEventListener('input', updateTextPreview);
@@ -856,6 +915,10 @@ $('fileInput').addEventListener('change', e => {
if (e.target.files[0]) handleFile(e.target.files[0]);
});
// Re-process image when dither toggle or label length changes
$('imgDither').addEventListener('change', () => { if (lastUploadedImg) reprocessImage(); });
$('labelLength').addEventListener('change', () => { if (lastUploadedImg) reprocessImage(); });
// Dropzone click -> trigger file input
$('dropzone').addEventListener('click', () => $('fileInput').click());