node-red-contrib-tcp-escpos 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/nodes/node.html +29 -0
- package/nodes/node.js +104 -6
- package/nodes/utilities/dither.js +41 -0
- package/package.json +3 -2
package/nodes/node.html
CHANGED
|
@@ -9,12 +9,29 @@
|
|
|
9
9
|
defaults: {
|
|
10
10
|
name: { value: '' },
|
|
11
11
|
host: { value: '' },
|
|
12
|
+
dotsPerLine: { value: 576 },
|
|
12
13
|
payloadType: { value: 'text' },
|
|
13
14
|
payload: { value: "I'm Mr. Printer.\nLook at me!" },
|
|
15
|
+
cutAfter: { value: true },
|
|
14
16
|
},
|
|
15
17
|
label: function () {
|
|
16
18
|
return this.name || this.host || 'tcp escpos'
|
|
17
19
|
},
|
|
20
|
+
oneditprepare: function () {
|
|
21
|
+
const $type = $('#node-input-payloadType')
|
|
22
|
+
const $cut = $('#node-input-cutAfter').closest('.form-row')
|
|
23
|
+
|
|
24
|
+
function refresh() {
|
|
25
|
+
if ($type.val() === 'buffer') {
|
|
26
|
+
$cut.hide()
|
|
27
|
+
} else {
|
|
28
|
+
$cut.show()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
$type.on('change', refresh)
|
|
33
|
+
refresh()
|
|
34
|
+
},
|
|
18
35
|
})
|
|
19
36
|
</script>
|
|
20
37
|
<script type="text/html" data-template-name="tcp escpos">
|
|
@@ -26,6 +43,12 @@
|
|
|
26
43
|
<label for="node-input-host"><i class="fa fa-dot-circle-o"></i> Host</label>
|
|
27
44
|
<input type="text" id="node-input-host" placeholder="192.168.1.101:9100" />
|
|
28
45
|
</div>
|
|
46
|
+
<div class="form-row">
|
|
47
|
+
<label for="node-input-dotsPerLine"
|
|
48
|
+
><i class="fa fa-ruler-horizontal"></i> Dots per line</label
|
|
49
|
+
>
|
|
50
|
+
<input type="number" id="node-input-dotsPerLine" placeholder="576" />
|
|
51
|
+
</div>
|
|
29
52
|
<div class="form-row">
|
|
30
53
|
<label for="node-input-payloadType"
|
|
31
54
|
><i class="fa fa-play-circle"></i> Type</label
|
|
@@ -42,6 +65,12 @@
|
|
|
42
65
|
>
|
|
43
66
|
<textarea id="node-input-payload"></textarea>
|
|
44
67
|
</div>
|
|
68
|
+
<div class="form-row">
|
|
69
|
+
<label for="node-input-cutAfter"
|
|
70
|
+
><i class="fa fa-dot-scissors"></i> Cut After</label
|
|
71
|
+
>
|
|
72
|
+
<input type="checkbox" id="node-input-cutAfter" />
|
|
73
|
+
</div>
|
|
45
74
|
</script>
|
|
46
75
|
<script type="text/html" data-help-name="tcp escpos">
|
|
47
76
|
<p>Typescript example Node-RED node</p>
|
package/nodes/node.js
CHANGED
|
@@ -12,8 +12,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
|
15
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
15
16
|
const node_net_1 = __importDefault(require("node:net"));
|
|
16
17
|
const node_url_1 = require("node:url");
|
|
18
|
+
const skia_canvas_1 = require("skia-canvas");
|
|
19
|
+
const dither_1 = require("./utilities/dither");
|
|
17
20
|
const connectTcp = (options) => new Promise((resolve, reject) => {
|
|
18
21
|
const socket = node_net_1.default.createConnection(options);
|
|
19
22
|
socket.once('connect', () => resolve(socket));
|
|
@@ -33,8 +36,11 @@ module.exports = function (RED) {
|
|
|
33
36
|
});
|
|
34
37
|
this.on('input', (message, _send, done) => __awaiter(this, void 0, void 0, function* () {
|
|
35
38
|
const error = yield (() => __awaiter(this, void 0, void 0, function* () {
|
|
39
|
+
var _a, _b;
|
|
36
40
|
try {
|
|
37
|
-
const
|
|
41
|
+
const cutAfter = (_a = message.cutAfter) !== null && _a !== void 0 ? _a : configuration.cutAfter;
|
|
42
|
+
const dotsPerLine = (_b = message.dotsPerLine) !== null && _b !== void 0 ? _b : configuration.dotsPerLine;
|
|
43
|
+
const payload = yield (() => __awaiter(this, void 0, void 0, function* () {
|
|
38
44
|
const payloadType = allowedPayloadTypes.find((type) => type === message.type) ||
|
|
39
45
|
configuration.payloadType;
|
|
40
46
|
const payload = message.payload || configuration.payload;
|
|
@@ -44,21 +50,104 @@ module.exports = function (RED) {
|
|
|
44
50
|
commands.push(Buffer.from([0x1b, 0x74, 18])); // Language options: 18 is CP852 code table
|
|
45
51
|
commands.push(Buffer.from([0x1b, 0x61, 1])); // Center alignment
|
|
46
52
|
commands.push(encodeText(String(payload)));
|
|
47
|
-
commands.push(Buffer.from([0x0a
|
|
48
|
-
commands.push(Buffer.from([0x1b, 0x69])); // Cut
|
|
53
|
+
commands.push(Buffer.from([0x0a])); // New line
|
|
49
54
|
return Buffer.concat(commands);
|
|
50
55
|
}
|
|
51
56
|
if (payloadType === 'image') {
|
|
52
|
-
|
|
57
|
+
const imageBuffer = yield (() => __awaiter(this, void 0, void 0, function* () {
|
|
58
|
+
if (Buffer.isBuffer(payload)) {
|
|
59
|
+
return payload;
|
|
60
|
+
}
|
|
61
|
+
if (typeof payload === 'string') {
|
|
62
|
+
if (payload.startsWith('data:image/')) {
|
|
63
|
+
const base64 = payload.split(',', 2)[1];
|
|
64
|
+
return Buffer.from(base64, 'base64');
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const url = new node_url_1.URL(payload);
|
|
68
|
+
if (url.protocol === 'http:' ||
|
|
69
|
+
url.protocol === 'https:' ||
|
|
70
|
+
url.protocol === 'file:') {
|
|
71
|
+
const response = yield fetch(url.toString());
|
|
72
|
+
return Buffer.from(yield response.arrayBuffer());
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (_c) { }
|
|
76
|
+
const base64Pattern = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
77
|
+
if (payload.length % 4 === 0 && base64Pattern.test(payload)) {
|
|
78
|
+
return Buffer.from(payload, 'base64');
|
|
79
|
+
}
|
|
80
|
+
const isFilePath = yield (() => __awaiter(this, void 0, void 0, function* () {
|
|
81
|
+
try {
|
|
82
|
+
yield promises_1.default.access(payload);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch (_d) { }
|
|
86
|
+
return false;
|
|
87
|
+
}))();
|
|
88
|
+
if (isFilePath) {
|
|
89
|
+
return promises_1.default.readFile(payload);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
throw new Error('Invalid image payload.');
|
|
93
|
+
}))();
|
|
94
|
+
const image = yield (0, skia_canvas_1.loadImage)(imageBuffer);
|
|
95
|
+
let imageWidth = image.width;
|
|
96
|
+
let imageHeight = image.height;
|
|
97
|
+
if (imageWidth > dotsPerLine) {
|
|
98
|
+
const aspectRatio = image.height / image.width;
|
|
99
|
+
imageWidth = dotsPerLine;
|
|
100
|
+
imageHeight = imageWidth * aspectRatio;
|
|
101
|
+
}
|
|
102
|
+
const canvas = new skia_canvas_1.Canvas(imageWidth, imageHeight);
|
|
103
|
+
const context = canvas.getContext('2d');
|
|
104
|
+
context.drawImage(image, 0, 0, imageWidth, imageHeight);
|
|
105
|
+
(0, dither_1.dither)(canvas);
|
|
106
|
+
const payloadParts = [];
|
|
107
|
+
payloadParts.push(Buffer.from([0x1b, 0x40])); // Initialize printer
|
|
108
|
+
payloadParts.push(Buffer.from([0x1b, 0x61, 1])); // Center alignment
|
|
109
|
+
const splitImageByHeightInPixels = 1024; // Splitting image to more smaller ones because Epson printer can't handle one super heigh image
|
|
110
|
+
for (let yOffset = 0; yOffset < canvas.height; yOffset += splitImageByHeightInPixels) {
|
|
111
|
+
const chunkHeight = Math.min(splitImageByHeightInPixels, canvas.height - yOffset);
|
|
112
|
+
const { width, height, data: imageData, } = context.getImageData(0, yOffset, canvas.width, chunkHeight);
|
|
113
|
+
const bytes = (width + 7) >> 3;
|
|
114
|
+
// For xL, xH, yL, yH, see the documentation https://download4.epson.biz/sec_pubs/pos/reference_en/escpos/gs_lv_0.html
|
|
115
|
+
const xL = bytes & 0xff;
|
|
116
|
+
const xH = bytes >> 8;
|
|
117
|
+
const yL = height & 0xff;
|
|
118
|
+
const yH = height >> 8;
|
|
119
|
+
const pixelsGroupedToBytes = Buffer.alloc(bytes * height);
|
|
120
|
+
for (let y = 0; y < height; y++) {
|
|
121
|
+
for (let x = 0; x < width; x++) {
|
|
122
|
+
const index = (y * width + x) * 4;
|
|
123
|
+
const red = imageData[index];
|
|
124
|
+
const green = imageData[index + 1];
|
|
125
|
+
const blue = imageData[index + 2];
|
|
126
|
+
const alpha = imageData[index + 3];
|
|
127
|
+
const greyscale = red * 0.299 + green * 0.587 + blue * 0.114;
|
|
128
|
+
if (alpha > 127 && greyscale < 128) {
|
|
129
|
+
const byteIndex = y * bytes + (x >> 3);
|
|
130
|
+
const bit = 1 << (7 - (x & 7));
|
|
131
|
+
pixelsGroupedToBytes[byteIndex] |= bit;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
payloadParts.push(Buffer.from([0x1d, 0x76, 0x30, 0, xL, xH, yL, yH]));
|
|
136
|
+
payloadParts.push(pixelsGroupedToBytes);
|
|
137
|
+
}
|
|
138
|
+
return Buffer.concat(payloadParts);
|
|
53
139
|
}
|
|
54
140
|
if (payloadType === 'buffer') {
|
|
141
|
+
if (Buffer.isBuffer(payload)) {
|
|
142
|
+
return payload;
|
|
143
|
+
}
|
|
55
144
|
if (Array.isArray(payload)) {
|
|
56
145
|
return Buffer.from(payload);
|
|
57
146
|
}
|
|
58
147
|
return Buffer.from(payload, 'base64');
|
|
59
148
|
}
|
|
60
149
|
return payloadType;
|
|
61
|
-
})();
|
|
150
|
+
}))();
|
|
62
151
|
this.status({ fill: 'yellow', shape: 'dot', text: 'connecting…' });
|
|
63
152
|
const hostname = message.host || configuration.host;
|
|
64
153
|
if (!hostname) {
|
|
@@ -70,7 +159,16 @@ module.exports = function (RED) {
|
|
|
70
159
|
const socket = yield connectTcp({ host, port });
|
|
71
160
|
this.status({ fill: 'yellow', shape: 'dot', text: 'sending…' });
|
|
72
161
|
yield new Promise((resolve, reject) => {
|
|
73
|
-
|
|
162
|
+
const commands = Buffer.concat([
|
|
163
|
+
payload,
|
|
164
|
+
...(cutAfter
|
|
165
|
+
? [
|
|
166
|
+
Buffer.from([0x0a, 0x0a, 0x0a, 0x0a, 0x0a]),
|
|
167
|
+
Buffer.from([0x1b, 0x69]), // Cut
|
|
168
|
+
]
|
|
169
|
+
: []),
|
|
170
|
+
]);
|
|
171
|
+
socket.write(commands, (error) => {
|
|
74
172
|
if (error) {
|
|
75
173
|
reject(error);
|
|
76
174
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dither = void 0;
|
|
4
|
+
const dither = (canvas) => {
|
|
5
|
+
const context = canvas.getContext('2d');
|
|
6
|
+
if (!context) {
|
|
7
|
+
throw new Error('Context not available.');
|
|
8
|
+
}
|
|
9
|
+
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
|
10
|
+
const { data } = imageData;
|
|
11
|
+
const dataLength = data.length;
|
|
12
|
+
const w = canvas.width;
|
|
13
|
+
const lumR = [];
|
|
14
|
+
const lumG = [];
|
|
15
|
+
const lumB = [];
|
|
16
|
+
let newPixel;
|
|
17
|
+
let err;
|
|
18
|
+
for (let i = 0; i < 256; i++) {
|
|
19
|
+
lumR[i] = i * 0.299;
|
|
20
|
+
lumG[i] = i * 0.587;
|
|
21
|
+
lumB[i] = i * 0.11;
|
|
22
|
+
}
|
|
23
|
+
// Greyscale luminance (sets r pixels to luminance of rgb)
|
|
24
|
+
for (let i = 0; i <= dataLength; i += 4) {
|
|
25
|
+
data[i] = Math.floor(lumR[data[i]] + lumG[data[i + 1]] + lumB[data[i + 2]]);
|
|
26
|
+
}
|
|
27
|
+
for (let currentPixel = 0; currentPixel <= dataLength; currentPixel += 4) {
|
|
28
|
+
// threshold for determining current pixel's conversion to a black or white pixel
|
|
29
|
+
newPixel = data[currentPixel] < 150 ? 0 : 255;
|
|
30
|
+
err = Math.floor((data[currentPixel] - newPixel) / 23);
|
|
31
|
+
data[currentPixel + 0 * 1 - 0] = newPixel;
|
|
32
|
+
data[currentPixel + 4 * 1 - 0] += err * 7;
|
|
33
|
+
data[currentPixel + 4 * w - 4] += err * 3;
|
|
34
|
+
data[currentPixel + 4 * w - 0] += err * 5;
|
|
35
|
+
data[currentPixel + 4 * w + 4] += err * 1;
|
|
36
|
+
// Set g and b values equal to r (effectively greyscales the image fully)
|
|
37
|
+
data[currentPixel + 1] = data[currentPixel + 2] = data[currentPixel];
|
|
38
|
+
}
|
|
39
|
+
context.putImageData(imageData, 0, 0);
|
|
40
|
+
};
|
|
41
|
+
exports.dither = dither;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-tcp-escpos",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Escpos over TCP/IP node for Node-RED",
|
|
5
5
|
"devDependencies": {
|
|
6
6
|
"@types/node": "^18.14.0",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"iconv-lite": "^0.7.0"
|
|
42
|
+
"iconv-lite": "^0.7.0",
|
|
43
|
+
"skia-canvas": "^3.0.8"
|
|
43
44
|
}
|
|
44
45
|
}
|