/** * ESC/POS - Bausteine fuer den Bondrucker * * Erzeugt die Bytefolge, die ein Thermo-Bondrucker versteht (getestet mit einem * Epson TM-T88IV). Bewusst ohne Fremdpaket: gebraucht werden ein Dutzend * Steuersequenzen, und der Drucker ist das Letzte, was wegen eines * Paket-Updates ploetzlich Buchstabensalat ausspucken soll. * * Zeichensatz: Der Drucker kennt kein UTF-8. Umgestellt wird auf Codepage 16 * (WPC1252) - die deckt die deutschen Umlaute ab und ist ab Werk in praktisch * jedem Epson-Bondrucker vorhanden. Alles, was dort fehlt, wird lesbar * ersetzt statt als Muell gedruckt. */ const ESC = 0x1b; const GS = 0x1d; // Codepage 16 = WPC1252. Die Zeichen decken sich dort mit Latin-1, mit // Ausnahme des Euro-Zeichens (0x80). Deshalb reicht die Latin-1-Kodierung // von Node plus diese eine Sonderbehandlung. const CODEPAGE_WPC1252 = 16; /** Zeichen, die WPC1252 nicht kennt, auf etwas Druckbares abbilden. */ const ERSATZ = { '€': '\x80', '„': '"', '“': '"', '”': '"', '‚': "'", '‘': "'", '’': "'", '–': '-', '—': '-', '…': '...', ' ': ' ', '\t': ' ' }; function encodeText(text) { let s = String(text == null ? '' : text); for (const [von, nach] of Object.entries(ERSATZ)) { s = s.split(von).join(nach); } // Was danach ausserhalb von Latin-1 liegt (Emoji, kyrillisch, ...), wuerde // als zufaelliges Zeichen gedruckt. Lieber ein sichtbares Fragezeichen. s = s.replace(/[^\x00-\xff]/g, '?'); return Buffer.from(s, 'latin1'); } class Bon { /** @param {number} width Zeichen je Zeile (Font A: 42 bei 80 mm, 32 bei 58 mm) */ constructor(width = 42) { this.width = width; this.parts = []; } raw(...bytes) { this.parts.push(Buffer.from(bytes)); return this; } /** Grundzustand herstellen - der Drucker merkt sich Einstellungen sonst. */ init() { return this.raw(ESC, 0x40) // ESC @ Zuruecksetzen .raw(ESC, 0x74, CODEPAGE_WPC1252); // ESC t Zeichensatz } /** @param {'left'|'center'|'right'} how */ align(how) { const n = how === 'center' ? 1 : how === 'right' ? 2 : 0; return this.raw(ESC, 0x61, n); } bold(on) { return this.raw(ESC, 0x45, on ? 1 : 0); } underline(on) { return this.raw(ESC, 0x2d, on ? 1 : 0); } /** Zeichengroesse als Vielfaches, 1..4 je Richtung. */ size(w = 1, h = 1) { const clamp = v => Math.max(1, Math.min(4, parseInt(v) || 1)) - 1; return this.raw(GS, 0x21, (clamp(w) << 4) | clamp(h)); } text(s) { this.parts.push(encodeText(s)); return this; } line(s = '') { return this.text(s + '\n'); } /** Zeile mit linkem und rechtem Block - die Trennlinie fuer Betraege. */ columns(left, right, width = this.width) { const l = String(left == null ? '' : left); const r = String(right == null ? '' : right); const luecke = width - l.length - r.length; if (luecke < 1) { // Passt nicht nebeneinander: rechts buendig in die naechste Zeile. return this.line(l).line(r.padStart(width)); } return this.line(l + ' '.repeat(luecke) + r); } /** Text mittig, ohne den Drucker auf zentriert umzustellen. */ center(s) { const t = String(s == null ? '' : s); if (t.length >= this.width) return this.line(t); return this.line(' '.repeat(Math.floor((this.width - t.length) / 2)) + t); } hr(zeichen = '-') { return this.line(zeichen.repeat(this.width)); } /** Langen Text auf die Zeilenbreite umbrechen. */ wrap(s, width = this.width) { const worte = String(s == null ? '' : s).split(/\s+/).filter(Boolean); let zeile = ''; for (const wort of worte) { if (!zeile) { zeile = wort; } else if ((zeile + ' ' + wort).length <= width) { zeile += ' ' + wort; } else { this.line(zeile); zeile = wort; } while (zeile.length > width) { this.line(zeile.slice(0, width)); zeile = zeile.slice(width); } } if (zeile) this.line(zeile); return this; } feed(n = 1) { return this.raw(ESC, 0x64, Math.max(0, Math.min(255, n))); } /** * Papier abschneiden. Vorher wird gefuettert: der Schneidekopf sitzt * einige Millimeter hinter dem Druckkopf, ohne Vorschub landet der Schnitt * mitten im Text. */ cut() { return this.feed(4).raw(GS, 0x56, 0x42, 0x00); } build() { return Buffer.concat(this.parts); } } module.exports = { Bon, encodeText };