UNPKG

receiptio

Version:

Node.js printing application for receipt printers, simple and easy with receipt markdown, printer status support.

1,031 lines (1,004 loc) 83.4 kB
/* Copyright 2021 Open Foodservice System Consortium Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const fs = require('fs/promises'); const net = require('net'); const stream = require('stream'); const decoder = require('string_decoder'); const receiptline = require('receiptline'); const iconv = require('iconv-lite'); const PNG = require('pngjs').PNG; let serialport; try { serialport = require('serialport'); } catch (e) { // nothing to do } let puppeteer; try { puppeteer = require('puppeteer'); } catch (e) { // nothing to do } let sharp; try { sharp = require('sharp'); } catch (e) { // nothing to do } /** * Print receipts, get printer status, or convert to print images. * @param {string} receiptmd receipt markdown text * @param {string} [options] options ([-d destination] [-p printer] [-q] [-c chars] [-u] [-v] [-r] [-s] [-n] [-i] [-b threshold] [-g gamma] [-t timeout] [-l language]) * @returns {string} print result, printer status, or print image */ const print = (receiptmd, options) => { return new Promise(async resolve => { // options const params = parseOption(options); const printer = convertOption(params); // print timeout const t = Number(params.t); const timeout = t >= 0 && t <= 3600 ? Math.trunc(t) : 300; // destination const dest = params.d; // state let state = 0; // connection let conn; // timer let tid = 0; let iid = 0; // close and resolve const close = res => { if (state > 0) { // close port conn.destroy(); if (conn.isOpen) { // serial conn.close(); } // clear timer clearTimeout(tid); clearInterval(iid); // closed state = 0; } // resolve with value resolve(res); }; // start const start = () => { // opened state = 1; // drain let drain = true; // drain event conn.on('drain', () => { // write buffer is empty drain = true; }); // receive buffer let buf = Buffer.alloc(0); // mode let mode = ''; // data event conn.on('data', async data => { // append data buf = Buffer.concat([buf, data]); // parse response let len; do { len = buf.length; // auto detection if (mode === '') { if ((buf[0] & 0xf0) === 0xb0) { // sii: initialized response // printer control language mode = 'sii'; printer.command = 'sii'; } else if ((buf[0] & 0x91) === 0x01) { // star: automatic status if (len > 1) { const l = ((buf[0] >> 2 & 0x18) | (buf[0] >> 1 & 0x07)) + (buf[1] >> 6 & 0x02); // check length if (l <= len) { // printer control language mode = 'star'; printer.command = `star${/^(ja|ko|zh)/.test(params.l) ? 'm' : 's'}bcs${/^(ko|zh)/.test(params.l) ? '2' : ''}`; } } } else if ((buf[0] & 0x93) === 0x12) { // escpos: realtime status // printer control language mode = 'generic'; printer.command = 'generic'; } else if ((buf[0] & 0x93) === 0x10) { // escpos: automatic status if (len > 3 && (buf[1] & 0x90) === 0 && (buf[2] & 0x90) === 0 && (buf[3] & 0x90) === 0) { buf = buf.subarray(4); } } else if (buf[0] === 0x35 || buf[0] === 0x37 || buf[0] === 0x3b || buf[0] === 0x3d || buf[0] === 0x5f) { // escpos: block data const i = buf.indexOf(0); // check length if (i > 0) { // clear data buf = buf.subarray(i + 1); } } else { // other buf = buf.subarray(1); } } // process by mode switch (mode) { case 'escpos': case 'generic': switch (state) { case 1: // parse realtime status if ((buf[0] & 0x93) === 0x12) { if (params.q === 'drawer') { // cash drawer close((buf[0] & 0x97) === 0x16 ? 'drawerclosed' : 'draweropen'); } else if (params.q === 'drawer2') { // cash drawer close((buf[0] & 0x97) === 0x16 ? 'draweropen' : 'drawerclosed'); } else if ((buf[0] & 0x97) === 0x16) { // cover open close('coveropen'); } else if ((buf[0] & 0xb3) === 0x32) { // paper empty close('paperempty'); } else if ((buf[0] & 0xd3) === 0x52) { // clear timer clearTimeout(tid); clearInterval(iid); // error drain = conn.write('\x10\x05\x02', 'binary'); // DLE ENQ n // set timer tid = setTimeout(() => close('error'), 1000); } else if (params.q === 'printer') { // online close('online'); } else if (mode === 'escpos') { // clear buffer buf = buf.subarray(len); // clear timer clearTimeout(tid); clearInterval(iid); // ready state = 2; // automatic status back const asb = '\x1b@\x1da\xff'; // ESC @ GS a n // enable automatic status drain = conn.write(asb, 'binary'); // set timer tid = setTimeout(() => { // no automatic status back const recover = '\x00'.repeat(8192) + asb; // flush out interrupted commands iid = setInterval(() => { // if drain if (drain) { // retry to enable drain = conn.write(recover, 'binary'); } }, 1000); // set timer tid = setTimeout(() => close('offline'), 10000); }, 2000); } else { // get model info drain = conn.write('\x1dI\x42\x1dI\x43', 'binary'); // GS I n GS I n // clear data buf = buf.subarray(1); } } else if ((buf[0] & 0x93) === 0x10) { // automatic status if (len > 3 && (buf[1] & 0x90) === 0 && (buf[2] & 0x90) === 0 && (buf[3] & 0x90) === 0) { buf = buf.subarray(4); } } else if (buf[0] === 0x35 || buf[0] === 0x37 || buf[0] === 0x3b || buf[0] === 0x3d || buf[0] === 0x5f) { // block data const i = buf.indexOf(0); // check length if (i > 0) { // clear data const block = buf.subarray(0, i + 1); buf = buf.subarray(i + 1); if (block[0] === 0x5f) { // model info const model = block.subarray(1, block.length - 1).reduce((a, c) => a + String.fromCharCode(c), '').toLowerCase(); if (printer.command === 'generic') { if (/^(epson|citizen|fit)$/.test(model)) { // escpos thermal printer.command = model; } if (model !== 'epson') { mode = 'escpos'; } } else if (printer.command === 'epson') { if (/^tm-u/.test(model)) { // escpos impact printer.command = 'impactb'; } mode = 'escpos'; } else { // nothing to do } if (mode === 'escpos') { // clear buffer buf = buf.subarray(len); // clear timer clearTimeout(tid); clearInterval(iid); // ready state = 2; // automatic status back const asb = '\x1b@\x1da\xff'; // ESC @ GS a n // enable automatic status drain = conn.write(asb, 'binary'); // set timer tid = setTimeout(() => { // no automatic status back const recover = '\x00'.repeat(8192) + asb; // flush out interrupted commands iid = setInterval(() => { // if drain if (drain) { // retry to enable drain = conn.write(recover, 'binary'); } }, 1000); // set timer tid = setTimeout(() => close('offline'), 10000); }, 2000); } } } } else { // other buf = buf.subarray(1); } break; case 2: case 3: // check response type if (buf[0] === 0x35 || buf[0] === 0x37 || buf[0] === 0x3b || buf[0] === 0x3d || buf[0] === 0x5f) { // block data const i = buf.indexOf(0); if (i > 0) { buf = buf.subarray(i + 1); } } else if ((buf[0] & 0x90) === 0) { // status if (state === 3 && drain) { // success close('success'); } else { // other buf = buf.subarray(1); } } else if ((buf[0] & 0x93) === 0x10) { // automatic status if (len > 3) { if ((buf[1] & 0x90) === 0 && (buf[2] & 0x90) === 0 && (buf[3] & 0x90) === 0) { if ((buf[0] & 0x20) === 0x20) { // cover open close('coveropen'); } else if ((buf[2] & 0x0c) === 0x0c) { // paper empty close('paperempty'); } else if ((buf[1] & 0x2c) !== 0) { // error close('error'); } else { // normal buf = buf.subarray(4); // ready to print if (state === 2) { // clear timer clearTimeout(tid); clearInterval(iid); // printing state = 3; // write command drain = conn.write((await transform(receiptmd, printer)).replace(/^\x1b@\x1da\x00/, ''), 'binary'); // set timer tid = setTimeout(() => close('timeout'), timeout * 1000); } } } } } else { // other buf = buf.subarray(1); } break; default: break; } break; case 'sii': switch (state) { case 1: // clear buffer buf = buf.subarray(len); // ready state = 2; // enable automatic status drain = conn.write('\x1da\xff', 'binary'); // GS a n break; case 2: // check response type if ((buf[0] & 0xf0) === 0xc0) { // automatic status if (len > 7) { if (params.q === 'drawer') { // cash drawer close((buf[3] & 0xf8) === 0xd8 ? 'drawerclosed' : 'draweropen'); } else if (params.q === 'drawer2') { // cash drawer close((buf[3] & 0xf8) === 0xd8 ? 'draweropen' : 'drawerclosed'); } else if ((buf[1] & 0xf8) === 0xd8) { // cover open close('coveropen'); } else if ((buf[1] & 0xf1) === 0xd1) { // paper empty close('paperempty'); } else if ((buf[0] & 0x0b) !== 0) { // error close('error'); } else if (params.q === 'printer') { // online close('online'); } else { // normal buf = buf.subarray(8); // clear timer clearTimeout(tid); clearInterval(iid); // printing state = 3; // write command drain = conn.write((await transform(receiptmd, printer)).replace(/^\x1b@\x1da\x00/, ''), 'binary'); // set timer tid = setTimeout(() => close('timeout'), timeout * 1000); } } } else { // other buf = buf.subarray(1); } break; case 3: // check response type if ((buf[0] & 0xf0) === 0x80) { // status if (drain) { // success close('success'); } else { // other buf = buf.subarray(1); } break; } else if ((buf[0] & 0xf0) === 0xc0) { // automatic status if (len > 7) { if ((buf[1] & 0xf8) === 0xd8) { // cover open close('coveropen'); } else if ((buf[1] & 0xf1) === 0xd1) { // paper empty close('paperempty'); } else if ((buf[0] & 0x0b) !== 0) { // error close('error'); } else { // normal buf = buf.subarray(8); } } } else { // other buf = buf.subarray(1); } break; default: break; } break; case 'star': switch (state) { case 1: // parse realtime status if ((buf[0] & 0x91) === 0x01) { // calculate length if (len > 1) { const l = ((buf[0] >> 2 & 0x18) | (buf[0] >> 1 & 0x07)) + (buf[1] >> 6 & 0x02); // check length if (l <= len) { // realtime status if (params.q === 'drawer') { // cash drawer close((buf[2] & 0x04) === 0x04 ? 'draweropen' : 'drawerclosed'); } else if (params.q === 'drawer2') { // cash drawer close((buf[2] & 0x04) === 0x04 ? 'drawerclosed' : 'draweropen'); } else if ((buf[2] & 0x20) === 0x20) { // cover open close('coveropen'); } else if ((buf[5] & 0x08) === 0x08) { // paper empty close('paperempty'); } else if ((buf[3] & 0x2c) !== 0 || (buf[4] & 0x0a) !== 0) { // error close('error'); } else if (params.q === 'printer') { // online close('online'); } else { // clear buffer buf = buf.subarray(len); // clear timer clearTimeout(tid); clearInterval(iid); // ready state = 2; // write command drain = conn.write((await transform(receiptmd, printer)) .replace(/^(\x1b@)?\x1b\x1ea\x00/, '$1\x1b\x1ea\x01\x17') // (ESC @) ESC RS a n ETB .replace(/(\x1b\x1d\x03\x01\x00\x00\x04?|\x1b\x06\x01)$/, '\x17'), 'binary'); // ETB // set timer tid = setTimeout(() => close('timeout'), timeout * 1000); } } } } break; case 2: case 3: // check response type if ((buf[0] & 0xf1) === 0x21) { // calculate length const l = ((buf[0] >> 2 & 0x08) | (buf[0] >> 1 & 0x07)) + (buf[1] >> 6 & 0x02); // check length if (l <= len) { // automatic status if ((buf[2] & 0x20) === 0x20) { // cover open close('coveropen'); } else if ((buf[5] & 0x08) === 0x08) { // paper empty close('paperempty'); } else if ((buf[3] & 0x2c) !== 0 || (buf[4] & 0x0a) !== 0) { // error close('error'); } else if (state === 3 && drain) { // success close('success'); } else { // normal buf = buf.subarray(l); // printing state = 3; } } } else { // other buf = buf.subarray(1); } break; default: break; } break; default: break; } } while (buf.length > 0 && buf.length < len); }); // select mode let hello = ''; switch (printer.command) { case '': mode = ''; // auto detection hello = '\x10\x04' + (/^drawer2?$/.test(params.q) ? '\x01': '\x02') + '\x1b\x06\x01\x1b@'; // DLE EOT n ESC ACK SOH ESC @ break; case 'escpos': case 'epson': case 'citizen': case 'fit': case 'impact': case 'impactb': case 'generic': mode = 'escpos'; // ESC/POS hello = '\x10\x04' + (/^drawer2?$/.test(params.q) ? '\x01': '\x02') // DLE EOT n break; case 'sii': mode = 'sii'; // ESC/POS SII hello = '\x1b@'; // ESC @ break; case 'starsbcs': case 'starmbcs': case 'starmbcs2': case 'starlinesbcs': case 'starlinembcs': case 'starlinembcs2': case 'emustarlinesbcs': case 'emustarlinembcs': case 'emustarlinembcs2': case 'stargraphic': case 'starimpact': case 'starimpact2': case 'starimpact3': mode = 'star'; // StarPRNT, Star Line Mode, Star Graphic Mode, Star Mode on dot impact printers hello = '\x1b\x06\x01'; // ESC ACK SOH break; default: break; } // hello to printer drain = conn.write(hello, 'binary'); // set timer tid = setTimeout(() => { // no hello back const recover = '\x00'.repeat(8192) + hello; // flush out interrupted commands iid = setInterval(() => { // if drain if (drain) { // retry hello drain = conn.write(recover, 'binary'); } }, 1000); // set timer tid = setTimeout(() => close('offline'), 10000); }, 2000); }; // open port if (net.isIP(dest)) { // net conn = net.connect(9100, dest); // connect event conn.on('connect', start); // error event conn.on('error', err => { // disconnect close('disconnect'); }); } else if (await isSerialPort(dest)) { // serial const parity = { n: 'none', e: 'even', o: 'odd' }; const dev = /^([^:]*)(:((?:24|48|96|192|384|576|1152)00),?([neo]),?([78]),?([12]),?([nrx]?)$)?/i.exec(dest); const opt = { baudRate: 115200 }; if (dev[2]) { opt.baudRate = Number(dev[3]); opt.parity = parity[dev[4].toLowerCase()]; opt.dataBits = Number(dev[5]); opt.stopBits = Number(dev[6]); opt.rtscts = /r/i.test(dev[7]); opt.xon = opt.xoff = /x/i.test(dev[7]); } if ('SerialPort' in serialport) { opt.path = dev[1]; conn = new serialport.SerialPort(opt); } else { conn = new serialport(dev[1], opt); } // open event conn.on('open', start); // error event conn.on('error', err => { // disconnect close('disconnect'); }); } else if (/^\/dev\/usb\/lp/.test(dest)) { try { // device node const handle = await fs.open(dest, 'r+'); // read const rid = setInterval(async () => { const { bytesRead, buffer } = await handle.read(); if (bytesRead > 0) { conn.emit('data', buffer.subarray(0, bytesRead)); } }, 100); handle.on('close', () => clearInterval(rid)); // write conn = handle.createWriteStream(); // error event conn.on('error', err => { // disconnect close('disconnect'); }); setImmediate(start); } catch (e) { close('disconnect'); } } else if (dest) { // disconnect close('disconnect'); } else { // transform resolve(await transform(receiptmd, printer)); } }); }; /** * Create a transform stream to print receipts, get printer status, or convert to print images. * @param {string} [options] options ([-d destination] [-p printer] [-q] [-c chars] [-u] [-v] [-r] [-s] [-n] [-i] [-b threshold] [-g gamma] [-t timeout] [-l language]) * @returns {stream.Transform} transform stream */ const createPrint = options => { // options const params = parseOption(options); const printer = convertOption(params); // transform if (params.d || /^png$/.test(printer.command) || params.i && (puppeteer || sharp)) { // create transform stream return new stream.Transform({ construct(callback) { // initialize this.decoder = new decoder.StringDecoder('utf8'); this.data = ''; callback(); }, transform(chunk, encoding, callback) { // append chunk this.data += this.decoder.write(chunk); callback(); }, async flush(callback) { // convert receiptline to command const cmd = await print(this.data, options); this.push(cmd, params.d || /^(svg|text)$/.test(printer.command) ? 'utf8' : 'binary'); callback(); } }); } else { const printer = convertOption(params); // convert receiptline to command if (printer.landscape && /^(escpos|epson|sii|citizen|star[sm]bcs2?)$/.test(printer.command)) { // landscape orientation printer.command = Object.assign({}, receiptline.commands[printer.command], ...landscape[printer.command]); } return receiptline.createTransform(printer); } }; const parseOption = options => { // parameters const params = { h: false, // show help d: '', // ip address or serial/usb port of target printer o: '', // file to output (if -d option is not present) p: '', // printer control language q: '', // inquire status (printer/drawer/drawer2) c: '-1', // characters per line u: false, // upside down v: false, // landscape orientation r: '-1', // print resolution for -v s: false, // paper saving n: false, // no paper cut m: '-1,-1', // print margin i: false, // print as image b: '-1', // image thresholding g: '-1', // image gamma correction t: '-1', // print timeout l: new Intl.NumberFormat().resolvedOptions().locale // language of source file }; // arguments const argv = options ? options.split(' ') : []; // parse arguments for (let i = 0; i < argv.length; i++) { const key = argv[i]; if (/^-[huvsni]$/.test(key)) { // option without value params[key[1]] = true; } else if (/^-[dopqcrmbgtl]$/.test(key)) { // option with value if (i < argv.length - 1) { const value = argv[i + 1]; if (/^[^-]/.test(value)) { params[key[1]] = value; i++; } } // default value of status inquiry if (key[1] === 'q') { const q = params.q.toLowerCase(); params.q = /^drawer2?$/.test(q) ? q : 'printer'; } } else { // undefined option } } return params; }; const convertOption = params => { // language let l = params.l.toLowerCase(); l = l.slice(0, /^zh-han[st]/.test(l) ? 7 : 2); // command system let p = params.p.toLowerCase(); if (!/^(svg|png|te?xt|escpos|epson|sii|citizen|fit|impactb?|generic|star(line|graphic|impact[23]?)?|emustarline)$/.test(p)) { const o = params.o.toLowerCase(); const ext = /^.+\.(svg|png|txt)$/.exec(o) || [ '', 'svg' ]; p = params.d ? '' : ext[1]; } else if (/^(emu)?star(line)?$/.test(p)) { p += `${/^(ja|ko|zh)/.test(l) ? 'm' : 's'}bcs${/^(ko|zh)/.test(l) ? '2' : ''}`; } p = p.replace('txt', 'text'); // language to codepage const codepage = { 'ja': 'shiftjis', 'ko': 'ksc5601', 'zh': 'gb18030', 'zh-hans': 'gb18030', 'zh-hant': 'big5', 'th': 'tis620' }; // string to number const c = Number(params.c); const m = params.m.split(',').map(c => Number(c)); const r = Number(params.r); const b = Number(params.b); const g = Number(params.g); // options return { asImage: params.i, landscape: params.v, resolution: r === 180 ? r : 203, cpl: c >= 24 && c <= 96 ? Math.trunc(c) : 48, encoding: codepage[l] || 'multilingual', gradient: !(b >= 0 && b <= 255), gamma: g >= 0.1 && g <= 10.0 ? g : 1.0, threshold: b >= 0 && b <= 255 ? Math.trunc(b) : 128, upsideDown: params.u, spacing: !params.s, cutting: !params.n, margin: m[0] >= 0 && m[0] <= 24 ? Math.trunc(m[0]) : 0, marginRight: m[1] >= 0 && m[1] <= 24 ? Math.trunc(m[1]) : 0, command: p }; }; const transform = async (receiptmd, printer) => { // convert receiptline to png if (printer.command === 'png') { return await rasterize(receiptmd, printer, 'binary'); } // convert receiptline to image command if (printer.asImage && (puppeteer || sharp)) { receiptmd = `|{i:${await rasterize(receiptmd, printer, 'base64')}}`; return receiptline.transform(receiptmd, printer); } // convert receiptline to command if (printer.landscape && /^(escpos|epson|sii|citizen|star[sm]bcs2?)$/.test(printer.command)) { // landscape orientation printer.command = Object.assign({}, receiptline.commands[printer.command], ...landscape[printer.command]); } return receiptline.transform(receiptmd, printer); }; const rasterize = async (receiptmd, printer, encoding) => { // convert receiptline to png const c = receiptline.commands.svg.charWidth; if (puppeteer) { const display = Object.assign({}, printer, { command: 'svg' }); const svg = receiptline.transform(receiptmd, display); const w = Number(svg.match(/width="(\d+)px"/)[1]); const h = Number(svg.match(/height="(\d+)px"/)[1]); const v = { width: w, height: h }; let t = ''; if (printer.landscape) { const m = printer.margin * c || 0; const n = printer.marginRight * c || 0; v.width = h; v.height = m + w + n; t = `svg{padding-left:${m}px;padding-right:${n}px;transform-origin:top left;transform:rotate(-90deg) translateX(-${v.height}px)}`; Object.assign(printer, { cpl: Math.ceil(h / 12), margin: 0, marginRight: 0 }); } const browser = await puppeteer.launch({ defaultViewport: v, headless: 'new' }); const page = await browser.newPage(); await page.setContent(`<!DOCTYPE html><html><head><meta charset="utf-8"><style>*{margin:0;background:transparent}${t}</style></head><body>${svg}</body></html>`); const png = await page.screenshot({ encoding: encoding, omitBackground: true }); await browser.close(); return png; } else if (sharp) { const display = Object.assign({}, printer, { command: svgsharp }); const svg = receiptline.transform(receiptmd, display); const h = Number(svg.match(/height="(\d+)px"/)[1]); const x = { background: 'transparent' }; let r = 0; if (printer.landscape) { x.bottom = printer.margin * c || 0; x.top = printer.marginRight * c || 0; r = -90; Object.assign(printer, { cpl: Math.ceil(h / 12), margin: 0, marginRight: 0 }); } return (await sharp(Buffer.from(svg)).rotate(r).extend(x).toFormat('png').toBuffer()).toString(encoding); } else { return ''; } }; const svgsharp = Object.assign({}, receiptline.commands.svg, { // print text: text: function (text, encoding) { let p = this.textPosition; const attr = Object.keys(this.textAttributes).reduce((a, key) => a + ` ${key}="${this.textAttributes[key]}"`, ''); const tspan = this.arrayFrom(text, encoding).reduce((a, c) => { const q = this.measureText(c, encoding) * this.textScale; const r = Math.floor((p + q / 2) * this.charWidth / this.textScale); p += q; return a + `<tspan${attr} x="${r}">${c.replace(/[ &<>]/g, r => ({' ': '&#xa0;', '&': '&amp;', '<': '&lt;', '>': '&gt;'}[r]))}</tspan>`; }, ''); this.textElement += `<text${attr}>${tspan}</text>`; this.textPosition += this.measureText(text, encoding) * this.textScale; return ''; } }); const isSerialPort = async destination => { let list = []; if (serialport) { if ('SerialPort' in serialport) { list = await serialport.SerialPort.list(); } else { list = await serialport.list(); } } return list.findIndex(port => port.path.toLowerCase() === destination.replace(/:.*/, '').toLowerCase()) > -1; }; // shortcut const $ = String.fromCharCode; // // ESC/POS Thermal Landscape // const _escpos90 = { position: 0, content: '', height: 1, feed: 24, cpl: 48, buffer: '', // start printing: ESC @ GS a n ESC M n FS ( A pL pH fn m ESC SP n FS S n1 n2 FS . GS P x y ESC L ESC T n open: function (printer) { this.upsideDown = printer.upsideDown; this.spacing = printer.spacing; this.cutting = printer.cutting; this.gradient = printer.gradient; this.gamma = printer.gamma; this.threshold = printer.threshold; this.alignment = 0; this.left = 0; this.width = printer.cpl; this.right = 0; this.position = 0; this.content = ''; this.height = 1; this.feed = this.charWidth * (printer.spacing ? 2.5 : 2); this.cpl = printer.cpl; this.margin = printer.margin; this.marginRight = printer.marginRight; this.buffer = ''; const r = printer.resolution; return '\x1b@\x1da\x00\x1bM' + (printer.encoding === 'tis620' ? 'a' : '0') + '\x1c(A' + $(2, 0, 48, 0) + '\x1b \x00\x1cS\x00\x00\x1c.\x1dP' + $(r, r) + '\x1bL\x1bT' + $(this.upsideDown ? 3 : 1); }, // finish printing: ESC W xL xH yL yH dxL dxH dyL dyH FF GS r n close: function () { const w = this.position; const h = this.cpl * this.charWidth; const v = (this.margin + this.cpl + this.marginRight) * this.charWidth; const m = (this.upsideDown ? this.margin : this.marginRight) * this.charWidth; return '\x1bW' + $(0, 0, 0, 0, w & 255, w >> 8 & 255, v & 255, v >> 8 & 255) + ' \x1bW' + $(0, 0, m & 255, m >> 8 & 255, w & 255, w >> 8 & 255, h & 255, h >> 8 & 255) + this.buffer + '\x0c' + (this.cutting ? this.cut() : '') + '\x1dr1'; }, // set print area: area: function (left, width, right) { this.left = left; this.width = width; this.right = right; return ''; }, // set line alignment: align: function (align) { this.alignment = align; return ''; }, // set absolute print position: ESC $ nL nH absolute: function (position) { const p = (this.left + position) * this.charWidth; this.content += '\x1b$' + $(p & 255, p >> 8 & 255); return ''; }, // set relative print position: ESC \ nL nH relative: function (position) { const p = position * this.charWidth; this.content += '\x1b\\' + $(p & 255, p >> 8 & 255); return ''; }, // print horizontal rule: FS C n FS . ESC t n ... hr: function (width) { this.content += '\x1cC0\x1c.\x1bt\x01' + '\x95'.repeat(width); return ''; }, // print vertical rules: GS ! n FS C n FS . ESC t n ... vr: function (widths, height) { this.content += widths.reduce((a, w) => { const p = w * this.charWidth; return a + '\x1b\\' + $(p & 255, p >> 8 & 255) + '\x96'; }, '\x1d!' + $(height - 1) + '\x1cC0\x1c.\x1bt\x01\x96'); return ''; }, // start rules: FS C n FS . ESC t n ... vrstart: function (widths) { this.content += '\x1cC0\x1c.\x1bt\x01' + widt