node-red-contrib-tcp-escpos 0.1.2 → 0.1.4

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 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" min="8" step="8" />
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,23 +65,59 @@
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
- <p>Typescript example Node-RED node</p>
76
+ <p>Sends raw text, images, or buffers to an ESC/POS-compatible printer over TCP/IP.</p>
77
+
78
+ <h3>Configuration</h3>
79
+ <dl class="message-properties">
80
+ <dt>Host</dt>
81
+ <dd>The IP address and port of the printer (e.g., <code>192.168.1.101:9100</code>). If the port is omitted, it defaults to <code>9100</code>.</dd>
82
+ <dt>Dots per line</dt>
83
+ <dd>The maximum width of the paper in dots. This is used to scale down images that are too wide. The default is <code>576</code>.</dd>
84
+ <dt>Type</dt>
85
+ <dd>The type of data to send to the printer. This can be <code>Text</code>, <code>Image</code>, or <code>Buffer</code>.</dd>
86
+ <dt>Payload</dt>
87
+ <dd>The content to send to the printer. The format depends on the selected <code>Type</code>.</dd>
88
+ <dt>Cut After</dt>
89
+ <dd>If checked, the printer will cut the paper after the payload has been printed.</dd>
90
+ </dl>
91
+
92
+ <h3>Payload Types</h3>
93
+ <dl class="message-properties">
94
+ <dt>Text</dt>
95
+ <dd>The payload should be a string of text. The node will automatically center the text and add a newline character at the end.</dd>
96
+ <dt>Image</dt>
97
+ <dd>
98
+ The payload can be one of the following:
99
+ <ul>
100
+ <li>A Buffer containing the image data.</li>
101
+ <li>A base64 encoded string.</li>
102
+ <li>A data URL (e.g., <code>data:image/png;base64,...</code>).</li>
103
+ <li>A URL (<code>http</code>, <code>https</code>, or <code>file</code>).</li>
104
+ <li>A file path.</li>
105
+ </ul>
106
+ The image will be dithered and converted to monochrome before printing. If the image is wider than the <code>Dots per line</code> setting, it will be scaled down to fit.
107
+ </dd>
108
+ <dt>Buffer</dt>
109
+ <dd>The payload can be a Buffer or an array of bytes. This allows you to send raw ESC/POS commands to the printer.</dd>
110
+ </dl>
111
+
48
112
  <h3>Inputs</h3>
49
113
  <dl class="message-properties">
50
- <dt>
51
- payload
52
- <span class="property-type">boolean</span>
53
- </dt>
54
- <h3>Outputs</h3>
55
- <dl class="message-properties">
56
- <dt>
57
- payload
58
- <span class="property-type">object</span>
59
- </dt>
60
- <h3>Details</h3>
61
- <p>Some more information about the node.</p>
62
- </dl>
114
+ <dt>payload</dt>
115
+ <dd>The payload to be sent to the printer. This will override the <code>Payload</code> setting in the node's configuration.</dd>
116
+ <dt>host</dt>
117
+ <dd>The host to connect to. This will override the <code>Host</code> setting in the node's configuration.</dd>
118
+ <dt>dotsPerLine</dt>
119
+ <dd>The dots per line to use. This will override the <code>Dots per line</code> setting in the node's configuration.</dd>
120
+ <dt>cutAfter</dt>
121
+ <dd>Whether to cut the paper after printing. This will override the <code>Cut After</code> setting in the node's configuration.</dd>
63
122
  </dl>
64
123
  </script>
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 payload = (() => {
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, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a])); // New lines
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
- throw new Error('Image type not implemented yet');
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
- socket.write(payload, (error) => {
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.2",
3
+ "version": "0.1.4",
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
  }