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
JavaScript
/*
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 => ({' ': ' ', '&': '&', '<': '<', '>': '>'}[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