@kybarg/rpu
Version:
Node.js implementation of RPU printer protocol
685 lines (561 loc) • 15.1 kB
JavaScript
const linewrap = require('linewrap');
// const { createCanvas } = require('canvas');
// const Dither = require('canvas-dither');
// const Flatten = require('canvas-flatten');
/**
* Create a byte stream based on commands for ESC/POS printers
*/
class EscPosEncoder {
/**
* Create a new object
*
* @param {object} options Object containing configuration options
*/
constructor(options) {
this._reset(options);
}
/**
* Reset the state of the object
*
* @param {object} options Object containing configuration options
*/
_reset(options) {
this._options = Object.assign(
{
width: null,
embedded: false,
wordWrap: true,
imageMode: 'column',
codepageMapping: 'epson',
codepageCandidates: [
'cp437',
'cp858',
'cp860',
'cp861',
'cp863',
'cp865',
'cp852',
'cp857',
'cp855',
'cp866',
'cp869',
],
},
options
);
this._embedded = this._options.width && this._options.embedded;
this._buffer = [];
this._queued = [];
this._cursor = 0;
this._codepage = 'ascii';
this._state = {
codepage: 0,
align: 'left',
bold: false,
italic: false,
underline: false,
invert: false,
width: 1,
height: 1,
};
}
/**
* Encode a string with the current code page
*
* @param {string} value String to encode
* @return {object} Encoded string as a ArrayBuffer
*
*/
_encode(value) {
return Buffer.from(value, 'ascii');
}
/**
* Add commands to the queue
*
* @param {array} value Add array of numbers, arrays, buffers or Uint8Arrays to add to the buffer
*
*/
_queue(value) {
value.forEach((item) => this._queued.push(item));
}
/**
* Flush current queue to the buffer
*
*/
_flush() {
if (this._embedded) {
let indent = this._options.width - this._cursor;
if (this._state.align == 'left') {
this._queued.push(new Array(indent).fill(0x20));
}
if (this._state.align == 'center') {
const remainder = indent % 2;
indent = indent >> 1;
if (indent > 0) {
this._queued.push(new Array(indent).fill(0x20));
}
if (indent + remainder > 0) {
this._queued.unshift(new Array(indent + remainder).fill(0x20));
}
}
if (this._state.align == 'right') {
this._queued.unshift(new Array(indent).fill(0x20));
}
}
this._buffer = this._buffer.concat(this._queued);
this._queued = [];
this._cursor = 0;
}
/**
* Wrap the text while respecting the position of the cursor
*
* @param {string} value String to wrap after the width of the paper has been reached
* @param {number} position Position on which to force a wrap
* @return {array} Array with each line
*/
_wrap(value, position) {
if (position || (this._options.wordWrap && this._options.width)) {
const indent = '-'.repeat(this._cursor);
const w = linewrap(position || this._options.width, {
lineBreak: '\n',
whitespace: 'all',
});
const result = w(indent + value)
.substring(this._cursor)
.split('\n');
return result;
}
return [value];
}
/**
* Restore styles and codepages after drawing boxes or lines
*/
_restoreState() {
this.bold(this._state.bold);
this.italic(this._state.italic);
this.underline(this._state.underline);
this.invert(this._state.invert);
}
/**
* Initialize the printer
*
* @return {object} Return the object, for easy chaining commands
*
*/
initialize() {
// this._queue([
// 0x1b, 0x21,
// ]);
this._flush();
return this;
}
/**
* Set left margin
*
* @return {object} Return the object, for easy chaining commands
*
*/
marginLeft() {
// if (!nL) nL = 0
// if (!nH) nH = 0
// if (nL < 0 || nL > 255) {
// throw new Error('Magin should be <= 0 and >= 255')
// }
// if (nH < 0 || nH > 255) {
// throw new Error('Width should be <= 0 and >= 255')
// }
// this._queue([
// 0x1d, 0x4c, nL, nH,
// // 0x1d, 0x4c, .5
// // 0x1b, 0x24, 0, 2
// ])
return this;
}
/**
* Print text
*
* @param {string} value Text that needs to be printed
* @param {number} wrap Wrap text after this many positions
* @return {object} Return the object, for easy chaining commands
*
*/
text(value, wrap) {
const lines = this._wrap(value, wrap);
for (let l = 0; l < lines.length; l++) {
const bytes = this._encode(lines[l]);
this._queue([bytes]);
this._cursor += lines[l].length * this._state.width;
if (this._options.width && !this._embedded) {
this._cursor = this._cursor % this._options.width;
}
if (l < lines.length - 1) {
this.newline();
}
}
return this;
}
/**
* Print a newline
*
* @return {object} Return the object, for easy chaining commands
*
*/
newline() {
this._flush();
this._queue([0x0d, 0x0a]);
if (this._embedded) {
this._restoreState();
}
return this;
}
/**
* Print text, followed by a newline
*
* @param {string} value Text that needs to be printed
* @param {number} wrap Wrap text after this many positions
* @return {object} Return the object, for easy chaining commands
*
*/
line(value, wrap) {
this.text(value, wrap);
this.newline();
return this;
}
/**
* Underline text
*
* @param {boolean|number} value true to turn on underline, false to turn off, or 2 for double underline
* @return {object} Return the object, for easy chaining commands
*
*/
underline(value) {
if (typeof value === 'undefined') {
value = !this._state.underline;
}
this._state.underline = value;
this._queue([0x1b, 0x2d, Number(value)]);
return this;
}
/**
* Italic text
*
* @param {boolean} value true to turn on italic, false to turn off
* @return {object} Return the object, for easy chaining commands
*
*/
italic(value) {
if (typeof value === 'undefined') {
value = !this._state.italic;
}
this._state.italic = value;
this._queue([0x1b, 0x34, Number(value)]);
return this;
}
/**
* Bold text
*
* @param {boolean} value true to turn on bold, false to turn off
* @return {object} Return the object, for easy chaining commands
*
*/
bold(value) {
if (typeof value === 'undefined') {
value = !this._state.bold;
}
this._state.bold = value;
this._queue([0x1b, value ? 0x42 : 0x62]);
return this;
}
/**
* Change width of text
*
* @param {number} width The width of the text, 1 - 8
* @return {object} Return the object, for easy chaining commands
*
*/
width(width) {
if (typeof width === 'undefined') {
width = 1;
}
if (typeof width !== 'number') {
throw new Error('Width must be a number');
}
if (width < 1 || width > 8) {
throw new Error('Width must be between 1 and 8');
}
this._state.width = width;
this._queue([
0x1d,
0x21,
(this._state.height - 1) | ((this._state.width - 1) << 4),
]);
return this;
}
/**
* Change height of text
*
* @param {number} height The height of the text, 1 - 8
* @return {object} Return the object, for easy chaining commands
*
*/
height(height) {
if (typeof height === 'undefined') {
height = 1;
}
if (typeof height !== 'number') {
throw new Error('Height must be a number');
}
if (height < 1 || height > 8) {
throw new Error('Height must be between 1 and 8');
}
this._state.height = height;
this._queue([
0x1d,
0x21,
(this._state.height - 1) | ((this._state.width - 1) << 4),
]);
return this;
}
/**
* Invert text
*
* @param {boolean} value true to turn on white text on black, false to turn off
* @return {object} Return the object, for easy chaining commands
*
*/
invert(value) {
if (typeof value === 'undefined') {
value = !this._state.invert;
}
this._state.invert = value;
this._queue([0x1d, 0x42, Number(value)]);
return this;
}
/**
* Change text size
*
* @param {string} value small or normal
* @return {object} Return the object, for easy chaining commands
*
*/
size(value) {
if (value === 'small') {
value = 0x01;
} else {
value = 0x00;
}
this._queue([0x1b, 0x4d, value]);
return this;
}
/**
* Change text alignment
*
* @param {string} value left, center or right
* @return {object} Return the object, for easy chaining commands
*
*/
align(value) {
const alignments = {
left: 0x63,
center: 0x43,
};
if (value in alignments) {
this._state.align = value;
if (!this._embedded) {
this._queue([0x1b, alignments[value]]);
}
} else {
throw new Error('Unknown alignment');
}
return this;
}
/**
* Print table with End Of Line
*
* @param {[List]} data [mandatory]
* @param {[String]} encoding [optional]
* @return {[Printer]} printer [the escpos printer instance]
*/
table(data) {
const cellWidth = 40 / data.length;
let lineTxt = '';
for (let i = 0; i < data.length; i += 1) {
lineTxt += data[i].toString();
const spaces = cellWidth - data[i].toString().length;
for (let j = 0; j < spaces; j += 1) {
lineTxt += ' ';
}
}
const bytes = this._encode(lineTxt);
this._queue([bytes]);
return this;
}
/**
* Barcode
*
* @param {string} value the value of the barcode
* @param {string} symbology the type of the barcode
* @param {number} height height of the barcode
* @return {object} Return the object, for easy chaining commands
*
*/
barcode(value, symbology, height) {
if (this._embedded) {
throw new Error('Barcodes are not supported in table cells or boxes');
}
const symbologies = {
upca: 0x00,
upce: 0x01,
ean13: 0x02,
ean8: 0x03,
code39: 0x04,
coda39: 0x04 /* typo, leave here for backwards compatibility */,
itf: 0x05,
codabar: 0x06,
code93: 0x48,
code128: 0x49,
'gs1-128': 0x50,
'gs1-databar-omni': 0x51,
'gs1-databar-truncated': 0x52,
'gs1-databar-limited': 0x53,
'gs1-databar-expanded': 0x54,
'code128-auto': 0x55,
};
if (symbology in symbologies) {
const bytes = Buffer.from(value, 'ascii');
if (this._cursor != 0) {
this.newline();
}
this._queue([
0x1d,
0x68,
height,
0x1d,
0x77,
symbology === 'code39' ? 0x02 : 0x03,
]);
if (symbology == 'code128' && bytes[0] !== 0x7b) {
/* Not yet encodeded Code 128, assume data is Code B, which is similar to ASCII without control chars */
this._queue([
0x1d,
0x6b,
symbologies[symbology],
bytes.length + 2,
0x7b,
0x42,
bytes,
]);
} else if (symbologies[symbology] > 0x40) {
/* Function B symbologies */
this._queue([0x1d, 0x6b, symbologies[symbology], bytes.length, bytes]);
} else {
/* Function A symbologies */
this._queue([0x1d, 0x6b, symbologies[symbology], bytes, 0x00]);
}
} else {
throw new Error('Symbology not supported by printer');
}
this._flush();
return this;
}
/**
* QR code
*
* @param {string} value the value of the qr code
* @param {number} model model of the qrcode, either 1 or 2
* @param {number} size size of the qrcode, a value between 1 and 8
* @param {string} errorlevel the amount of error correction used, either 'l', 'm', 'q', 'h'
* @return {object} Return the object, for easy chaining commands
*
*/
qrcode(value, model, size) {
if (this._embedded) {
throw new Error('QR codes are not supported in table cells or boxes');
}
/* Force printing the print buffer and moving to a new line */
this._queue([0x1b, 0x71]);
/* Size */
// The range is 1 to 8 (0x31 to 0x38).
if (typeof size === 'undefined' || size === 0) {
size = 6;
}
if (typeof size !== 'number') {
throw new Error('Size must be a number');
}
if (size < 1 || size > 8) {
throw new Error('Size must be between 1 and 8');
}
this._queue([0x30 + size]);
/* Starting Position */
// The range is 0 to 60 (0x20 to 0x5C).
this._queue([0x20]);
// this._queue([0x20 + Math.round((60 - size * 4) / 2)])
/* Data */
const bytes = Buffer.from(value, 'ascii');
const length = bytes.length;
let len1 = 0x20;
let len2 = 0x20;
const multiplier = Math.floor(length / 95);
len1 += multiplier;
len2 += length % 95;
this._queue([len1, len2, bytes]);
this._flush();
return this;
}
/**
* Cut paper
*
* @param {string} value full or partial. When not specified a full cut will be assumed
* @return {object} Return the object, for easy chaining commands
*
*/
cut() {
if (this._embedded) {
throw new Error('Cut is not supported in table cells or boxes');
}
// WARNING: for RPU pritner thre is separate command for cutting paper
return this;
}
/**
* Add raw printer commands
*
* @param {array} data raw bytes to be included
* @return {object} Return the object, for easy chaining commands
*
*/
raw(data) {
this._queue(data);
return this;
}
/**
* Encode all previous commands
*
* @return {Uint8Array} Return the encoded bytes
*
*/
encode() {
this._flush();
let length = 0;
this._buffer.forEach((item) => {
if (typeof item === 'number') {
length++;
} else {
length += item.length;
}
});
const result = new Uint8Array(length);
let index = 0;
this._buffer.forEach((item) => {
if (typeof item === 'number') {
result[index] = item;
index++;
} else {
result.set(item, index);
index += item.length;
}
});
this._reset();
return Buffer.from(result);
}
}
module.exports = EscPosEncoder;