UNPKG

@point-of-sale/receipt-printer-encoder

Version:

Create a set of commands that can be send to any receipt printer that supports ESC/POS, StarLine or StarPRNT

2,030 lines (1,699 loc) 106 kB
'use strict'; var Dither = require('canvas-dither'); var Flatten = require('canvas-flatten'); var CodepageEncoder = require('@point-of-sale/codepage-encoder'); var ImageData = require('@canvas/image-data'); var resizeImageData = require('resize-image-data'); /** * ESC/POS Language commands */ class LanguageEscPos { /** * Initialize the printer * @return {Array} Array of bytes to send to the printer */ initialize() { return [ { type: 'initialize', payload: [0x1b, 0x40], }, { type: 'character-mode', value: 'single byte', payload: [0x1c, 0x2e], }, { type: 'font', value: 'A', payload: [0x1b, 0x4d, 0x00], }, ]; } /** * Change the font * @param {string} value Font type ('A', 'B', or more) * @return {Array} Array of bytes to send to the printer */ font(value) { const type = value.charCodeAt(0) - 0x41; return [ { type: 'font', value, payload: [0x1b, 0x4d, type], }, ]; } /** * Change the alignment * @param {string} value Alignment value ('left', 'center', 'right') * @return {Array} Array of bytes to send to the printer */ align(value) { let align = 0x00; if (value === 'center') { align = 0x01; } else if (value === 'right') { align = 0x02; } return [ { type: 'align', value, payload: [0x1b, 0x61, align], }, ]; } /** * Generate a barcode * @param {string} value Value to encode * @param {string|number} symbology Barcode symbology * @param {object} options Configuration object * @return {Array} Array of bytes to send to the printer */ barcode(value, symbology, options) { const result = []; const symbologies = { 'upca': 0x00, 'upce': 0x01, 'ean13': 0x02, 'ean8': 0x03, 'code39': 0x04, 'coda39': 0x04, /* typo, leave here for backwards compatibility */ 'itf': 0x05, 'interleaved-2-of-5': 0x05, 'nw-7': 0x06, 'codabar': 0x06, 'code93': 0x48, 'code128': 0x49, 'gs1-128': 0x48, 'gs1-databar-omni': 0x4b, 'gs1-databar-truncated': 0x4c, 'gs1-databar-limited': 0x4d, 'gs1-databar-expanded': 0x4e, 'code128-auto': 0x4f, }; if (typeof symbology === 'string' && typeof symbologies[symbology] === 'undefined') { throw new Error(`Symbology '${symbology}' not supported by language`); } /* Calculate segment width */ if (options.width < 1 || options.width > 3) { throw new Error('Width must be between 1 and 3'); } let width = options.width + 1; if (symbology === 'itf') { width = options.width * 2; } if (symbology === 'gs1-128' || symbology === 'gs1-databar-omni' || symbology === 'gs1-databar-truncated' || symbology === 'gs1-databar-limited' || symbology === 'gs1-databar-expanded') { width = options.width; } /* Set barcode options */ result.push( { type: 'barcode', property: 'height', value: options.height, payload: [0x1d, 0x68, options.height], }, { type: 'barcode', property: 'width', value: options.width, payload: [0x1d, 0x77, width], }, { type: 'barcode', property: 'text', value: options.text, payload: [0x1d, 0x48, options.text ? 0x02 : 0x00], }, ); /* Encode barcode */ if (symbology == 'code128' && !value.startsWith('{')) { value = '{B' + value; } if (symbology == 'gs1-128') { value = value.replace(/[()*]/g, ''); } const bytes = CodepageEncoder.encode(value, 'ascii'); const identifier = typeof symbology === 'string' ? symbologies[symbology] : symbology; if (identifier > 0x40) { /* Function B symbologies */ result.push( { type: 'barcode', value: {symbology: symbology, data: value}, payload: [0x1d, 0x6b, identifier, bytes.length, ...bytes], }, ); } else { /* Function A symbologies */ result.push( { type: 'barcode', value: {symbology: symbology, data: value}, payload: [0x1d, 0x6b, identifier, ...bytes, 0x00], }, ); } return result; } /** * Generate a QR code * @param {string} value Value to encode * @param {object} options Configuration object * @return {Array} Array of bytes to send to the printer */ qrcode(value, options) { const result = []; /* Model */ if (typeof options.model === 'number') { const models = { 1: 0x31, 2: 0x32, }; if (options.model in models) { result.push( { type: 'qrcode', property: 'model', value: options.model, payload: [0x1d, 0x28, 0x6b, 0x04, 0x00, 0x31, 0x41, models[options.model], 0x00], }, ); } else { throw new Error('Model must be 1 or 2'); } } /* Size */ if (typeof options.size !== 'number') { throw new Error('Size must be a number'); } if (options.size < 1 || options.size > 8) { throw new Error('Size must be between 1 and 8'); } result.push( { type: 'qrcode', property: 'size', value: options.size, payload: [0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x43, options.size], }, ); /* Error level */ const errorlevels = { 'l': 0x30, 'm': 0x31, 'q': 0x32, 'h': 0x33, }; if (options.errorlevel in errorlevels) { result.push( { type: 'qrcode', property: 'errorlevel', value: options.errorlevel, payload: [0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x45, errorlevels[options.errorlevel]], }, ); } else { throw new Error('Error level must be l, m, q or h'); } /* Data */ const bytes = CodepageEncoder.encode(value, 'iso8859-1'); const length = bytes.length + 3; result.push( { type: 'qrcode', property: 'data', value, payload: [0x1d, 0x28, 0x6b, length & 0xff, (length >> 8) & 0xff, 0x31, 0x50, 0x30, ...bytes], }, ); /* Print QR code */ result.push( { type: 'qrcode', command: 'print', payload: [0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x51, 0x30], }, ); return result; } /** * Generate a PDF417 code * @param {string} value Value to encode * @param {object} options Configuration object * @return {Array} Array of bytes to send to the printer */ pdf417(value, options) { const result = []; /* Columns */ if (typeof options.columns !== 'number') { throw new Error('Columns must be a number'); } if (options.columns !== 0 && (options.columns < 1 || options.columns > 30)) { throw new Error('Columns must be 0, or between 1 and 30'); } result.push( { type: 'pdf417', property: 'columns', value: options.columns, payload: [0x1d, 0x28, 0x6b, 0x03, 0x00, 0x30, 0x41, options.columns], }, ); /* Rows */ if (typeof options.rows !== 'number') { throw new Error('Rows must be a number'); } if (options.rows !== 0 && (options.rows < 3 || options.rows > 90)) { throw new Error('Rows must be 0, or between 3 and 90'); } result.push( { type: 'pdf417', property: 'rows', value: options.rows, payload: [0x1d, 0x28, 0x6b, 0x03, 0x00, 0x30, 0x42, options.rows], }, ); /* Width */ if (typeof options.width !== 'number') { throw new Error('Width must be a number'); } if (options.width < 2 || options.width > 8) { throw new Error('Width must be between 2 and 8'); } result.push( { type: 'pdf417', property: 'width', value: options.width, payload: [0x1d, 0x28, 0x6b, 0x03, 0x00, 0x30, 0x43, options.width], }, ); /* Height */ if (typeof options.height !== 'number') { throw new Error('Height must be a number'); } if (options.height < 2 || options.height > 8) { throw new Error('Height must be between 2 and 8'); } result.push( { type: 'pdf417', property: 'height', value: options.height, payload: [0x1d, 0x28, 0x6b, 0x03, 0x00, 0x30, 0x44, options.height], }, ); /* Error level */ if (typeof options.errorlevel !== 'number') { throw new Error('Errorlevel must be a number'); } if (options.errorlevel < 0 || options.errorlevel > 8) { throw new Error('Errorlevel must be between 0 and 8'); } result.push( { type: 'pdf417', property: 'errorlevel', value: options.errorlevel, payload: [0x1d, 0x28, 0x6b, 0x04, 0x00, 0x30, 0x45, 0x30, options.errorlevel + 0x30], }, ); /* Model: standard or truncated */ result.push( { type: 'pdf417', property: 'truncated', value: !!options.truncated, payload: [0x1d, 0x28, 0x6b, 0x03, 0x00, 0x30, 0x46, options.truncated ? 0x01 : 0x00], }, ); /* Data */ const bytes = CodepageEncoder.encode(value, 'ascii'); const length = bytes.length + 3; result.push( { type: 'pdf417', property: 'data', value, payload: [0x1d, 0x28, 0x6b, length & 0xff, (length >> 8) & 0xff, 0x30, 0x50, 0x30, ...bytes], }, ); /* Print PDF417 code */ result.push( { type: 'pdf417', command: 'print', payload: [0x1d, 0x28, 0x6b, 0x03, 0x00, 0x30, 0x51, 0x30], }, ); return result; } /** * Encode an image * @param {ImageData} image ImageData object * @param {number} width Width of the image * @param {number} height Height of the image * @param {string} mode Image encoding mode ('column' or 'raster') * @return {Array} Array of bytes to send to the printer */ image(image, width, height, mode) { const result = []; const getPixel = (x, y) => x < width && y < height ? (image.data[((width * y) + x) * 4] > 0 ? 0 : 1) : 0; const getColumnData = (width, height) => { const data = []; for (let s = 0; s < Math.ceil(height / 24); s++) { const bytes = new Uint8Array(width * 3); for (let x = 0; x < width; x++) { for (let c = 0; c < 3; c++) { for (let b = 0; b < 8; b++) { bytes[(x * 3) + c] |= getPixel(x, (s * 24) + b + (8 * c)) << (7 - b); } } } data.push(bytes); } return data; }; const getRowData = (width, height) => { const bytes = new Uint8Array((width * height) >> 3); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x = x + 8) { for (let b = 0; b < 8; b++) { bytes[(y * (width >> 3)) + (x >> 3)] |= getPixel(x + b, y) << (7 - b); } } } return bytes; }; /* Encode images with ESC * */ if (mode == 'column') { result.push( { type: 'line-spacing', value: '24 dots', payload: [0x1b, 0x33, 0x24], }, ); getColumnData(width, height).forEach((bytes) => { result.push( { type: 'image', property: 'data', value: 'column', width, height: 24, payload: [0x1b, 0x2a, 0x21, width & 0xff, (width >> 8) & 0xff, ...bytes, 0x0a], }, ); }); result.push( { type: 'line-spacing', value: 'default', payload: [0x1b, 0x32], }, ); } /* Encode images with GS v */ if (mode == 'raster') { result.push( { type: 'image', command: 'data', value: 'raster', width, height, payload: [ 0x1d, 0x76, 0x30, 0x00, (width >> 3) & 0xff, (((width >> 3) >> 8) & 0xff), height & 0xff, ((height >> 8) & 0xff), ...getRowData(width, height), ], }, ); } return result; } /** * Cut the paper * @param {string} value Cut type ('full' or 'partial') * @return {Array} Array of bytes to send to the printer */ cut(value) { let data = 0x00; if (value == 'partial') { data = 0x01; } return [ { type: 'cut', payload: [0x1d, 0x56, data], }, ]; } /** * Send a pulse to the cash drawer * @param {number} device Device number * @param {number} on Pulse ON time * @param {number} off Pulse OFF time * @return {Array} Array of bytes to send to the printer */ pulse(device, on, off) { if (typeof device === 'undefined') { device = 0; } if (typeof on === 'undefined') { on = 100; } if (typeof off === 'undefined') { off = 500; } on = Math.min(500, Math.round(on / 2)); off = Math.min(500, Math.round(off / 2)); return [ { type: 'pulse', payload: [0x1b, 0x70, device ? 1 : 0, on & 0xff, off & 0xff], }, ]; } /** * Enable or disable bold text * @param {boolean} value Enable or disable bold text, optional, default toggles between states * @return {Array} Array of bytes to send to the printer */ bold(value) { let data = 0x00; if (value) { data = 0x01; } return [ 0x1b, 0x45, data, ]; } /** * Enable or disable underline text * @param {boolean} value Enable or disable underline text, optional, default toggles between states * @return {Array} Array of bytes to send to the printer */ underline(value) { let data = 0x00; if (value) { data = 0x01; } return [ 0x1b, 0x2d, data, ]; } /** * Enable or disable italic text * @param {boolean} value Enable or disable italic text, optional, default toggles between states * @return {Array} Array of bytes to send to the printer */ italic(value) { let data = 0x00; if (value) { data = 0x01; } return [ 0x1b, 0x34, data, ]; } /** * Enable or disable inverted text * @param {boolean} value Enable or disable inverted text, optional, default toggles between states * @return {Array} Array of bytes to send to the printer */ invert(value) { let data = 0x00; if (value) { data = 0x01; } return [ 0x1d, 0x42, data, ]; } /** * Change text size * @param {number} width Width of the text (1-8) * @param {number} height Height of the text (1-8) * @return {Array} Array of bytes to send to the printer */ size(width, height) { return [ 0x1d, 0x21, (height - 1) | (width - 1) << 4, ]; } /** * Change the codepage * @param {number} value Codepage value * @return {Array} Array of bytes to send to the printer */ codepage(value) { return [ 0x1b, 0x74, value, ]; } /** * Flush the printers line buffer * @return {Array} Array of bytes to send to the printer */ flush() { return []; } } /** * StarPRNT Language commands */ class LanguageStarPrnt { /** * Initialize the printer * @return {Array} Array of bytes to send to the printer */ initialize() { return [ { type: 'initialize', payload: [0x1b, 0x40, 0x18], }, ]; } /** * Change the font * @param {string} value Font type ('A', 'B' or 'C') * @return {Array} Array of bytes to send to the printer */ font(value) { let type = 0x00; if (value === 'B') { type = 0x01; } if (value === 'C') { type = 0x02; } return [ { type: 'font', value, payload: [0x1b, 0x1e, 0x46, type], }, ]; } /** * Change the alignment * @param {string} value Alignment value ('left', 'center', 'right') * @return {Array} Array of bytes to send to the printer */ align(value) { let align = 0x00; if (value === 'center') { align = 0x01; } else if (value === 'right') { align = 0x02; } return [ { type: 'align', value, payload: [0x1b, 0x1d, 0x61, align], }, ]; } /** * Generate a barcode * @param {string} value Value to encode * @param {string|number} symbology Barcode symbology * @param {object} options Configuration object * @return {Array} Array of bytes to send to the printer */ barcode(value, symbology, options) { const result = []; const symbologies = { 'upce': 0x00, 'upca': 0x01, 'ean8': 0x02, 'ean13': 0x03, 'code39': 0x04, 'itf': 0x05, 'interleaved-2-of-5': 0x05, 'code128': 0x06, 'code93': 0x07, 'nw-7': 0x08, 'codabar': 0x08, 'gs1-128': 0x09, 'gs1-databar-omni': 0x0a, 'gs1-databar-truncated': 0x0b, 'gs1-databar-limited': 0x0c, 'gs1-databar-expanded': 0x0d, }; if (typeof symbology === 'string' && typeof symbologies[symbology] === 'undefined') { throw new Error(`Symbology '${symbology}' not supported by language`); } if (options.width < 1 || options.width > 3) { throw new Error('Width must be between 1 and 3'); } /* Selecting mode A, B or C for Code128 is not supported for StarPRNT, so ignore it and let the printer choose */ if (symbology === 'code128' && value.startsWith('{')) { value = value.slice(2); } /* Encode the barcode value */ const bytes = CodepageEncoder.encode(value, 'ascii'); const identifier = typeof symbology === 'string' ? symbologies[symbology] : symbology; result.push( { type: 'barcode', value: {symbology: symbology, data: value, width: options.width, height: options.height, text: options.text}, payload: [ 0x1b, 0x62, identifier, options.text ? 0x02 : 0x01, options.width, options.height, ...bytes, 0x1e, ], }, ); return result; } /** * Generate a QR code * @param {string} value Value to encode * @param {object} options Configuration object * @return {Array} Array of bytes to send to the printer */ qrcode(value, options) { const result = []; /* Model */ const models = { 1: 0x01, 2: 0x02, }; if (options.model in models) { result.push( { type: 'qrcode', property: 'model', value: options.model, payload: [0x1b, 0x1d, 0x79, 0x53, 0x30, models[options.model]], }, ); } else { throw new Error('Model must be 1 or 2'); } /* Size */ if (typeof options.size !== 'number') { throw new Error('Size must be a number'); } if (options.size < 1 || options.size > 8) { throw new Error('Size must be between 1 and 8'); } result.push( { type: 'qrcode', property: 'size', value: options.size, payload: [0x1b, 0x1d, 0x79, 0x53, 0x32, options.size], }, ); /* Error level */ const errorlevels = { 'l': 0x00, 'm': 0x01, 'q': 0x02, 'h': 0x03, }; if (options.errorlevel in errorlevels) { result.push( { type: 'qrcode', property: 'errorlevel', value: options.errorlevel, payload: [0x1b, 0x1d, 0x79, 0x53, 0x31, errorlevels[options.errorlevel]], }, ); } else { throw new Error('Error level must be l, m, q or h'); } /* Data */ const bytes = CodepageEncoder.encode(value, 'iso8859-1'); const length = bytes.length; result.push( { type: 'qrcode', property: 'data', value, payload: [ 0x1b, 0x1d, 0x79, 0x44, 0x31, 0x00, length & 0xff, (length >> 8) & 0xff, ...bytes, ], }, ); /* Print QR code */ result.push( { type: 'qrcode', command: 'print', payload: [0x1b, 0x1d, 0x79, 0x50], }, ); return result; } /** * Generate a PDF417 code * @param {string} value Value to encode * @param {object} options Configuration object * @return {Array} Array of bytes to send to the printer */ pdf417(value, options) { const result = []; /* Columns and Rows */ if (typeof options.columns !== 'number') { throw new Error('Columns must be a number'); } if (options.columns !== 0 && (options.columns < 1 || options.columns > 30)) { throw new Error('Columns must be 0, or between 1 and 30'); } if (typeof options.rows !== 'number') { throw new Error('Rows must be a number'); } if (options.rows !== 0 && (options.rows < 3 || options.rows > 90)) { throw new Error('Rows must be 0, or between 3 and 90'); } result.push( { type: 'pdf417', value: `rows: ${options.rows}, columns: ${options.columns}`, payload: [0x1b, 0x1d, 0x78, 0x53, 0x30, 0x01, options.rows, options.columns], }, ); /* Width */ if (typeof options.width !== 'number') { throw new Error('Width must be a number'); } if (options.width < 2 || options.width > 8) { throw new Error('Width must be between 2 and 8'); } result.push( { type: 'pdf417', property: 'width', value: options.width, payload: [0x1b, 0x1d, 0x78, 0x53, 0x32, options.width], }, ); /* Height */ if (typeof options.height !== 'number') { throw new Error('Height must be a number'); } if (options.height < 2 || options.height > 8) { throw new Error('Height must be between 2 and 8'); } result.push( { type: 'pdf417', property: 'height', value: options.height, payload: [0x1b, 0x1d, 0x78, 0x53, 0x33, options.height], }, ); /* Error level */ if (typeof options.errorlevel !== 'number') { throw new Error('Errorlevel must be a number'); } if (options.errorlevel < 0 || options.errorlevel > 8) { throw new Error('Errorlevel must be between 0 and 8'); } result.push( { type: 'pdf417', property: 'errorlevel', value: options.errorlevel, payload: [0x1b, 0x1d, 0x78, 0x53, 0x31, options.errorlevel], }, ); /* Data */ const bytes = CodepageEncoder.encode(value, 'ascii'); const length = bytes.length; result.push( { type: 'pdf417', property: 'data', value, payload: [ 0x1b, 0x1d, 0x78, 0x44, length & 0xff, (length >> 8) & 0xff, ...bytes, ], }, ); /* Print PDF417 code */ result.push( { type: 'pdf417', command: 'print', payload: [0x1b, 0x1d, 0x78, 0x50], }, ); return result; } /** * Encode an image * @param {ImageData} image ImageData object * @param {number} width Width of the image * @param {number} height Height of the image * @param {string} mode Image encoding mode (value is ignored) * @return {Array} Array of bytes to send to the printer */ image(image, width, height, mode) { const result = []; const getPixel = (x, y) => typeof image.data[((width * y) + x) * 4] === 'undefined' || image.data[((width * y) + x) * 4] > 0 ? 0 : 1; result.push( { type: 'line-spacing', value: '24 dots', payload: [0x1b, 0x30], }, ); for (let s = 0; s < height / 24; s++) { const y = s * 24; const bytes = new Uint8Array(width * 3); for (let x = 0; x < width; x++) { const i = x * 3; bytes[i] = getPixel(x, y + 0) << 7 | getPixel(x, y + 1) << 6 | getPixel(x, y + 2) << 5 | getPixel(x, y + 3) << 4 | getPixel(x, y + 4) << 3 | getPixel(x, y + 5) << 2 | getPixel(x, y + 6) << 1 | getPixel(x, y + 7); bytes[i + 1] = getPixel(x, y + 8) << 7 | getPixel(x, y + 9) << 6 | getPixel(x, y + 10) << 5 | getPixel(x, y + 11) << 4 | getPixel(x, y + 12) << 3 | getPixel(x, y + 13) << 2 | getPixel(x, y + 14) << 1 | getPixel(x, y + 15); bytes[i + 2] = getPixel(x, y + 16) << 7 | getPixel(x, y + 17) << 6 | getPixel(x, y + 18) << 5 | getPixel(x, y + 19) << 4 | getPixel(x, y + 20) << 3 | getPixel(x, y + 21) << 2 | getPixel(x, y + 22) << 1 | getPixel(x, y + 23); } result.push( { type: 'image', property: 'data', value: 'column', width, height: 24, payload: [ 0x1b, 0x58, width & 0xff, (width >> 8) & 0xff, ...bytes, 0x0a, 0x0d, ], }, ); } result.push( { type: 'line-spacing', value: 'default', payload: [0x1b, 0x7a, 0x01], }, ); return result; } /** * Cut the paper * @param {string} value Cut type ('full' or 'partial') * @return {Array} Array of bytes to send to the printer */ cut(value) { let data = 0x00; if (value == 'partial') { data = 0x01; } return [ { type: 'cut', payload: [0x1b, 0x64, data], }, ]; } /** * Send a pulse to the cash drawer * @param {number} device Device number * @param {number} on Pulse ON time * @param {number} off Pulse OFF time * @return {Array} Array of bytes to send to the printer */ pulse(device, on, off) { if (typeof device === 'undefined') { device = 0; } if (typeof on === 'undefined') { on = 200; } if (typeof off === 'undefined') { off = 200; } on = Math.min(127, Math.round(on / 10)); off = Math.min(127, Math.round(off / 10)); return [ { type: 'pulse', payload: [0x1b, 0x07, on & 0xff, off & 0xff, device ? 0x1a : 0x07], }, ]; } /** * Enable or disable bold text * @param {boolean} value Enable or disable bold text, optional, default toggles between states * @return {Array} Array of bytes to send to the printer */ bold(value) { let data = 0x46; if (value) { data = 0x45; } return [ 0x1b, data, ]; } /** * Enable or disable underline text * @param {boolean} value Enable or disable underline text, optional, default toggles between states * @return {Array} Array of bytes to send to the printer */ underline(value) { let data = 0x00; if (value) { data = 0x01; } return [ 0x1b, 0x2d, data, ]; } /** * Enable or disable italic text * @param {boolean} value Enable or disable italic text, optional, default toggles between states * @return {Array} Array of bytes to send to the printer */ italic(value) { return []; } /** * Enable or disable inverted text * @param {boolean} value Enable or disable inverted text, optional, default toggles between states * @return {Array} Array of bytes to send to the printer */ invert(value) { let data = 0x35; if (value) { data = 0x34; } return [ 0x1b, data, ]; } /** * Change text size * @param {number} width Width of the text (1-8) * @param {number} height Height of the text (1-8) * @return {Array} Array of bytes to send to the printer */ size(width, height) { return [ 0x1b, 0x69, height - 1, width - 1, ]; } /** * Change the codepage * @param {number} value Codepage value * @return {Array} Array of bytes to send to the printer */ codepage(value) { return [ 0x1b, 0x1d, 0x74, value, ]; } /** * Flush the printers line buffer * @return {Array} Array of bytes to send to the printer */ flush() { return [ { type: 'print-mode', value: 'page', payload: [0x1b, 0x1d, 0x50, 0x30], }, { type: 'print-mode', value: 'line', payload: [0x1b, 0x1d, 0x50, 0x31], }, ]; } } /** * Store and manage text styles */ class TextStyle { #default = { bold: false, italic: false, underline: false, invert: false, width: 1, height: 1, }; #current; #callback; /** * Create a new TextStyle object * * @param {object} options Object containing configuration options */ constructor(options) { this.#current = structuredClone(this.#default); this.#callback = options.callback || (() => {}); } /** * Return commands to get to the default style from the current style * * @return {array} Array of modified properties */ store() { const result = []; const properties = new Map(); for (const property in this.#current) { if (this.#current[property] !== this.#default[property]) { if (property === 'width' || property === 'height') { properties.set('size', {width: this.#default.width, height: this.#default.height}); } else { properties.set(property, this.#default[property]); } } } for (const property of properties) { result.push({ type: 'style', property: property[0], value: property[1], }); } return result; } /** * Return commands to get to the current style from the default style * * @return {array} Array of modified properties */ restore() { const result = []; const properties = new Map(); for (const property in this.#current) { if (this.#current[property] !== this.#default[property]) { if (property === 'width' || property === 'height') { properties.set('size', {width: this.#current.width, height: this.#current.height}); } else { properties.set(property, this.#current[property]); } } } for (const property of properties) { result.push({ type: 'style', property: property[0], value: property[1], }); } return result; } /** * Set the bold property * * @param {boolean} value Is bold enabled, or not? */ set bold(value) { if (value !== this.#current.bold) { this.#current.bold = value; this.#callback({ type: 'style', property: 'bold', value, }); } } /** * Get the bold property * * @return {boolean} Is bold enabled, or not? */ get bold() { return this.#current.bold; } /** * Set the italic property * * @param {boolean} value Is italic enabled, or not? */ set italic(value) { if (value !== this.#current.italic) { this.#current.italic = value; this.#callback({ type: 'style', property: 'italic', value, }); } } /** * Get the italic property * * @return {boolean} Is italic enabled, or not? */ get italic() { return this.#current.italic; } /** * Set the underline property * * @param {boolean} value Is underline enabled, or not? */ set underline(value) { if (value !== this.#current.underline) { this.#current.underline = value; this.#callback({ type: 'style', property: 'underline', value, }); } } /** * Get the underline property * * @return {boolean} Is underline enabled, or not? */ get underline() { return this.#current.underline; } /** * Set the invert property * * @param {boolean} value Is invert enabled, or not? */ set invert(value) { if (value !== this.#current.invert) { this.#current.invert = value; this.#callback({ type: 'style', property: 'invert', value, }); } } /** * Get the invert property * * @return {boolean} Is invert enabled, or not? */ get invert() { return this.#current.invert; } /** * Set the width property * * @param {number} value The width of a character */ set width(value) { if (value !== this.#current.width) { this.#current.width = value; this.#callback({ type: 'style', property: 'size', value: {width: this.#current.width, height: this.#current.height}, }); } } /** * Get the width property * * @return {number} The width of a character */ get width() { return this.#current.width; } /** * Set the height property * * @param {number} value The height of a character */ set height(value) { if (value !== this.#current.height) { this.#current.height = value; this.#callback({ type: 'style', property: 'size', value: {width: this.#current.width, height: this.#current.height}, }); } } /** * Get the height property * * @return {number} The height of a character */ get height() { return this.#current.height; } } /** * Wrap text into lines of a specified width. */ class TextWrap { /** * Static function to wrap text into lines of a specified width. * * @param {string} value Text to wrap * @param {object} options Object containing configuration options * @return {array} Array of lines */ static wrap(value, options) { const result = []; let line = []; let length = options.indent || 0; const width = options.width || 1; const columns = options.columns || 42; const lines = String(value).split(/\r\n|\n/g); for (const value of lines) { const chunks = value.match(/[^\s-]+?-\b|\S+|\s+|\r\n?|\n/g) || ['~~empty~~']; for (const chunk of chunks) { if (chunk === '~~empty~~') { result.push(line); line = []; length = 0; continue; } /* The word does not fit on the line */ if (length + (chunk.length * width) > columns) { /* The word is longer than the line */ if (chunk.length * width > columns) { /* Calculate the remaining space on the line */ const remaining = columns - length; /* Split the word into pieces */ const letters = chunk.split(''); let piece; const pieces = []; /* If there are at least 8 position remaining, break early */ if (remaining > 8 * width) { piece = letters.splice(0, Math.floor(remaining / width)).join(''); line.push(piece); result.push(line); line = []; length = 0; } /* The remaining letters can be split into pieces the size of the width */ while ((piece = letters.splice(0, Math.floor(columns / width))).length) { pieces.push(piece.join('')); } for (const piece of pieces) { if (length + (piece.length * width) > columns) { result.push(line); line = []; length = 0; } line.push(piece); length += piece.length * width; } continue; } /* Word fits on the next line */ result.push(line); line = []; length = 0; } /* Check if we are whitespace */ if (chunk.match(/\s+/) && length == 0) { continue; } line.push(chunk); length += chunk.length * width; } if (line.length > 0) { result.push(line); line = []; length = 0; } } for (let i = 0; i < result.length; i++) { result[i] = result[i].join(''); if (i < result.length - 1) { result[i] = result[i].trimEnd(); } } return result; } } /** * Compose lines of text and commands */ class LineComposer { #embedded; #columns; #align; #callback; #cursor = 0; #stored; #buffer = []; /** * Create a new LineComposer object * * @param {object} options Object containing configuration options */ constructor(options) { this.#embedded = options.embedded || false; this.#columns = options.columns || 42; this.#align = options.align || 'left'; this.#callback = options.callback || (() => {}); this.style = new TextStyle({ callback: (value) => { this.add(value, 0); }, }); this.#stored = this.style.store(); } /** * Add text to the line, potentially wrapping it * * @param {string} value Text to add to the line * @param {number} codepage Codepage to use for the text */ text(value, codepage) { const lines = TextWrap.wrap(value, {columns: this.#columns, width: this.style.width, indent: this.#cursor}); for (let i = 0; i < lines.length; i++) { if (lines[i].length) { /* Add the line to the buffer */ this.add({type: 'text', value: lines[i], codepage}, lines[i].length * this.style.width); /* If it is not the last line, flush the buffer */ if (i < lines.length - 1) { this.flush(); } } else { /* In case the line is empty, flush the buffer */ this.flush({forceNewline: true}); } } } /** * Add spaces to the line * * @param {number} size Number of spaces to add to the line */ space(size) { this.add({type: 'space', size}, size); } /** * Add raw bytes to to the line * * @param {array} value Array of bytes to add to the line * @param {number} length Length in characters of the value */ raw(value, length) { this.add({type: 'raw', payload: value}, length || 0); } /** * Add an item to the line buffer, potentially flushing it * * @param {object} value Item to add to the line buffer * @param {number} length Length in characters of the value */ add(value, length) { if (value instanceof Array) { for (const item of value) { this.add(item); } this.#cursor += length || 0; return; } length = length || 0; if (length + this.#cursor > this.#columns) { this.flush(); } this.#cursor += length; this.#buffer = this.#buffer.concat(value); } /** * Move the cursor to the end of the line, forcing a flush * with the next item to add to the line buffer */ end() { this.#cursor = this.#columns; } /** * Fetch the contents of line buffer * * @param {options} options Options for flushing the buffer * @return {array} Array of items in the line buffer */ fetch(options) { /* Unless forced keep style changes for the next line */ if (this.#cursor === 0 && !options.forceNewline && !options.forceFlush) { return []; } /* Check the alignment of the current line */ const align = { current: this.#align, next: null, }; for (let i = 0; i < this.#buffer.length - 1; i++) { if (this.#buffer[i].type === 'align' && !this.#buffer[i].payload) { align.current = this.#buffer[i].value; } } /* Check the last item in the buffer, to see if it changes the alignment, then save it for the next line */ if (this.#buffer.length) { const last = this.#buffer[this.#buffer.length - 1]; if (last.type === 'align' && !last.payload) { align.next = last.value; } } this.#align = align.current; /* Create a clean buffer without alignment changes */ const buffer = this.#buffer.filter((item) => item.type !== 'align' || item.payload); /* Fetch the contents of the line buffer */ let result = []; const restore = this.style.restore(); const store = this.style.store(); if (this.#cursor === 0 && (options.ignoreAlignment || !this.#embedded)) { result = this.#merge([ ...this.#stored, ...buffer, ...store, ]); } else { if (this.#align === 'right') { let last; /* Find index of last text or space element */ for (let i = buffer.length - 1; i >= 0; i--) { if (buffer[i].type === 'text' || buffer[i].type === 'space') { last = i; break; } } /* Remove trailing spaces from lines */ if (typeof last === 'number') { if (buffer[last].type === 'space' && buffer[last].size > this.style.width) { buffer[last].size -= this.style.width; this.#cursor -= this.style.width; } if (buffer[last].type === 'text' && buffer[last].value.endsWith(' ')) { buffer[last].value = buffer[last].value.slice(0, -1); this.#cursor -= this.style.width; } } result = this.#merge([ {type: 'space', size: this.#columns - this.#cursor}, ...this.#stored, ...buffer, ...store, ]); } if (this.#align === 'center') { const left = (this.#columns - this.#cursor) >> 1; result = this.#merge([ {type: 'space', size: left}, ...this.#stored, ...buffer, ...store, {type: 'space', size: this.#embedded ? this.#columns - this.#cursor - left : 0}, ]); } if (this.#align === 'left') { result = this.#merge([ ...this.#stored, ...buffer, ...store, {type: 'space', size: this.#embedded ? this.#columns - this.#cursor : 0}, ]); } } this.#stored = restore; this.#buffer = []; this.#cursor = 0; if (result.length === 0 && options.forceNewline) { result.push({type: 'empty'}); } if (align.next) { this.#align = align.next; } return result; } /** * Flush the contents of the line buffer * * @param {options} options Options for flushing the buffer */ flush(options) { options = Object.assign({ forceNewline: false, forceFlush: false, ignoreAlignment: false, }, options || {}); const result = this.fetch(options); if (result.length) { this.#callback(result); } } /** * Merge text items and spaces in the line buffer * * @param {array} items Array of items * @return {array} Array of merged items */ #merge(items) { const result = []; let last = -1; for (let item of items) { if (item.type === 'space') { if (item.size === 0) { continue; } item = {type: 'text', value: ' '.repeat(item.size), codepage: null}; } if (item.type === 'text') { /* Check if we can merge the text with the last item */ const allowMerge = last >= 0 && result[last].type === 'text' && ( result[last].codepage === item.codepage || result[last].codepage === null || item.codepage === null ); if (allowMerge) { result[last].value += item.value; result[last].codepage = result[last].codepage || item.codepage; continue; } result.push(item); last++; } else if (item.type === 'style' && item.property === 'size') { const allowMerge = last >= 0 && result[last].type === 'style' && result[last].property === 'size'; if (allowMerge) { result[last].value = item.value; continue; } result.push(item); last++; } else { result.push(item); last++; } } return result; } /** * Get the current position of the cursor * * @return {number} Current position of the cursor */ get cursor() { return this.#cursor; } /** * Set the alignment of the current line * * @param {string} value Text alignment, can be 'left', 'center', or 'right' */ set align(value) { this.add({type: 'align', value}, 0); } /** * Get the alignment of the current line * * @return {string} Text alignment, can be 'left', 'center', or 'right' */ get align() { let align = this.#align; for (let i = 0; i < this.#buffer.length; i++) { if (this.#buffer[i].type === 'align') { align = this.#buffer[i].value; } } return align; } /** * Set the number of columns of the current line * * @param {number} value columns of the line */ set columns(value) { this.#columns = value; } /** * Get the number of columns of the current line * * @return {number} columns of the line */ get columns() { return this.#columns; } } const codepageMappings = { 'esc-pos': { 'bixolon/legacy': ['cp437','epson/katakana','cp850','cp860','cp863','cp865',,,,,,,,,,,,,,'cp858'], 'bixolon': ['cp437','epson/katakana','cp850','cp860','cp863','cp865',,,,,,,,,,,'windows1252','cp866','cp852','cp858',,'cp862','cp864','thai42','windows1253','windows1254','windows1257',,'windows1251','cp737','cp775','thai14','bixolon/hebrew','windows1255','thai11','thai18','cp885','cp857','iso8859-7','thai16','windows1256','windows1258','khmer',,,,'bixolon/cp866','windows1250',,'tcvn3','tcvn3capitals','viscii'], 'citizen': ['cp437','epson/katakana','cp858','cp860','cp863','cp865','cp852','cp866','cp857',,,,,,,,'windows1252',,,,,'thai11',,,,,'thai13',,,,'tcvn3','tcvn3capitals','windows1258',,,,,,,,'cp864'], 'epson/legacy': ['cp437','epson/katakana','cp850','cp860','cp863','cp865',,,,,,,,,,,'windows1252','cp866','cp852','cp858'], 'epson': ['cp437','epson/katakana','cp850','cp860','cp863','cp865',,,,,,'cp851','cp853','cp857','cp737','iso8859-7','windows1252','cp866','cp852','cp858','thai42','thai11',,,,,'thai13',,,,'tcvn3','tcvn3capitals','cp720','cp775','cp855','cp861','cp862','cp864','cp869','epson/iso8859-2','iso8859-15','cp1098','cp774','cp772','cp1125','windows1250','windows1251','windows1253','windows1254','windows1255','windows1256','windows1257','windows1258','rk1048'], 'fujitsu': ['cp437','epson/katakana','cp850','cp860','cp863','cp865',,,'cp857',,,,,,,,'windows1252','cp866','cp852','cp858',,,,,,,'thai13',,,,,,,,,,,,,,'cp864'], 'hp': ['cp437','cp850','cp852','cp860','cp863','cp865','cp858','cp866','windows1252','cp862','cp737','cp874','cp857','windows1251','windows1255','rk1048'], 'metapace': ['cp437','epson/katakana','cp850','cp860','cp863','cp865',,,,,,,,,,,,,,'cp858'], 'mpt': ['cp437',,'cp850','cp860','cp863','cp865','windows1251','cp866','cp3021','cp3012'], 'pos-5890': ['cp437','epson/katakana','cp850','cp860','cp863','cp865','iso8859-1',,'cp862',,,,,,,,'windows1252','cp866','cp852','cp858',,,,'windows1251','cp737','windows1257',,'windows1258','cp864',,,,'windows1255',,,,,,,,,,,,,,,,,,,,,,,,'cp861',,,,'cp855','cp857',,,,'cp851','cp869',,'cp772','cp774',,,'windows1250',,'cp3840',,'cp3843','cp3844','cp3845','cp3846','cp3847','cp3848',,'cp771','cp3001','cp3002','cp3011','cp3012',,'cp3041','windows1253','windows1254','windows1256','cp720',,'cp775'], 'pos-8360': ['cp437','epson/katakana','cp850','cp860','cp863','cp865','iso8859-1','windows1253','cp862',,,,,,,,'windows1252','cp866','cp852','cp858',,'latvian',,'windows1251','cp737','windows1257',,'windows1258','cp864',,,'pos8360/hebrew','windows1255',,,,,,,,,,,,,,,,,,,,,,,,'cp861',,,,'cp855','cp857',,,,'cp851','cp869',,'cp772','cp774',,,'windows1250',,'cp3840',,'cp3843','cp3844','cp3845','cp3846','cp3847','cp3848',,'cp771','cp3001','cp3002','cp3011','cp3012',,,,'windows1254',