esc-pos-encoder-ionic
Version:
Create a set of commands that can be send to any receipt printer that supports ESC/POS. Support Ionic (Typescript)
610 lines (512 loc) • 15.5 kB
JavaScript
const iconv = require('iconv-lite');
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
*
*/
constructor() {
this._reset();
}
/**
* Reset the state of the object
*
*/
_reset() {
this._buffer = [];
this._codepage = 'ascii';
this._state = {
'bold': false,
'italic': false,
'underline': false,
'hanzi': false,
};
}
/**
* Encode a string with the current code page
*
* @param {string} value String to encode
* @return {object} Encoded string as a ArrayBuffer
*
*/
_encode(value) {
return iconv.encode(value, this._codepage);
}
/**
* Add commands to the buffer
*
* @param {array} value And array of numbers, arrays, buffers or Uint8Arrays to add to the buffer
*
*/
_queue(value) {
value.forEach((item) => this._buffer.push(item));
}
/**
* Initialize the printer
*
* @return {object} Return the object, for easy chaining commands
*
*/
initialize() {
this._queue([
0x1b, 0x40,
]);
return this;
}
/**
* Change the code page
*
* @param {string} value The codepage that we set the printer to
* @return {object} Return the object, for easy chaining commands
*
*/
codepage(value) {
const codepages = {
'cp437': [0x00, false],
'cp737': [0x40, false],
'cp850': [0x02, false],
'cp775': [0x5f, false],
'cp852': [0x12, false],
'cp855': [0x3c, false],
'cp857': [0x3d, false],
'cp858': [0x13, false],
'cp860': [0x03, false],
'cp861': [0x38, false],
'cp862': [0x3e, false],
'cp863': [0x04, false],
'cp864': [0x1c, false],
'cp865': [0x05, false],
'cp866': [0x11, false],
'cp869': [0x42, false],
'cp936': [0xff, true],
'cp949': [0xfd, true],
'cp950': [0xfe, true],
'cp1252': [0x10, false],
'iso88596': [0x16, false],
'shiftjis': [0xfc, true],
'windows1250': [0x48, false],
'windows1251': [0x49, false],
'windows1252': [0x47, false],
'windows1253': [0x5a, false],
'windows1254': [0x5b, false],
'windows1255': [0x20, false],
'windows1256': [0x5c, false],
'windows1257': [0x19, false],
'windows1258': [0x5e, false],
};
let codepage;
if (!iconv.encodingExists(value)) {
throw new Error('Unknown codepage');
}
if (value in iconv.encodings) {
if (typeof iconv.encodings[value] === 'string') {
codepage = iconv.encodings[value];
} else {
codepage = value;
}
} else {
throw new Error('Unknown codepage');
}
if (typeof codepages[codepage] !== 'undefined') {
this._codepage = codepage;
this._state.hanzi = codepages[codepage][1];
this._queue([
0x1b, 0x74, codepages[codepage][0],
]);
} else {
throw new Error('Codepage not supported by printer');
}
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) {
if (wrap) {
let w = linewrap(wrap, {lineBreak: '\r\n'});
value = w(value);
}
let bytes = this._encode(value);
if (this._state.hanzi) {
this._queue([
0x1c, 0x26, bytes, 0x1c, 0x2e,
]);
} else {
this._queue([
bytes,
]);
}
return this;
}
/**
* Print a newline
*
* @return {object} Return the object, for easy chaining commands
*
*/
newline() {
this._queue([
0x0a, 0x0d,
]);
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, or 2 for double underline
* @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, 0x45, 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': 0x00,
'center': 0x01,
'right': 0x02,
};
if (value in alignments) {
this._queue([
0x1b, 0x61, alignments[value],
]);
} else {
throw new Error('Unknown alignment');
}
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) {
const symbologies = {
'upca': 0x00,
'upce': 0x01,
'ean13': 0x02,
'ean8': 0x03,
'coda39': 0x04,
'itf': 0x05,
'codabar': 0x06,
};
if (symbology in symbologies) {
let bytes = iconv.encode(value, 'ascii');
this._queue([
0x1d, 0x68, height,
0x1d, 0x77, symbology === 'code39' ? 0x02 : 0x03,
0x1d, 0x6b, symbologies[symbology],
bytes,
0x00,
]);
} else {
throw new Error('Symbology not supported by printer');
}
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, errorlevel) {
/* Force printing the print buffer and moving to a new line */
this._queue([
0x0a,
]);
/* Model */
const models = {
1: 0x31,
2: 0x32,
};
if (typeof model === 'undefined') {
model = 2;
}
if (model in models) {
this._queue([
0x1d, 0x28, 0x6b, 0x04, 0x00, 0x31, 0x41, models[model], 0x00,
]);
} else {
throw new Error('Model must be 1 or 2');
}
/* Size */
if (typeof size === 'undefined') {
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([
0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x43, size,
]);
/* Error level */
const errorlevels = {
'l': 0x30,
'm': 0x31,
'q': 0x32,
'h': 0x33,
};
if (typeof errorlevel === 'undefined') {
errorlevel = 'm';
}
if (errorlevel in errorlevels) {
this._queue([
0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x45, errorlevels[errorlevel],
]);
} else {
throw new Error('Error level must be l, m, q or h');
}
/* Data */
let bytes = iconv.encode(value, 'iso88591');
let length = bytes.length + 3;
this._queue([
0x1d, 0x28, 0x6b, length % 0xff, length / 0xff, 0x31, 0x50, 0x30, bytes,
]);
/* Print QR code */
this._queue([
0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x51, 0x30,
]);
return this;
}
/**
* Image
*
* @param {object} element an element, like a canvas or image that needs to be printed
* @param {number} width width of the image on the printer
* @param {number} height height of the image on the printer
* @param {string} algorithm the dithering algorithm for making the image black and white
* @param {number} threshold threshold for the dithering algorithm
* @return {object} Return the object, for easy chaining commands
*
*/
image(element, width, height, algorithm, threshold) {
if (width % 8 !== 0) {
throw new Error('Width must be a multiple of 8');
}
if (height % 8 !== 0) {
throw new Error('Height must be a multiple of 8');
}
if (typeof algorithm === 'undefined') {
algorithm = 'threshold';
}
if (typeof threshold === 'undefined') {
threshold = 128;
}
let canvas = createCanvas(width, height);
let context = canvas.getContext('2d');
context.drawImage(element, 0, 0, width, height);
let image = context.getImageData(0, 0, width, height);
image = Flatten.flatten(image, [0xff, 0xff, 0xff]);
switch (algorithm) {
case 'threshold': image = Dither.threshold(image, threshold); break;
case 'bayer': image = Dither.bayer(image, threshold); break;
case 'floydsteinberg': image = Dither.floydsteinberg(image); break;
case 'atkinson': image = Dither.atkinson(image); break;
}
let getPixel = (x, y) => image.data[((width * y) + x) * 4] > 0 ? 0 : 1;
let bytes = new Uint8Array((width * height) >> 3);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x = x + 8) {
let i = (y * (width >> 3)) + (x >> 3);
bytes[i] =
getPixel(x + 0, y) << 7 |
getPixel(x + 1, y) << 6 |
getPixel(x + 2, y) << 5 |
getPixel(x + 3, y) << 4 |
getPixel(x + 4, y) << 3 |
getPixel(x + 5, y) << 2 |
getPixel(x + 6, y) << 1 |
getPixel(x + 7, y);
}
}
this._queue([
0x1d, 0x76, 0x30, 0x00,
(width >> 3) & 0xff, (((width >> 3) >> 8) & 0xff),
height & 0xff, ((height >> 8) & 0xff),
bytes,
]);
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(value) {
let data = 0x00;
if (value == 'partial') {
data = 0x01;
}
this._queue([
0x1d, 0x56, data,
]);
return this;
}
/**
* Beeper sound functionality
*
* @return {object} Return the object, for easy chaining commands
*/
beeper() {
this._queue([
0x1b, 0x42, 0x05, 0x01,
]);
return this;
}
/**
* Open cash drawer
*
* @return {object} Return the object, for easy chaining commands
*/
openCashDrawer() {
this._queue([
0x1b, 0x70, 0x00,
]);
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() {
let length = 0;
this._buffer.forEach((item) => {
if (typeof item === 'number') {
length++;
} else {
length += item.length;
}
});
let 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 result;
}
}
module.exports = EscPosEncoder;