2 Commits
Author SHA1 Message Date
jessikitty cdc1bc4264 Dither badge photos before drawing them 2026-09-07 05:34:41 +00:00
jessikitty a9e07b324d Wire printer.js to the native QL protocol module 2026-09-07 05:34:25 +00:00
+46 -26
View File
@@ -6,6 +6,8 @@ import { execFile } from 'node:child_process';
import { createCanvas, loadImage, GlobalFonts } from '@napi-rs/canvas'; import { createCanvas, loadImage, GlobalFonts } from '@napi-rs/canvas';
import config from './config.js'; import config from './config.js';
import { photoAbsolutePath } from './photos.js'; import { photoAbsolutePath } from './photos.js';
import { ditherPhoto } from './printing/dither.js';
import { printPng, printerHealth } from './printing/ql-print.js';
/** /**
* Printing happens on the server, not in the kiosk browser. * Printing happens on the server, not in the kiosk browser.
@@ -113,6 +115,17 @@ async function drawBadge(ctx, widthDots, heightDots, visit, site, accent, startY
} }
const photoSize = photo ? Math.round(unit * (portrait ? 0.52 : 0.5)) : 0; const photoSize = photo ? Math.round(unit * (portrait ? 0.52 : 0.5)) : 0;
// Thermal heads print pure black or nothing, so a photo has to be reduced to
// 1-bit before it lands on the canvas — otherwise the plane conversion
// thresholds it into a solid blob. Done once here rather than at each draw
// site, and at exactly photoSize: rescaling a dithered image resamples the
// dot pattern back into greys and undoes the whole thing.
//
// Skipped on the measuring pass, which never paints.
if (photo && photoSize > 0 && startY !== null) {
photo = ditherPhoto(photo, photoSize);
}
let cursorY = startY === null ? pad : startY; let cursorY = startY === null ? pad : startY;
let textLeft = pad; let textLeft = pad;
let textWidth = widthDots - pad * 2; let textWidth = widthDots - pad * 2;
@@ -330,33 +343,24 @@ export async function printBadge(visit, site) {
if (!isConfigured(site)) throw new Error('Server printing is not turned on for this site.'); if (!isConfigured(site)) throw new Error('Server printing is not turned on for this site.');
const png = await renderBadgePng(visit, site); const png = await renderBadgePng(visit, site);
const file = path.join(os.tmpdir(), `badge-${crypto.randomBytes(6).toString('hex')}.png`);
fs.writeFileSync(file, png);
const port = Number(site.printer_port) || 9100; const port = Number(site.printer_port) || 9100;
const target = `tcp://${site.printer_host}:${port}`; const target = `tcp://${site.printer_host}:${port}`;
try { // The roll decides whether the job is two-colour, not the accent setting.
await runBrotherQl( // A DK-22251 roll refuses a monochrome job even when the badge has no red
[ // on it, so labelFor() is passed straight through and ql-print maps it.
'--backend', 'network', const result = await printPng(png, {
'--model', site.printer_model || 'QL-820NWB', host: site.printer_host,
'--printer', target, port,
'print', label: labelFor(site),
'--label', labelFor(site), });
file,
], note(site.id, result.ok, result.message);
config.printing.timeoutMs if (!result.ok) throw new Error(result.message);
);
note(site.id, true, `Printed to ${site.printer_host}`); // confirmed is false when the printer accepted the bytes but never said the
return { ok: true, target }; // label came out. Over the network that is the normal case, not a fault.
} catch (err) { return { ok: true, confirmed: Boolean(result.confirmed), target };
const friendly = explainPrintError(err.message, site.printer_host);
note(site.id, false, friendly);
throw new Error(friendly);
} finally {
fs.rm(file, { force: true }, () => {});
}
} }
/** A sample badge, for checking the printer and the layout without a real visit. */ /** A sample badge, for checking the printer and the layout without a real visit. */
@@ -375,7 +379,23 @@ export function sampleVisit(site) {
} }
export function available() { export function available() {
return new Promise((resolve) => { // Kept for callers that only ask "can this server print at all?". The
execFile(config.printing.command, ['--version'], (err) => resolve(!err)); // external brother_ql binary is no longer involved, so the answer is always
// yes; use health(site) to ask about a specific printer.
return Promise.resolve(true);
}
/**
* Reachability and, where the printer will say, roll state.
*
* ready is tri-state: true, false, or null meaning "reachable but it won't
* tell us". Null is the normal answer over the network — show it as unknown
* in the admin console rather than green or red.
*/
export function health(site) {
return printerHealth({
host: site?.printer_host,
port: Number(site?.printer_port) || 9100,
label: labelFor(site),
}); });
} }