@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
JavaScript
'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',