@naphatjm/cdh-thermal-printer 1.0.4 → 1.0.6
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/LICENSE +21 -0
- package/README.md +580 -59
- package/dist/index.d.ts +45 -14
- package/dist/thermal-printer.es.js +117 -76
- package/dist/thermal-printer.es.js.map +1 -1
- package/dist/thermal-printer.umd.js +4 -4
- package/dist/thermal-printer.umd.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,16 @@ export interface PrintResponse {
|
|
|
11
11
|
message?: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Printer information from Windows/system
|
|
16
|
+
*/
|
|
17
|
+
export interface PrinterInfo {
|
|
18
|
+
name: string;
|
|
19
|
+
driver: string;
|
|
20
|
+
port: string;
|
|
21
|
+
status_text: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
/**
|
|
15
25
|
* Convert Thai text to ImageData (Client-side only)
|
|
16
26
|
*
|
|
@@ -56,33 +66,33 @@ export function textToImageData(
|
|
|
56
66
|
): ImageData;
|
|
57
67
|
|
|
58
68
|
/**
|
|
59
|
-
* ThermalPrinter Service
|
|
69
|
+
* ThermalPrinter Service with Auto-Discovery
|
|
60
70
|
* Fluent API for controlling thermal printer with ESC/POS commands
|
|
71
|
+
* Automatically discovers printer driver on ports 9123-9130
|
|
61
72
|
*
|
|
62
73
|
* @example
|
|
63
74
|
* ```javascript
|
|
64
|
-
* const printer = new ThermalPrinter(
|
|
75
|
+
* const printer = new ThermalPrinter(384); // 384 dots = 58mm (default)
|
|
65
76
|
* printer
|
|
66
77
|
* .init()
|
|
67
78
|
* .align(1)
|
|
68
79
|
* .printThaiText("สวัสดี")
|
|
69
80
|
* .feed(2)
|
|
70
81
|
* .cut()
|
|
71
|
-
* .print("Printer Name");
|
|
82
|
+
* .print("Printer Name"); // Auto-discovers driver
|
|
72
83
|
* ```
|
|
73
84
|
*/
|
|
74
85
|
export class ThermalPrinter {
|
|
75
86
|
/**
|
|
76
|
-
* Initialize ThermalPrinter instance
|
|
77
|
-
* @param driverApiUrl - URL of the thermal printer driver API (default: http://localhost:9123)
|
|
87
|
+
* Initialize ThermalPrinter instance with auto-discovery
|
|
78
88
|
* @param width - Printer width in dots (384 = 58mm, 576 = 80mm, default: 384)
|
|
79
89
|
*/
|
|
80
|
-
constructor(
|
|
90
|
+
constructor(width?: number);
|
|
81
91
|
|
|
82
92
|
/**
|
|
83
|
-
* Driver API URL
|
|
93
|
+
* Driver API URL (null until driver is discovered)
|
|
84
94
|
*/
|
|
85
|
-
|
|
95
|
+
driverApi: string | null;
|
|
86
96
|
|
|
87
97
|
/**
|
|
88
98
|
* Printer width in dots
|
|
@@ -211,21 +221,42 @@ export class ThermalPrinter {
|
|
|
211
221
|
getBuffer(): Uint8Array;
|
|
212
222
|
|
|
213
223
|
/**
|
|
214
|
-
*
|
|
224
|
+
* Auto-discover printer driver on localhost:9123-9130
|
|
225
|
+
* Performs health check and verifies it's a CDH-Driver service
|
|
226
|
+
* Caches discovered URL for subsequent calls
|
|
227
|
+
*
|
|
228
|
+
* @returns Promise<boolean> - true if driver found, false otherwise
|
|
229
|
+
* @throws Error if driver not found after scanning all ports
|
|
230
|
+
*/
|
|
231
|
+
findDriver(): Promise<boolean>;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Print to specific printer (auto-discovers driver if needed)
|
|
215
235
|
* @param printerName - Name of printer to print to (optional, will use first available if not provided)
|
|
216
236
|
* @returns Promise resolving to print response
|
|
217
|
-
* @throws Error if
|
|
237
|
+
* @throws Error if driver not found or print failed
|
|
218
238
|
*/
|
|
219
239
|
print(printerName?: string): Promise<PrintResponse>;
|
|
220
240
|
|
|
221
241
|
// --- Static Methods ---
|
|
222
242
|
|
|
223
243
|
/**
|
|
224
|
-
* Get list of available printers
|
|
225
|
-
*
|
|
226
|
-
* @
|
|
244
|
+
* Get list of available printers from Windows/system
|
|
245
|
+
* Auto-discovers driver if URL not provided
|
|
246
|
+
* @param overrideUrl - Driver API URL (optional, will auto-discover if not provided)
|
|
247
|
+
* @returns Promise resolving to array of PrinterInfo objects
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```javascript
|
|
251
|
+
* const printers = await ThermalPrinter.getPrinters();
|
|
252
|
+
* // [
|
|
253
|
+
* // { name: "thermal printer", driver: "Generic / Text Only", port: "USB001", status_text: "Ready" },
|
|
254
|
+
* // { name: "HP LaserJet M1530 MFP Series PCL 6", ... },
|
|
255
|
+
* // ...
|
|
256
|
+
* // ]
|
|
257
|
+
* ```
|
|
227
258
|
*/
|
|
228
|
-
static getPrinters(
|
|
259
|
+
static getPrinters(overrideUrl?: string): Promise<PrinterInfo[]>;
|
|
229
260
|
}
|
|
230
261
|
|
|
231
262
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const o = {
|
|
2
2
|
ESC: 27,
|
|
3
3
|
GS: 29,
|
|
4
4
|
LF: 10,
|
|
@@ -18,86 +18,91 @@ const s = {
|
|
|
18
18
|
// Cash Drawer
|
|
19
19
|
DRAWER_KICK: [27, 112, 0, 12, 120],
|
|
20
20
|
// Beep (ESC B - default 100ms)
|
|
21
|
-
BEEP: (
|
|
21
|
+
BEEP: (l = 100) => [27, 66, l]
|
|
22
22
|
};
|
|
23
|
-
function
|
|
24
|
-
const
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
for (let h = 0; h <
|
|
28
|
-
for (let c = 0; c <
|
|
23
|
+
function b(l, t = 382) {
|
|
24
|
+
const r = l.width, e = l.height, n = l.data, s = Math.ceil(r / 8) * 8 / 8;
|
|
25
|
+
let i = [];
|
|
26
|
+
i.push(29, 118, 48, 0), i.push(s % 256, Math.floor(s / 256)), i.push(e % 256, Math.floor(e / 256));
|
|
27
|
+
for (let h = 0; h < e; h++)
|
|
28
|
+
for (let c = 0; c < s; c++) {
|
|
29
29
|
let f = 0;
|
|
30
|
-
for (let
|
|
31
|
-
const
|
|
32
|
-
|
|
30
|
+
for (let u = 0; u < 8; u++) {
|
|
31
|
+
const p = c * 8 + u;
|
|
32
|
+
if (p >= r)
|
|
33
|
+
continue;
|
|
34
|
+
const d = (h * r + p) * 4;
|
|
35
|
+
n[d] + // Red
|
|
36
|
+
n[d + 1] + // Green
|
|
37
|
+
n[d + 2] < t && (f |= 1 << 7 - u);
|
|
33
38
|
}
|
|
34
|
-
|
|
39
|
+
i.push(f);
|
|
35
40
|
}
|
|
36
|
-
return
|
|
41
|
+
return i;
|
|
37
42
|
}
|
|
38
|
-
function
|
|
43
|
+
function g(l, t = 384, r = 22) {
|
|
39
44
|
if (typeof document > "u")
|
|
40
45
|
throw new Error(
|
|
41
46
|
"textToImageData() requires browser environment with Canvas API"
|
|
42
47
|
);
|
|
43
|
-
const
|
|
44
|
-
`),
|
|
45
|
-
|
|
46
|
-
let
|
|
47
|
-
for (let
|
|
48
|
-
n.fillText(
|
|
49
|
-
return n.getImageData(0, 0,
|
|
48
|
+
const e = document.createElement("canvas"), n = e.getContext("2d"), a = "'Sarabun', sans-serif", s = r + 12, i = l.split(`
|
|
49
|
+
`), h = i.length * s + 20;
|
|
50
|
+
e.width = t, e.height = h, n.fillStyle = "white", n.fillRect(0, 0, e.width, e.height), n.fillStyle = "black", n.font = `${r}px ${a}`, n.textBaseline = "top";
|
|
51
|
+
let c = 10;
|
|
52
|
+
for (let f of i)
|
|
53
|
+
n.fillText(f, 10, c), c += s;
|
|
54
|
+
return n.getImageData(0, 0, t, h);
|
|
50
55
|
}
|
|
51
56
|
class x {
|
|
52
|
-
constructor(
|
|
53
|
-
this.driverApi =
|
|
57
|
+
constructor(t = 384) {
|
|
58
|
+
this.driverApi = null, this.printerWidth = t, this.buffer = [];
|
|
54
59
|
}
|
|
55
60
|
// --- Basic Commands ---
|
|
56
61
|
init() {
|
|
57
|
-
return this.buffer.push(...
|
|
62
|
+
return this.buffer.push(...o.INIT), this;
|
|
58
63
|
}
|
|
59
|
-
align(
|
|
60
|
-
const
|
|
61
|
-
return
|
|
64
|
+
align(t = 1) {
|
|
65
|
+
const r = [...o.ALIGN_LEFT];
|
|
66
|
+
return r[2] = t, this.buffer.push(...r), this;
|
|
62
67
|
}
|
|
63
|
-
bold(
|
|
64
|
-
return this.buffer.push(...
|
|
68
|
+
bold(t = !0) {
|
|
69
|
+
return this.buffer.push(...t ? o.BOLD_ON : o.BOLD_OFF), this;
|
|
65
70
|
}
|
|
66
|
-
feed(
|
|
67
|
-
for (let
|
|
71
|
+
feed(t = 1) {
|
|
72
|
+
for (let r = 0; r < t; r++) this.buffer.push(o.LF);
|
|
68
73
|
return this;
|
|
69
74
|
}
|
|
70
|
-
cut(
|
|
71
|
-
return this.buffer.push(...
|
|
75
|
+
cut(t = !1) {
|
|
76
|
+
return this.buffer.push(...t ? o.CUT_PARTIAL : o.CUT_FULL), this;
|
|
72
77
|
}
|
|
73
|
-
line(
|
|
74
|
-
const
|
|
78
|
+
line(t = "") {
|
|
79
|
+
const e = new TextEncoder().encode(t + `
|
|
75
80
|
`);
|
|
76
|
-
return this.buffer.push(...
|
|
81
|
+
return this.buffer.push(...e), this;
|
|
77
82
|
}
|
|
78
|
-
newline(
|
|
79
|
-
for (let
|
|
80
|
-
this.buffer.push(
|
|
83
|
+
newline(t = 1) {
|
|
84
|
+
for (let r = 0; r < t; r++)
|
|
85
|
+
this.buffer.push(o.LF);
|
|
81
86
|
return this;
|
|
82
87
|
}
|
|
83
|
-
divider(
|
|
84
|
-
return this.line(
|
|
88
|
+
divider(t = "-", r = 32) {
|
|
89
|
+
return this.line(t.repeat(r)), this;
|
|
85
90
|
}
|
|
86
|
-
beep(
|
|
87
|
-
for (let
|
|
88
|
-
this.buffer.push(...
|
|
91
|
+
beep(t = 1, r = 100) {
|
|
92
|
+
for (let e = 0; e < t; e++)
|
|
93
|
+
this.buffer.push(...o.BEEP(r));
|
|
89
94
|
return this;
|
|
90
95
|
}
|
|
91
96
|
drawerKick() {
|
|
92
|
-
return this.buffer.push(...
|
|
97
|
+
return this.buffer.push(...o.DRAWER_KICK), this;
|
|
93
98
|
}
|
|
94
|
-
raw(
|
|
95
|
-
return this.buffer.push(...
|
|
99
|
+
raw(t) {
|
|
100
|
+
return this.buffer.push(...t), this;
|
|
96
101
|
}
|
|
97
102
|
// --- Image & Thai Text Logic ---
|
|
98
|
-
image(
|
|
99
|
-
const
|
|
100
|
-
return this.buffer.push(...
|
|
103
|
+
image(t) {
|
|
104
|
+
const r = b(t);
|
|
105
|
+
return this.buffer.push(...r), this;
|
|
101
106
|
}
|
|
102
107
|
/**
|
|
103
108
|
* พิมพ์ข้อความภาษาไทย (Render เป็นรูปภาพอัตโนมัติ)
|
|
@@ -110,17 +115,17 @@ class x {
|
|
|
110
115
|
* @param {string} text - Thai text to print
|
|
111
116
|
* @param {number} fontSize - Font size in pixels (default: 22)
|
|
112
117
|
*/
|
|
113
|
-
printThaiText(
|
|
118
|
+
printThaiText(t, r = 22) {
|
|
114
119
|
if (typeof document > "u")
|
|
115
120
|
return console.error("Browser environment required for printThaiText"), this;
|
|
116
|
-
const
|
|
117
|
-
`),
|
|
118
|
-
|
|
119
|
-
let
|
|
120
|
-
for (let u of
|
|
121
|
-
n.fillText(u, 10,
|
|
122
|
-
const
|
|
123
|
-
return this.image(
|
|
121
|
+
const e = document.createElement("canvas"), n = e.getContext("2d"), a = "'Sarabun', sans-serif", s = r + 12, i = t.split(`
|
|
122
|
+
`), h = i.length * s + 20;
|
|
123
|
+
e.width = this.printerWidth, e.height = h, n.fillStyle = "white", n.fillRect(0, 0, e.width, e.height), n.fillStyle = "black", n.font = `${r}px ${a}`, n.textBaseline = "top";
|
|
124
|
+
let c = 10;
|
|
125
|
+
for (let u of i)
|
|
126
|
+
n.fillText(u, 10, c), c += s;
|
|
127
|
+
const f = n.getImageData(0, 0, this.printerWidth, h);
|
|
128
|
+
return this.image(f);
|
|
124
129
|
}
|
|
125
130
|
// --- Execution ---
|
|
126
131
|
clear() {
|
|
@@ -129,38 +134,74 @@ class x {
|
|
|
129
134
|
getBuffer() {
|
|
130
135
|
return new Uint8Array(this.buffer);
|
|
131
136
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
// --- 🔥 Auto Discovery & Print Logic 🔥 ---
|
|
138
|
+
/**
|
|
139
|
+
* วิ่งหา Driver ใน Port 9123-9130
|
|
140
|
+
* (Lazy Mode: เรียกใช้เมื่อจำเป็น)
|
|
141
|
+
*/
|
|
142
|
+
async findDriver() {
|
|
143
|
+
if (this.driverApi)
|
|
144
|
+
try {
|
|
145
|
+
if ((await fetch(`${this.driverApi}/health`)).ok) return !0;
|
|
146
|
+
} catch {
|
|
147
|
+
console.warn("Driver connection lost, rescanning..."), this.driverApi = null;
|
|
148
|
+
}
|
|
149
|
+
for (let t = 9123; t <= 9130; t++) {
|
|
150
|
+
const r = `http://localhost:${t}`;
|
|
151
|
+
try {
|
|
152
|
+
const e = new AbortController(), n = setTimeout(() => e.abort(), 100), a = await fetch(`${r}/health`, {
|
|
153
|
+
signal: e.signal
|
|
154
|
+
});
|
|
155
|
+
if (clearTimeout(n), a.ok && (await a.json()).service === "CDH-Driver")
|
|
156
|
+
return this.driverApi = r, console.log(`✅ Driver found at: ${r}`), !0;
|
|
157
|
+
} catch {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return console.error("❌ CDH Driver not found (Is .exe running?)"), !1;
|
|
162
|
+
}
|
|
163
|
+
async print(t) {
|
|
164
|
+
if (!await this.findDriver())
|
|
165
|
+
throw new Error(
|
|
166
|
+
"Cannot connect to Printer Driver. Please run the application."
|
|
167
|
+
);
|
|
168
|
+
if (!t) {
|
|
169
|
+
const e = await x.getPrinters(this.driverApi);
|
|
170
|
+
if (e.length > 0) t = e[0].name;
|
|
171
|
+
else throw new Error("No printer found in Windows settings.");
|
|
137
172
|
}
|
|
138
173
|
try {
|
|
139
|
-
const
|
|
174
|
+
const e = this.getBuffer(), n = encodeURIComponent(t);
|
|
140
175
|
if (!(await fetch(
|
|
141
|
-
`${this.driverApi}/print?printer=${
|
|
176
|
+
`${this.driverApi}/print?printer=${n}`,
|
|
142
177
|
{
|
|
143
178
|
method: "POST",
|
|
144
179
|
headers: { "Content-Type": "application/octet-stream" },
|
|
145
|
-
body:
|
|
180
|
+
body: e
|
|
146
181
|
}
|
|
147
|
-
)).ok) throw new Error("Driver
|
|
182
|
+
)).ok) throw new Error("Driver returned error");
|
|
148
183
|
return this.clear(), { success: !0 };
|
|
149
|
-
} catch (
|
|
150
|
-
throw console.error("Print Failed:",
|
|
184
|
+
} catch (e) {
|
|
185
|
+
throw console.error("Print Failed:", e), e;
|
|
151
186
|
}
|
|
152
187
|
}
|
|
153
|
-
//
|
|
154
|
-
static async getPrinters(
|
|
188
|
+
// Static Method: ต้องสร้าง instance ชั่วคราวไปหา driver
|
|
189
|
+
static async getPrinters(t = null) {
|
|
190
|
+
let r = t;
|
|
191
|
+
if (!r) {
|
|
192
|
+
const e = new x();
|
|
193
|
+
if (await e.findDriver()) r = e.driverApi;
|
|
194
|
+
else return [];
|
|
195
|
+
}
|
|
155
196
|
try {
|
|
156
|
-
return await (await fetch(`${
|
|
157
|
-
} catch (
|
|
158
|
-
return console.error("
|
|
197
|
+
return await (await fetch(`${r}/printers`)).json();
|
|
198
|
+
} catch (e) {
|
|
199
|
+
return console.error("Get Printers Failed:", e), [];
|
|
159
200
|
}
|
|
160
201
|
}
|
|
161
202
|
}
|
|
162
203
|
export {
|
|
163
204
|
x as ThermalPrinter,
|
|
164
|
-
|
|
205
|
+
g as textToImageData
|
|
165
206
|
};
|
|
166
207
|
//# sourceMappingURL=thermal-printer.es.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thermal-printer.es.js","sources":["../src/constants.js","../src/utils.js","../src/index.js"],"sourcesContent":["// src/constants.js\r\nexport const CMD = {\r\n ESC: 0x1b,\r\n GS: 0x1d,\r\n LF: 0x0a,\r\n\r\n // Commands\r\n INIT: [0x1b, 0x40], // Initialize printer\r\n CUT_FULL: [0x1d, 0x56, 0x42, 0x00], // Full cut\r\n CUT_PARTIAL: [0x1d, 0x56, 0x41, 0x00], // Partial cut\r\n\r\n // Formatting\r\n BOLD_ON: [0x1b, 0x45, 0x01],\r\n BOLD_OFF: [0x1b, 0x45, 0x00],\r\n\r\n ALIGN_LEFT: [0x1b, 0x61, 0x00],\r\n ALIGN_CENTER: [0x1b, 0x61, 0x01],\r\n ALIGN_RIGHT: [0x1b, 0x61, 0x02],\r\n\r\n // Cash Drawer\r\n DRAWER_KICK: [0x1b, 0x70, 0x00, 0x0c, 0x78],\r\n\r\n // Beep (ESC B - default 100ms)\r\n BEEP: (duration = 100) => [0x1b, 0x42, duration],\r\n};\r\n","// src/utils.js\r\n\r\n/**\r\n * แปลงข้อมูลภาพ (ImageData) เป็นคำสั่ง ESC/POS GS v 0\r\n * @param {ImageData} imageData\r\n * @returns {number[]} Array ของ Byte คำสั่ง\r\n */\r\nexport function convertCanvasToEscPos(imageData) {\r\n const width = imageData.width;\r\n const height = imageData.height;\r\n const pixels = imageData.data;\r\n\r\n // 1 Byte = 8 pixels แนวนอน\r\n const xBytes = width / 8;\r\n\r\n let command = [];\r\n\r\n // Header: GS v 0 (Raster Bit Image)\r\n command.push(0x1d, 0x76, 0x30, 0x00);\r\n\r\n // บอกขนาดภาพ (Little Endian)\r\n command.push(xBytes % 256, Math.floor(xBytes / 256)); // xL, xH\r\n command.push(height % 256, Math.floor(height / 256)); // yL, yH\r\n\r\n // Loop ทุก Pixel แปลงเป็น Bit\r\n for (let y = 0; y < height; y++) {\r\n for (let x = 0; x < xBytes; x++) {\r\n let byte = 0;\r\n for (let bit = 0; bit < 8; bit++) {\r\n // คำนวณตำแหน่ง Pixel ใน Array (RGBA = 4 ช่องต่อ 1 pixel)\r\n const pxIndex = (y * width + x * 8 + bit) * 4;\r\n\r\n // เช็คความสว่าง (R+G+B)\r\n // ถ้าค่าต่ำกว่า 380 (ค่อนข้างมืด) ให้ถือเป็นสีดำ (Bit=1)\r\n const brightness =\r\n pixels[pxIndex] + pixels[pxIndex + 1] + pixels[pxIndex + 2];\r\n if (brightness < 380) {\r\n byte |= 1 << (7 - bit);\r\n }\r\n }\r\n command.push(byte);\r\n }\r\n }\r\n\r\n return command;\r\n}\r\n/**\r\n * แปลงข้อความเป็น ImageData (Client-side only)\r\n * ต้องใช้ใน Browser environment เท่านั้น\r\n *\r\n * ⚠️ REQUIRED: Sarabun font must be loaded in your HTML:\r\n * <link href=\"https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap\" rel=\"stylesheet\">\r\n *\r\n * @param {string} text - Thai text to convert\r\n * @param {number} width - Canvas width in pixels (default: 384)\r\n * @param {number} fontSize - Font size in pixels (default: 22)\r\n * @returns {ImageData} Canvas ImageData object\r\n * @throws {Error} If not in browser environment\r\n *\r\n * @example\r\n * // Client-side (React/Next.js)\r\n * const imageData = textToImageData(\"สวัสดี\", 384, 22);\r\n * await fetch('/api/print', {\r\n * body: JSON.stringify({\r\n * data: Array.from(imageData.data),\r\n * width: imageData.width,\r\n * height: imageData.height,\r\n * })\r\n * });\r\n */\r\nexport function textToImageData(text, width = 384, fontSize = 22) {\r\n // ตรวจสอบว่าเป็น Browser environment\r\n if (typeof document === \"undefined\") {\r\n throw new Error(\r\n \"textToImageData() requires browser environment with Canvas API\",\r\n );\r\n }\r\n\r\n const canvas = document.createElement(\"canvas\");\r\n const ctx = canvas.getContext(\"2d\");\r\n\r\n // ตั้งค่า Font\r\n const fontFamily = \"'Sarabun', sans-serif\";\r\n const lineHeight = fontSize + 12;\r\n const lines = text.split(\"\\n\");\r\n\r\n // คำนวณความสูง\r\n const height = lines.length * lineHeight + 20;\r\n\r\n canvas.width = width;\r\n canvas.height = height;\r\n\r\n // วาดพื้นหลังขาว\r\n ctx.fillStyle = \"white\";\r\n ctx.fillRect(0, 0, canvas.width, canvas.height);\r\n\r\n // วาดข้อความ\r\n ctx.fillStyle = \"black\";\r\n ctx.font = `${fontSize}px ${fontFamily}`;\r\n ctx.textBaseline = \"top\";\r\n\r\n let y = 10;\r\n for (let line of lines) {\r\n ctx.fillText(line, 10, y);\r\n y += lineHeight;\r\n }\r\n\r\n // ดึง ImageData\r\n return ctx.getImageData(0, 0, width, height);\r\n}\r\n","// src/index.js\r\nimport { CMD } from \"./constants\";\r\nimport { convertCanvasToEscPos, textToImageData } from \"./utils\";\r\n\r\n/**\r\n * Thermal Printer Library for ESC/POS printers\r\n *\r\n * IMPORTANT: When using printThaiText() or textToImageData(), you must load the Sarabun font in your HTML:\r\n * <link href=\"https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap\" rel=\"stylesheet\">\r\n *\r\n * Without this link, the printer will use the default system font instead of Sarabun.\r\n */\r\nexport { textToImageData } from \"./utils\";\r\nexport class ThermalPrinter {\r\n constructor(driverApiUrl = \"http://localhost:9123\", width = 384) {\r\n this.driverApi = driverApiUrl;\r\n this.printerWidth = width; // 384 dots = 58mm, 576 dots = 80mm\r\n this.buffer = [];\r\n }\r\n\r\n // --- Basic Commands ---\r\n\r\n init() {\r\n this.buffer.push(...CMD.INIT);\r\n return this;\r\n }\r\n\r\n align(alignment = 1) {\r\n // 0=Left, 1=Center, 2=Right\r\n const cmd = [...CMD.ALIGN_LEFT];\r\n cmd[2] = alignment;\r\n this.buffer.push(...cmd);\r\n return this;\r\n }\r\n\r\n bold(enable = true) {\r\n this.buffer.push(...(enable ? CMD.BOLD_ON : CMD.BOLD_OFF));\r\n return this;\r\n }\r\n\r\n feed(lines = 1) {\r\n for (let i = 0; i < lines; i++) this.buffer.push(CMD.LF);\r\n return this;\r\n }\r\n\r\n cut(partial = false) {\r\n this.buffer.push(...(partial ? CMD.CUT_PARTIAL : CMD.CUT_FULL));\r\n return this;\r\n }\r\n\r\n line(text = \"\") {\r\n const encoder = new TextEncoder();\r\n const bytes = encoder.encode(text + \"\\n\");\r\n this.buffer.push(...bytes);\r\n return this;\r\n }\r\n\r\n newline(count = 1) {\r\n for (let i = 0; i < count; i++) {\r\n this.buffer.push(CMD.LF);\r\n }\r\n return this;\r\n }\r\n\r\n divider(char = \"-\", width = 32) {\r\n this.line(char.repeat(width));\r\n return this;\r\n }\r\n\r\n beep(times = 1, duration = 100) {\r\n for (let i = 0; i < times; i++) {\r\n this.buffer.push(...CMD.BEEP(duration));\r\n }\r\n return this;\r\n }\r\n\r\n drawerKick() {\r\n this.buffer.push(...CMD.DRAWER_KICK);\r\n return this;\r\n }\r\n\r\n raw(bytes) {\r\n this.buffer.push(...bytes);\r\n return this;\r\n }\r\n\r\n // --- Image & Thai Text Logic ---\r\n\r\n image(imageData) {\r\n const bytes = convertCanvasToEscPos(imageData);\r\n this.buffer.push(...bytes);\r\n return this;\r\n }\r\n\r\n /**\r\n * พิมพ์ข้อความภาษาไทย (Render เป็นรูปภาพอัตโนมัติ)\r\n *\r\n * ⚠️ REQUIRED: Sarabun font must be loaded in your HTML file:\r\n * <link href=\"https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap\" rel=\"stylesheet\">\r\n *\r\n * Without this link, the system will use the default font instead.\r\n *\r\n * @param {string} text - Thai text to print\r\n * @param {number} fontSize - Font size in pixels (default: 22)\r\n */\r\n printThaiText(text, fontSize = 22) {\r\n if (typeof document === \"undefined\") {\r\n console.error(\"Browser environment required for printThaiText\");\r\n return this;\r\n }\r\n\r\n const canvas = document.createElement(\"canvas\");\r\n const ctx = canvas.getContext(\"2d\");\r\n\r\n // ตั้งค่า Font\r\n const fontFamily = \"'Sarabun', sans-serif\";\r\n const lineHeight = fontSize + 12;\r\n const lines = text.split(\"\\n\");\r\n\r\n // คำนวณความสูง\r\n const height = lines.length * lineHeight + 20;\r\n\r\n canvas.width = this.printerWidth;\r\n canvas.height = height;\r\n\r\n // วาดพื้นหลังขาว\r\n ctx.fillStyle = \"white\";\r\n ctx.fillRect(0, 0, canvas.width, canvas.height);\r\n\r\n // วาดข้อความ\r\n ctx.fillStyle = \"black\";\r\n ctx.font = `${fontSize}px ${fontFamily}`;\r\n ctx.textBaseline = \"top\";\r\n\r\n let y = 10;\r\n for (let line of lines) {\r\n ctx.fillText(line, 10, y);\r\n y += lineHeight;\r\n }\r\n\r\n const imageData = ctx.getImageData(0, 0, this.printerWidth, height);\r\n return this.image(imageData);\r\n }\r\n\r\n // --- Execution ---\r\n\r\n clear() {\r\n this.buffer = [];\r\n return this;\r\n }\r\n\r\n getBuffer() {\r\n return new Uint8Array(this.buffer);\r\n }\r\n\r\n async print(printerName) {\r\n // ถ้าไม่ส่งชื่อมา ให้ลองดึงตัวแรก\r\n if (!printerName) {\r\n const printers = await ThermalPrinter.getPrinters(this.driverApi);\r\n if (printers.length > 0) printerName = printers[0];\r\n else throw new Error(\"No printer found.\");\r\n }\r\n\r\n try {\r\n const bufferData = this.getBuffer();\r\n const encodedName = encodeURIComponent(printerName);\r\n\r\n const res = await fetch(\r\n `${this.driverApi}/print?printer=${encodedName}`,\r\n {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/octet-stream\" },\r\n body: bufferData,\r\n },\r\n );\r\n\r\n if (!res.ok) throw new Error(\"Driver Error\");\r\n\r\n this.clear();\r\n return { success: true };\r\n } catch (e) {\r\n console.error(\"Print Failed:\", e);\r\n throw e;\r\n }\r\n }\r\n\r\n // --- Static Methods ---\r\n\r\n static async getPrinters(apiUrl = \"http://localhost:9123\") {\r\n try {\r\n const res = await fetch(`${apiUrl}/printers`);\r\n return await res.json();\r\n } catch (e) {\r\n console.error(\"Connection Error:\", e);\r\n return [];\r\n }\r\n }\r\n}\r\n"],"names":["CMD","duration","convertCanvasToEscPos","imageData","width","height","pixels","xBytes","command","y","x","byte","bit","pxIndex","textToImageData","text","fontSize","canvas","ctx","fontFamily","lineHeight","lines","line","ThermalPrinter","driverApiUrl","alignment","cmd","enable","i","partial","bytes","count","char","times","printerName","printers","bufferData","encodedName","e","apiUrl"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"thermal-printer.es.js","sources":["../src/constants.js","../src/utils.js","../src/index.js"],"sourcesContent":["// src/constants.js\r\nexport const CMD = {\r\n ESC: 0x1b,\r\n GS: 0x1d,\r\n LF: 0x0a,\r\n\r\n // Commands\r\n INIT: [0x1b, 0x40], // Initialize printer\r\n CUT_FULL: [0x1d, 0x56, 0x42, 0x00], // Full cut\r\n CUT_PARTIAL: [0x1d, 0x56, 0x41, 0x00], // Partial cut\r\n\r\n // Formatting\r\n BOLD_ON: [0x1b, 0x45, 0x01],\r\n BOLD_OFF: [0x1b, 0x45, 0x00],\r\n\r\n ALIGN_LEFT: [0x1b, 0x61, 0x00],\r\n ALIGN_CENTER: [0x1b, 0x61, 0x01],\r\n ALIGN_RIGHT: [0x1b, 0x61, 0x02],\r\n\r\n // Cash Drawer\r\n DRAWER_KICK: [0x1b, 0x70, 0x00, 0x0c, 0x78],\r\n\r\n // Beep (ESC B - default 100ms)\r\n BEEP: (duration = 100) => [0x1b, 0x42, duration],\r\n};\r\n","// src/utils.js\r\n\r\n/**\r\n * แปลงข้อมูลภาพ (ImageData) เป็นคำสั่ง ESC/POS GS v 0\r\n * รองรับภาพ QR Code, Logo, Thai Text เป็นต้น\r\n *\r\n * @param {ImageData|Object} imageData - ImageData object หรือ ImageDataLike {data, width, height}\r\n * @param {number} brightnessThreshold - ค่าเกณฑ์สำหรับแยกสีดำ/ขาว (0-765, default: 382)\r\n * @returns {number[]} Array ของ Byte คำสั่ง ESC/POS\r\n *\r\n * @example\r\n * // QR Code (300x300px)\r\n * const qrCanvas = document.createElement('canvas');\r\n * await QRCode.toCanvas(qrCanvas, 'https://example.com', { width: 300 });\r\n * const imageData = qrCanvas.getContext('2d').getImageData(0, 0, 300, 300);\r\n * const bytes = convertCanvasToEscPos(imageData);\r\n */\r\nexport function convertCanvasToEscPos(imageData, brightnessThreshold = 382) {\r\n const width = imageData.width;\r\n const height = imageData.height;\r\n const pixels = imageData.data;\r\n\r\n // 1 Byte = 8 pixels แนวนอน (pad ให้ได้ width ที่หารด้วย 8 ลงตัว)\r\n const paddedWidth = Math.ceil(width / 8) * 8;\r\n const xBytes = paddedWidth / 8;\r\n\r\n let command = [];\r\n\r\n // Header: GS v 0 (Raster Bit Image)\r\n command.push(0x1d, 0x76, 0x30, 0x00);\r\n\r\n // บอกขนาดภาพ (Little Endian)\r\n command.push(xBytes % 256, Math.floor(xBytes / 256)); // xL, xH\r\n command.push(height % 256, Math.floor(height / 256)); // yL, yH\r\n\r\n // Loop ทุก Pixel แปลงเป็น Bit\r\n for (let y = 0; y < height; y++) {\r\n for (let x = 0; x < xBytes; x++) {\r\n let byte = 0;\r\n for (let bit = 0; bit < 8; bit++) {\r\n const pixelX = x * 8 + bit;\r\n\r\n // ถ้าเกินขอบภาพ ให้เป็นสีขาว (Bit=0)\r\n if (pixelX >= width) {\r\n continue;\r\n }\r\n\r\n // คำนวณตำแหน่ง Pixel ใน Array (RGBA = 4 ช่องต่อ 1 pixel)\r\n const pxIndex = (y * width + pixelX) * 4;\r\n\r\n // เช็คความสว่าง: R+G+B (ระหว่าง 0-765)\r\n // ถ้าค่าต่ำกว่า threshold ให้ถือเป็นสีดำ (Bit=1)\r\n const brightness =\r\n pixels[pxIndex] + // Red\r\n pixels[pxIndex + 1] + // Green\r\n pixels[pxIndex + 2]; // Blue\r\n // (ไม่ใช้ Alpha channel)\r\n\r\n if (brightness < brightnessThreshold) {\r\n byte |= 1 << (7 - bit);\r\n }\r\n }\r\n command.push(byte);\r\n }\r\n }\r\n\r\n return command;\r\n}\r\n/**\r\n * แปลงข้อความเป็น ImageData (Client-side only)\r\n * ต้องใช้ใน Browser environment เท่านั้น\r\n *\r\n * ⚠️ REQUIRED: Sarabun font must be loaded in your HTML:\r\n * <link href=\"https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap\" rel=\"stylesheet\">\r\n *\r\n * @param {string} text - Thai text to convert\r\n * @param {number} width - Canvas width in pixels (default: 384)\r\n * @param {number} fontSize - Font size in pixels (default: 22)\r\n * @returns {ImageData} Canvas ImageData object\r\n * @throws {Error} If not in browser environment\r\n *\r\n * @example\r\n * // Client-side (React/Next.js)\r\n * const imageData = textToImageData(\"สวัสดี\", 384, 22);\r\n * await fetch('/api/print', {\r\n * body: JSON.stringify({\r\n * data: Array.from(imageData.data),\r\n * width: imageData.width,\r\n * height: imageData.height,\r\n * })\r\n * });\r\n */\r\nexport function textToImageData(text, width = 384, fontSize = 22) {\r\n // ตรวจสอบว่าเป็น Browser environment\r\n if (typeof document === \"undefined\") {\r\n throw new Error(\r\n \"textToImageData() requires browser environment with Canvas API\",\r\n );\r\n }\r\n\r\n const canvas = document.createElement(\"canvas\");\r\n const ctx = canvas.getContext(\"2d\");\r\n\r\n // ตั้งค่า Font\r\n const fontFamily = \"'Sarabun', sans-serif\";\r\n const lineHeight = fontSize + 12;\r\n const lines = text.split(\"\\n\");\r\n\r\n // คำนวณความสูง\r\n const height = lines.length * lineHeight + 20;\r\n\r\n canvas.width = width;\r\n canvas.height = height;\r\n\r\n // วาดพื้นหลังขาว\r\n ctx.fillStyle = \"white\";\r\n ctx.fillRect(0, 0, canvas.width, canvas.height);\r\n\r\n // วาดข้อความ\r\n ctx.fillStyle = \"black\";\r\n ctx.font = `${fontSize}px ${fontFamily}`;\r\n ctx.textBaseline = \"top\";\r\n\r\n let y = 10;\r\n for (let line of lines) {\r\n ctx.fillText(line, 10, y);\r\n y += lineHeight;\r\n }\r\n\r\n // ดึง ImageData\r\n return ctx.getImageData(0, 0, width, height);\r\n}\r\n","// src/index.js\r\nimport { CMD } from \"./constants\";\r\nimport { convertCanvasToEscPos, textToImageData } from \"./utils\";\r\n\r\n/**\r\n * Thermal Printer Library for ESC/POS printers\r\n *\r\n * IMPORTANT: When using printThaiText() or textToImageData(), you must load the Sarabun font in your HTML:\r\n * <link href=\"https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap\" rel=\"stylesheet\">\r\n *\r\n * Without this link, the printer will use the default system font instead of Sarabun.\r\n */\r\nexport { textToImageData } from \"./utils\";\r\nexport class ThermalPrinter {\r\n constructor(width = 384) {\r\n this.driverApi = null; // เริ่มต้นยังไม่มี URL\r\n this.printerWidth = width; // 384 dots = 58mm, 576 dots = 80mm\r\n this.buffer = [];\r\n }\r\n\r\n // --- Basic Commands ---\r\n\r\n init() {\r\n this.buffer.push(...CMD.INIT);\r\n return this;\r\n }\r\n\r\n align(alignment = 1) {\r\n // 0=Left, 1=Center, 2=Right\r\n const cmd = [...CMD.ALIGN_LEFT];\r\n cmd[2] = alignment;\r\n this.buffer.push(...cmd);\r\n return this;\r\n }\r\n\r\n bold(enable = true) {\r\n this.buffer.push(...(enable ? CMD.BOLD_ON : CMD.BOLD_OFF));\r\n return this;\r\n }\r\n\r\n feed(lines = 1) {\r\n for (let i = 0; i < lines; i++) this.buffer.push(CMD.LF);\r\n return this;\r\n }\r\n\r\n cut(partial = false) {\r\n this.buffer.push(...(partial ? CMD.CUT_PARTIAL : CMD.CUT_FULL));\r\n return this;\r\n }\r\n\r\n line(text = \"\") {\r\n const encoder = new TextEncoder();\r\n const bytes = encoder.encode(text + \"\\n\");\r\n this.buffer.push(...bytes);\r\n return this;\r\n }\r\n\r\n newline(count = 1) {\r\n for (let i = 0; i < count; i++) {\r\n this.buffer.push(CMD.LF);\r\n }\r\n return this;\r\n }\r\n\r\n divider(char = \"-\", width = 32) {\r\n this.line(char.repeat(width));\r\n return this;\r\n }\r\n\r\n beep(times = 1, duration = 100) {\r\n for (let i = 0; i < times; i++) {\r\n this.buffer.push(...CMD.BEEP(duration));\r\n }\r\n return this;\r\n }\r\n\r\n drawerKick() {\r\n this.buffer.push(...CMD.DRAWER_KICK);\r\n return this;\r\n }\r\n\r\n raw(bytes) {\r\n this.buffer.push(...bytes);\r\n return this;\r\n }\r\n\r\n // --- Image & Thai Text Logic ---\r\n\r\n image(imageData) {\r\n const bytes = convertCanvasToEscPos(imageData);\r\n this.buffer.push(...bytes);\r\n return this;\r\n }\r\n\r\n /**\r\n * พิมพ์ข้อความภาษาไทย (Render เป็นรูปภาพอัตโนมัติ)\r\n *\r\n * ⚠️ REQUIRED: Sarabun font must be loaded in your HTML file:\r\n * <link href=\"https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap\" rel=\"stylesheet\">\r\n *\r\n * Without this link, the system will use the default font instead.\r\n *\r\n * @param {string} text - Thai text to print\r\n * @param {number} fontSize - Font size in pixels (default: 22)\r\n */\r\n printThaiText(text, fontSize = 22) {\r\n if (typeof document === \"undefined\") {\r\n console.error(\"Browser environment required for printThaiText\");\r\n return this;\r\n }\r\n\r\n const canvas = document.createElement(\"canvas\");\r\n const ctx = canvas.getContext(\"2d\");\r\n\r\n // ตั้งค่า Font\r\n const fontFamily = \"'Sarabun', sans-serif\";\r\n const lineHeight = fontSize + 12;\r\n const lines = text.split(\"\\n\");\r\n\r\n // คำนวณความสูง\r\n const height = lines.length * lineHeight + 20;\r\n\r\n canvas.width = this.printerWidth;\r\n canvas.height = height;\r\n\r\n // วาดพื้นหลังขาว\r\n ctx.fillStyle = \"white\";\r\n ctx.fillRect(0, 0, canvas.width, canvas.height);\r\n\r\n // วาดข้อความ\r\n ctx.fillStyle = \"black\";\r\n ctx.font = `${fontSize}px ${fontFamily}`;\r\n ctx.textBaseline = \"top\";\r\n\r\n let y = 10;\r\n for (let line of lines) {\r\n ctx.fillText(line, 10, y);\r\n y += lineHeight;\r\n }\r\n\r\n const imageData = ctx.getImageData(0, 0, this.printerWidth, height);\r\n return this.image(imageData);\r\n }\r\n\r\n // --- Execution ---\r\n\r\n clear() {\r\n this.buffer = [];\r\n return this;\r\n }\r\n\r\n getBuffer() {\r\n return new Uint8Array(this.buffer);\r\n }\r\n\r\n // --- 🔥 Auto Discovery & Print Logic 🔥 ---\r\n\r\n /**\r\n * วิ่งหา Driver ใน Port 9123-9130\r\n * (Lazy Mode: เรียกใช้เมื่อจำเป็น)\r\n */\r\n async findDriver() {\r\n // 1. ถ้ามี URL เดิมอยู่ ลองเช็คว่ายังอยู่ไหม (Ping)\r\n if (this.driverApi) {\r\n try {\r\n const res = await fetch(`${this.driverApi}/health`);\r\n if (res.ok) return true; // ยังอยู่ดี\r\n } catch (e) {\r\n console.warn(\"Driver connection lost, rescanning...\");\r\n this.driverApi = null;\r\n }\r\n }\r\n\r\n // 2. ถ้าไม่มี หรือหลุด ให้วนหาใหม่\r\n for (let port = 9123; port <= 9130; port++) {\r\n const url = `http://localhost:${port}`;\r\n try {\r\n // ตั้ง Timeout สั้นๆ (100ms) เพื่อความเร็ว\r\n const controller = new AbortController();\r\n const timeoutId = setTimeout(() => controller.abort(), 100);\r\n\r\n const res = await fetch(`${url}/health`, {\r\n signal: controller.signal,\r\n });\r\n clearTimeout(timeoutId);\r\n\r\n if (res.ok) {\r\n const data = await res.json();\r\n // เช็ค Signature ว่าใช่ Driver ของเราไหม\r\n if (data.service === \"CDH-Driver\") {\r\n this.driverApi = url;\r\n console.log(`✅ Driver found at: ${url}`);\r\n return true;\r\n }\r\n }\r\n } catch (e) {\r\n // Port ไม่ว่าง หรือไม่ใช่ของเรา -> ข้าม\r\n continue;\r\n }\r\n }\r\n console.error(\"❌ CDH Driver not found (Is .exe running?)\");\r\n return false;\r\n }\r\n\r\n async print(printerName) {\r\n // 1. หา Driver ก่อน (Lazy Discovery)\r\n const found = await this.findDriver();\r\n if (!found) {\r\n throw new Error(\r\n \"Cannot connect to Printer Driver. Please run the application.\",\r\n );\r\n }\r\n\r\n // 2. ถ้าไม่ระบุชื่อ Printer ให้ดึงตัวแรก\r\n if (!printerName) {\r\n const printers = await ThermalPrinter.getPrinters(this.driverApi);\r\n if (printers.length > 0) printerName = printers[0].name;\r\n else throw new Error(\"No printer found in Windows settings.\");\r\n }\r\n\r\n try {\r\n const bufferData = this.getBuffer();\r\n const encodedName = encodeURIComponent(printerName);\r\n\r\n const res = await fetch(\r\n `${this.driverApi}/print?printer=${encodedName}`,\r\n {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/octet-stream\" },\r\n body: bufferData,\r\n },\r\n );\r\n\r\n if (!res.ok) throw new Error(\"Driver returned error\");\r\n\r\n this.clear();\r\n return { success: true };\r\n } catch (e) {\r\n console.error(\"Print Failed:\", e);\r\n throw e;\r\n }\r\n }\r\n\r\n // Static Method: ต้องสร้าง instance ชั่วคราวไปหา driver\r\n static async getPrinters(overrideUrl = null) {\r\n let apiUrl = overrideUrl;\r\n\r\n if (!apiUrl) {\r\n const temp = new ThermalPrinter();\r\n const found = await temp.findDriver();\r\n if (found) apiUrl = temp.driverApi;\r\n else return [];\r\n }\r\n\r\n try {\r\n const res = await fetch(`${apiUrl}/printers`);\r\n return await res.json();\r\n } catch (e) {\r\n console.error(\"Get Printers Failed:\", e);\r\n return [];\r\n }\r\n }\r\n}\r\n"],"names":["CMD","duration","convertCanvasToEscPos","imageData","brightnessThreshold","width","height","pixels","xBytes","command","y","x","byte","bit","pixelX","pxIndex","textToImageData","text","fontSize","canvas","ctx","fontFamily","lineHeight","lines","line","ThermalPrinter","alignment","cmd","enable","i","partial","bytes","count","char","times","port","url","controller","timeoutId","res","printerName","printers","bufferData","encodedName","overrideUrl","apiUrl","temp"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
(function(
|
|
2
|
-
`),
|
|
3
|
-
`);return this.buffer.push(...
|
|
4
|
-
`),
|
|
1
|
+
(function(f,i){typeof exports=="object"&&typeof module<"u"?i(exports):typeof define=="function"&&define.amd?define(["exports"],i):(f=typeof globalThis<"u"?globalThis:f||self,i(f.ThermalPrinter={}))})(this,(function(f){"use strict";const i={ESC:27,GS:29,LF:10,INIT:[27,64],CUT_FULL:[29,86,66,0],CUT_PARTIAL:[29,86,65,0],BOLD_ON:[27,69,1],BOLD_OFF:[27,69,0],ALIGN_LEFT:[27,97,0],ALIGN_CENTER:[27,97,1],ALIGN_RIGHT:[27,97,2],DRAWER_KICK:[27,112,0,12,120],BEEP:(l=100)=>[27,66,l]};function g(l,t=382){const r=l.width,e=l.height,n=l.data,o=Math.ceil(r/8)*8/8;let s=[];s.push(29,118,48,0),s.push(o%256,Math.floor(o/256)),s.push(e%256,Math.floor(e/256));for(let h=0;h<e;h++)for(let c=0;c<o;c++){let u=0;for(let d=0;d<8;d++){const b=c*8+d;if(b>=r)continue;const x=(h*r+b)*4;n[x]+n[x+1]+n[x+2]<t&&(u|=1<<7-d)}s.push(u)}return s}function w(l,t=384,r=22){if(typeof document>"u")throw new Error("textToImageData() requires browser environment with Canvas API");const e=document.createElement("canvas"),n=e.getContext("2d"),a="'Sarabun', sans-serif",o=r+12,s=l.split(`
|
|
2
|
+
`),h=s.length*o+20;e.width=t,e.height=h,n.fillStyle="white",n.fillRect(0,0,e.width,e.height),n.fillStyle="black",n.font=`${r}px ${a}`,n.textBaseline="top";let c=10;for(let u of s)n.fillText(u,10,c),c+=o;return n.getImageData(0,0,t,h)}class p{constructor(t=384){this.driverApi=null,this.printerWidth=t,this.buffer=[]}init(){return this.buffer.push(...i.INIT),this}align(t=1){const r=[...i.ALIGN_LEFT];return r[2]=t,this.buffer.push(...r),this}bold(t=!0){return this.buffer.push(...t?i.BOLD_ON:i.BOLD_OFF),this}feed(t=1){for(let r=0;r<t;r++)this.buffer.push(i.LF);return this}cut(t=!1){return this.buffer.push(...t?i.CUT_PARTIAL:i.CUT_FULL),this}line(t=""){const e=new TextEncoder().encode(t+`
|
|
3
|
+
`);return this.buffer.push(...e),this}newline(t=1){for(let r=0;r<t;r++)this.buffer.push(i.LF);return this}divider(t="-",r=32){return this.line(t.repeat(r)),this}beep(t=1,r=100){for(let e=0;e<t;e++)this.buffer.push(...i.BEEP(r));return this}drawerKick(){return this.buffer.push(...i.DRAWER_KICK),this}raw(t){return this.buffer.push(...t),this}image(t){const r=g(t);return this.buffer.push(...r),this}printThaiText(t,r=22){if(typeof document>"u")return console.error("Browser environment required for printThaiText"),this;const e=document.createElement("canvas"),n=e.getContext("2d"),a="'Sarabun', sans-serif",o=r+12,s=t.split(`
|
|
4
|
+
`),h=s.length*o+20;e.width=this.printerWidth,e.height=h,n.fillStyle="white",n.fillRect(0,0,e.width,e.height),n.fillStyle="black",n.font=`${r}px ${a}`,n.textBaseline="top";let c=10;for(let d of s)n.fillText(d,10,c),c+=o;const u=n.getImageData(0,0,this.printerWidth,h);return this.image(u)}clear(){return this.buffer=[],this}getBuffer(){return new Uint8Array(this.buffer)}async findDriver(){if(this.driverApi)try{if((await fetch(`${this.driverApi}/health`)).ok)return!0}catch{console.warn("Driver connection lost, rescanning..."),this.driverApi=null}for(let t=9123;t<=9130;t++){const r=`http://localhost:${t}`;try{const e=new AbortController,n=setTimeout(()=>e.abort(),100),a=await fetch(`${r}/health`,{signal:e.signal});if(clearTimeout(n),a.ok&&(await a.json()).service==="CDH-Driver")return this.driverApi=r,console.log(`✅ Driver found at: ${r}`),!0}catch{continue}}return console.error("❌ CDH Driver not found (Is .exe running?)"),!1}async print(t){if(!await this.findDriver())throw new Error("Cannot connect to Printer Driver. Please run the application.");if(!t){const e=await p.getPrinters(this.driverApi);if(e.length>0)t=e[0].name;else throw new Error("No printer found in Windows settings.")}try{const e=this.getBuffer(),n=encodeURIComponent(t);if(!(await fetch(`${this.driverApi}/print?printer=${n}`,{method:"POST",headers:{"Content-Type":"application/octet-stream"},body:e})).ok)throw new Error("Driver returned error");return this.clear(),{success:!0}}catch(e){throw console.error("Print Failed:",e),e}}static async getPrinters(t=null){let r=t;if(!r){const e=new p;if(await e.findDriver())r=e.driverApi;else return[]}try{return await(await fetch(`${r}/printers`)).json()}catch(e){return console.error("Get Printers Failed:",e),[]}}}f.ThermalPrinter=p,f.textToImageData=w,Object.defineProperty(f,Symbol.toStringTag,{value:"Module"})}));
|
|
5
5
|
//# sourceMappingURL=thermal-printer.umd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thermal-printer.umd.js","sources":["../src/constants.js","../src/utils.js","../src/index.js"],"sourcesContent":["// src/constants.js\r\nexport const CMD = {\r\n ESC: 0x1b,\r\n GS: 0x1d,\r\n LF: 0x0a,\r\n\r\n // Commands\r\n INIT: [0x1b, 0x40], // Initialize printer\r\n CUT_FULL: [0x1d, 0x56, 0x42, 0x00], // Full cut\r\n CUT_PARTIAL: [0x1d, 0x56, 0x41, 0x00], // Partial cut\r\n\r\n // Formatting\r\n BOLD_ON: [0x1b, 0x45, 0x01],\r\n BOLD_OFF: [0x1b, 0x45, 0x00],\r\n\r\n ALIGN_LEFT: [0x1b, 0x61, 0x00],\r\n ALIGN_CENTER: [0x1b, 0x61, 0x01],\r\n ALIGN_RIGHT: [0x1b, 0x61, 0x02],\r\n\r\n // Cash Drawer\r\n DRAWER_KICK: [0x1b, 0x70, 0x00, 0x0c, 0x78],\r\n\r\n // Beep (ESC B - default 100ms)\r\n BEEP: (duration = 100) => [0x1b, 0x42, duration],\r\n};\r\n","// src/utils.js\r\n\r\n/**\r\n * แปลงข้อมูลภาพ (ImageData) เป็นคำสั่ง ESC/POS GS v 0\r\n * @param {ImageData} imageData\r\n * @returns {number[]} Array ของ Byte คำสั่ง\r\n */\r\nexport function convertCanvasToEscPos(imageData) {\r\n const width = imageData.width;\r\n const height = imageData.height;\r\n const pixels = imageData.data;\r\n\r\n // 1 Byte = 8 pixels แนวนอน\r\n const xBytes = width / 8;\r\n\r\n let command = [];\r\n\r\n // Header: GS v 0 (Raster Bit Image)\r\n command.push(0x1d, 0x76, 0x30, 0x00);\r\n\r\n // บอกขนาดภาพ (Little Endian)\r\n command.push(xBytes % 256, Math.floor(xBytes / 256)); // xL, xH\r\n command.push(height % 256, Math.floor(height / 256)); // yL, yH\r\n\r\n // Loop ทุก Pixel แปลงเป็น Bit\r\n for (let y = 0; y < height; y++) {\r\n for (let x = 0; x < xBytes; x++) {\r\n let byte = 0;\r\n for (let bit = 0; bit < 8; bit++) {\r\n // คำนวณตำแหน่ง Pixel ใน Array (RGBA = 4 ช่องต่อ 1 pixel)\r\n const pxIndex = (y * width + x * 8 + bit) * 4;\r\n\r\n // เช็คความสว่าง (R+G+B)\r\n // ถ้าค่าต่ำกว่า 380 (ค่อนข้างมืด) ให้ถือเป็นสีดำ (Bit=1)\r\n const brightness =\r\n pixels[pxIndex] + pixels[pxIndex + 1] + pixels[pxIndex + 2];\r\n if (brightness < 380) {\r\n byte |= 1 << (7 - bit);\r\n }\r\n }\r\n command.push(byte);\r\n }\r\n }\r\n\r\n return command;\r\n}\r\n/**\r\n * แปลงข้อความเป็น ImageData (Client-side only)\r\n * ต้องใช้ใน Browser environment เท่านั้น\r\n *\r\n * ⚠️ REQUIRED: Sarabun font must be loaded in your HTML:\r\n * <link href=\"https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap\" rel=\"stylesheet\">\r\n *\r\n * @param {string} text - Thai text to convert\r\n * @param {number} width - Canvas width in pixels (default: 384)\r\n * @param {number} fontSize - Font size in pixels (default: 22)\r\n * @returns {ImageData} Canvas ImageData object\r\n * @throws {Error} If not in browser environment\r\n *\r\n * @example\r\n * // Client-side (React/Next.js)\r\n * const imageData = textToImageData(\"สวัสดี\", 384, 22);\r\n * await fetch('/api/print', {\r\n * body: JSON.stringify({\r\n * data: Array.from(imageData.data),\r\n * width: imageData.width,\r\n * height: imageData.height,\r\n * })\r\n * });\r\n */\r\nexport function textToImageData(text, width = 384, fontSize = 22) {\r\n // ตรวจสอบว่าเป็น Browser environment\r\n if (typeof document === \"undefined\") {\r\n throw new Error(\r\n \"textToImageData() requires browser environment with Canvas API\",\r\n );\r\n }\r\n\r\n const canvas = document.createElement(\"canvas\");\r\n const ctx = canvas.getContext(\"2d\");\r\n\r\n // ตั้งค่า Font\r\n const fontFamily = \"'Sarabun', sans-serif\";\r\n const lineHeight = fontSize + 12;\r\n const lines = text.split(\"\\n\");\r\n\r\n // คำนวณความสูง\r\n const height = lines.length * lineHeight + 20;\r\n\r\n canvas.width = width;\r\n canvas.height = height;\r\n\r\n // วาดพื้นหลังขาว\r\n ctx.fillStyle = \"white\";\r\n ctx.fillRect(0, 0, canvas.width, canvas.height);\r\n\r\n // วาดข้อความ\r\n ctx.fillStyle = \"black\";\r\n ctx.font = `${fontSize}px ${fontFamily}`;\r\n ctx.textBaseline = \"top\";\r\n\r\n let y = 10;\r\n for (let line of lines) {\r\n ctx.fillText(line, 10, y);\r\n y += lineHeight;\r\n }\r\n\r\n // ดึง ImageData\r\n return ctx.getImageData(0, 0, width, height);\r\n}\r\n","// src/index.js\r\nimport { CMD } from \"./constants\";\r\nimport { convertCanvasToEscPos, textToImageData } from \"./utils\";\r\n\r\n/**\r\n * Thermal Printer Library for ESC/POS printers\r\n *\r\n * IMPORTANT: When using printThaiText() or textToImageData(), you must load the Sarabun font in your HTML:\r\n * <link href=\"https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap\" rel=\"stylesheet\">\r\n *\r\n * Without this link, the printer will use the default system font instead of Sarabun.\r\n */\r\nexport { textToImageData } from \"./utils\";\r\nexport class ThermalPrinter {\r\n constructor(driverApiUrl = \"http://localhost:9123\", width = 384) {\r\n this.driverApi = driverApiUrl;\r\n this.printerWidth = width; // 384 dots = 58mm, 576 dots = 80mm\r\n this.buffer = [];\r\n }\r\n\r\n // --- Basic Commands ---\r\n\r\n init() {\r\n this.buffer.push(...CMD.INIT);\r\n return this;\r\n }\r\n\r\n align(alignment = 1) {\r\n // 0=Left, 1=Center, 2=Right\r\n const cmd = [...CMD.ALIGN_LEFT];\r\n cmd[2] = alignment;\r\n this.buffer.push(...cmd);\r\n return this;\r\n }\r\n\r\n bold(enable = true) {\r\n this.buffer.push(...(enable ? CMD.BOLD_ON : CMD.BOLD_OFF));\r\n return this;\r\n }\r\n\r\n feed(lines = 1) {\r\n for (let i = 0; i < lines; i++) this.buffer.push(CMD.LF);\r\n return this;\r\n }\r\n\r\n cut(partial = false) {\r\n this.buffer.push(...(partial ? CMD.CUT_PARTIAL : CMD.CUT_FULL));\r\n return this;\r\n }\r\n\r\n line(text = \"\") {\r\n const encoder = new TextEncoder();\r\n const bytes = encoder.encode(text + \"\\n\");\r\n this.buffer.push(...bytes);\r\n return this;\r\n }\r\n\r\n newline(count = 1) {\r\n for (let i = 0; i < count; i++) {\r\n this.buffer.push(CMD.LF);\r\n }\r\n return this;\r\n }\r\n\r\n divider(char = \"-\", width = 32) {\r\n this.line(char.repeat(width));\r\n return this;\r\n }\r\n\r\n beep(times = 1, duration = 100) {\r\n for (let i = 0; i < times; i++) {\r\n this.buffer.push(...CMD.BEEP(duration));\r\n }\r\n return this;\r\n }\r\n\r\n drawerKick() {\r\n this.buffer.push(...CMD.DRAWER_KICK);\r\n return this;\r\n }\r\n\r\n raw(bytes) {\r\n this.buffer.push(...bytes);\r\n return this;\r\n }\r\n\r\n // --- Image & Thai Text Logic ---\r\n\r\n image(imageData) {\r\n const bytes = convertCanvasToEscPos(imageData);\r\n this.buffer.push(...bytes);\r\n return this;\r\n }\r\n\r\n /**\r\n * พิมพ์ข้อความภาษาไทย (Render เป็นรูปภาพอัตโนมัติ)\r\n *\r\n * ⚠️ REQUIRED: Sarabun font must be loaded in your HTML file:\r\n * <link href=\"https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap\" rel=\"stylesheet\">\r\n *\r\n * Without this link, the system will use the default font instead.\r\n *\r\n * @param {string} text - Thai text to print\r\n * @param {number} fontSize - Font size in pixels (default: 22)\r\n */\r\n printThaiText(text, fontSize = 22) {\r\n if (typeof document === \"undefined\") {\r\n console.error(\"Browser environment required for printThaiText\");\r\n return this;\r\n }\r\n\r\n const canvas = document.createElement(\"canvas\");\r\n const ctx = canvas.getContext(\"2d\");\r\n\r\n // ตั้งค่า Font\r\n const fontFamily = \"'Sarabun', sans-serif\";\r\n const lineHeight = fontSize + 12;\r\n const lines = text.split(\"\\n\");\r\n\r\n // คำนวณความสูง\r\n const height = lines.length * lineHeight + 20;\r\n\r\n canvas.width = this.printerWidth;\r\n canvas.height = height;\r\n\r\n // วาดพื้นหลังขาว\r\n ctx.fillStyle = \"white\";\r\n ctx.fillRect(0, 0, canvas.width, canvas.height);\r\n\r\n // วาดข้อความ\r\n ctx.fillStyle = \"black\";\r\n ctx.font = `${fontSize}px ${fontFamily}`;\r\n ctx.textBaseline = \"top\";\r\n\r\n let y = 10;\r\n for (let line of lines) {\r\n ctx.fillText(line, 10, y);\r\n y += lineHeight;\r\n }\r\n\r\n const imageData = ctx.getImageData(0, 0, this.printerWidth, height);\r\n return this.image(imageData);\r\n }\r\n\r\n // --- Execution ---\r\n\r\n clear() {\r\n this.buffer = [];\r\n return this;\r\n }\r\n\r\n getBuffer() {\r\n return new Uint8Array(this.buffer);\r\n }\r\n\r\n async print(printerName) {\r\n // ถ้าไม่ส่งชื่อมา ให้ลองดึงตัวแรก\r\n if (!printerName) {\r\n const printers = await ThermalPrinter.getPrinters(this.driverApi);\r\n if (printers.length > 0) printerName = printers[0];\r\n else throw new Error(\"No printer found.\");\r\n }\r\n\r\n try {\r\n const bufferData = this.getBuffer();\r\n const encodedName = encodeURIComponent(printerName);\r\n\r\n const res = await fetch(\r\n `${this.driverApi}/print?printer=${encodedName}`,\r\n {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/octet-stream\" },\r\n body: bufferData,\r\n },\r\n );\r\n\r\n if (!res.ok) throw new Error(\"Driver Error\");\r\n\r\n this.clear();\r\n return { success: true };\r\n } catch (e) {\r\n console.error(\"Print Failed:\", e);\r\n throw e;\r\n }\r\n }\r\n\r\n // --- Static Methods ---\r\n\r\n static async getPrinters(apiUrl = \"http://localhost:9123\") {\r\n try {\r\n const res = await fetch(`${apiUrl}/printers`);\r\n return await res.json();\r\n } catch (e) {\r\n console.error(\"Connection Error:\", e);\r\n return [];\r\n }\r\n }\r\n}\r\n"],"names":["CMD","duration","convertCanvasToEscPos","imageData","width","height","pixels","xBytes","command","y","x","byte","bit","pxIndex","textToImageData","text","fontSize","canvas","ctx","fontFamily","lineHeight","lines","line","ThermalPrinter","driverApiUrl","alignment","cmd","enable","i","partial","bytes","count","char","times","printerName","printers","bufferData","encodedName","e","apiUrl"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"thermal-printer.umd.js","sources":["../src/constants.js","../src/utils.js","../src/index.js"],"sourcesContent":["// src/constants.js\r\nexport const CMD = {\r\n ESC: 0x1b,\r\n GS: 0x1d,\r\n LF: 0x0a,\r\n\r\n // Commands\r\n INIT: [0x1b, 0x40], // Initialize printer\r\n CUT_FULL: [0x1d, 0x56, 0x42, 0x00], // Full cut\r\n CUT_PARTIAL: [0x1d, 0x56, 0x41, 0x00], // Partial cut\r\n\r\n // Formatting\r\n BOLD_ON: [0x1b, 0x45, 0x01],\r\n BOLD_OFF: [0x1b, 0x45, 0x00],\r\n\r\n ALIGN_LEFT: [0x1b, 0x61, 0x00],\r\n ALIGN_CENTER: [0x1b, 0x61, 0x01],\r\n ALIGN_RIGHT: [0x1b, 0x61, 0x02],\r\n\r\n // Cash Drawer\r\n DRAWER_KICK: [0x1b, 0x70, 0x00, 0x0c, 0x78],\r\n\r\n // Beep (ESC B - default 100ms)\r\n BEEP: (duration = 100) => [0x1b, 0x42, duration],\r\n};\r\n","// src/utils.js\r\n\r\n/**\r\n * แปลงข้อมูลภาพ (ImageData) เป็นคำสั่ง ESC/POS GS v 0\r\n * รองรับภาพ QR Code, Logo, Thai Text เป็นต้น\r\n *\r\n * @param {ImageData|Object} imageData - ImageData object หรือ ImageDataLike {data, width, height}\r\n * @param {number} brightnessThreshold - ค่าเกณฑ์สำหรับแยกสีดำ/ขาว (0-765, default: 382)\r\n * @returns {number[]} Array ของ Byte คำสั่ง ESC/POS\r\n *\r\n * @example\r\n * // QR Code (300x300px)\r\n * const qrCanvas = document.createElement('canvas');\r\n * await QRCode.toCanvas(qrCanvas, 'https://example.com', { width: 300 });\r\n * const imageData = qrCanvas.getContext('2d').getImageData(0, 0, 300, 300);\r\n * const bytes = convertCanvasToEscPos(imageData);\r\n */\r\nexport function convertCanvasToEscPos(imageData, brightnessThreshold = 382) {\r\n const width = imageData.width;\r\n const height = imageData.height;\r\n const pixels = imageData.data;\r\n\r\n // 1 Byte = 8 pixels แนวนอน (pad ให้ได้ width ที่หารด้วย 8 ลงตัว)\r\n const paddedWidth = Math.ceil(width / 8) * 8;\r\n const xBytes = paddedWidth / 8;\r\n\r\n let command = [];\r\n\r\n // Header: GS v 0 (Raster Bit Image)\r\n command.push(0x1d, 0x76, 0x30, 0x00);\r\n\r\n // บอกขนาดภาพ (Little Endian)\r\n command.push(xBytes % 256, Math.floor(xBytes / 256)); // xL, xH\r\n command.push(height % 256, Math.floor(height / 256)); // yL, yH\r\n\r\n // Loop ทุก Pixel แปลงเป็น Bit\r\n for (let y = 0; y < height; y++) {\r\n for (let x = 0; x < xBytes; x++) {\r\n let byte = 0;\r\n for (let bit = 0; bit < 8; bit++) {\r\n const pixelX = x * 8 + bit;\r\n\r\n // ถ้าเกินขอบภาพ ให้เป็นสีขาว (Bit=0)\r\n if (pixelX >= width) {\r\n continue;\r\n }\r\n\r\n // คำนวณตำแหน่ง Pixel ใน Array (RGBA = 4 ช่องต่อ 1 pixel)\r\n const pxIndex = (y * width + pixelX) * 4;\r\n\r\n // เช็คความสว่าง: R+G+B (ระหว่าง 0-765)\r\n // ถ้าค่าต่ำกว่า threshold ให้ถือเป็นสีดำ (Bit=1)\r\n const brightness =\r\n pixels[pxIndex] + // Red\r\n pixels[pxIndex + 1] + // Green\r\n pixels[pxIndex + 2]; // Blue\r\n // (ไม่ใช้ Alpha channel)\r\n\r\n if (brightness < brightnessThreshold) {\r\n byte |= 1 << (7 - bit);\r\n }\r\n }\r\n command.push(byte);\r\n }\r\n }\r\n\r\n return command;\r\n}\r\n/**\r\n * แปลงข้อความเป็น ImageData (Client-side only)\r\n * ต้องใช้ใน Browser environment เท่านั้น\r\n *\r\n * ⚠️ REQUIRED: Sarabun font must be loaded in your HTML:\r\n * <link href=\"https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap\" rel=\"stylesheet\">\r\n *\r\n * @param {string} text - Thai text to convert\r\n * @param {number} width - Canvas width in pixels (default: 384)\r\n * @param {number} fontSize - Font size in pixels (default: 22)\r\n * @returns {ImageData} Canvas ImageData object\r\n * @throws {Error} If not in browser environment\r\n *\r\n * @example\r\n * // Client-side (React/Next.js)\r\n * const imageData = textToImageData(\"สวัสดี\", 384, 22);\r\n * await fetch('/api/print', {\r\n * body: JSON.stringify({\r\n * data: Array.from(imageData.data),\r\n * width: imageData.width,\r\n * height: imageData.height,\r\n * })\r\n * });\r\n */\r\nexport function textToImageData(text, width = 384, fontSize = 22) {\r\n // ตรวจสอบว่าเป็น Browser environment\r\n if (typeof document === \"undefined\") {\r\n throw new Error(\r\n \"textToImageData() requires browser environment with Canvas API\",\r\n );\r\n }\r\n\r\n const canvas = document.createElement(\"canvas\");\r\n const ctx = canvas.getContext(\"2d\");\r\n\r\n // ตั้งค่า Font\r\n const fontFamily = \"'Sarabun', sans-serif\";\r\n const lineHeight = fontSize + 12;\r\n const lines = text.split(\"\\n\");\r\n\r\n // คำนวณความสูง\r\n const height = lines.length * lineHeight + 20;\r\n\r\n canvas.width = width;\r\n canvas.height = height;\r\n\r\n // วาดพื้นหลังขาว\r\n ctx.fillStyle = \"white\";\r\n ctx.fillRect(0, 0, canvas.width, canvas.height);\r\n\r\n // วาดข้อความ\r\n ctx.fillStyle = \"black\";\r\n ctx.font = `${fontSize}px ${fontFamily}`;\r\n ctx.textBaseline = \"top\";\r\n\r\n let y = 10;\r\n for (let line of lines) {\r\n ctx.fillText(line, 10, y);\r\n y += lineHeight;\r\n }\r\n\r\n // ดึง ImageData\r\n return ctx.getImageData(0, 0, width, height);\r\n}\r\n","// src/index.js\r\nimport { CMD } from \"./constants\";\r\nimport { convertCanvasToEscPos, textToImageData } from \"./utils\";\r\n\r\n/**\r\n * Thermal Printer Library for ESC/POS printers\r\n *\r\n * IMPORTANT: When using printThaiText() or textToImageData(), you must load the Sarabun font in your HTML:\r\n * <link href=\"https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap\" rel=\"stylesheet\">\r\n *\r\n * Without this link, the printer will use the default system font instead of Sarabun.\r\n */\r\nexport { textToImageData } from \"./utils\";\r\nexport class ThermalPrinter {\r\n constructor(width = 384) {\r\n this.driverApi = null; // เริ่มต้นยังไม่มี URL\r\n this.printerWidth = width; // 384 dots = 58mm, 576 dots = 80mm\r\n this.buffer = [];\r\n }\r\n\r\n // --- Basic Commands ---\r\n\r\n init() {\r\n this.buffer.push(...CMD.INIT);\r\n return this;\r\n }\r\n\r\n align(alignment = 1) {\r\n // 0=Left, 1=Center, 2=Right\r\n const cmd = [...CMD.ALIGN_LEFT];\r\n cmd[2] = alignment;\r\n this.buffer.push(...cmd);\r\n return this;\r\n }\r\n\r\n bold(enable = true) {\r\n this.buffer.push(...(enable ? CMD.BOLD_ON : CMD.BOLD_OFF));\r\n return this;\r\n }\r\n\r\n feed(lines = 1) {\r\n for (let i = 0; i < lines; i++) this.buffer.push(CMD.LF);\r\n return this;\r\n }\r\n\r\n cut(partial = false) {\r\n this.buffer.push(...(partial ? CMD.CUT_PARTIAL : CMD.CUT_FULL));\r\n return this;\r\n }\r\n\r\n line(text = \"\") {\r\n const encoder = new TextEncoder();\r\n const bytes = encoder.encode(text + \"\\n\");\r\n this.buffer.push(...bytes);\r\n return this;\r\n }\r\n\r\n newline(count = 1) {\r\n for (let i = 0; i < count; i++) {\r\n this.buffer.push(CMD.LF);\r\n }\r\n return this;\r\n }\r\n\r\n divider(char = \"-\", width = 32) {\r\n this.line(char.repeat(width));\r\n return this;\r\n }\r\n\r\n beep(times = 1, duration = 100) {\r\n for (let i = 0; i < times; i++) {\r\n this.buffer.push(...CMD.BEEP(duration));\r\n }\r\n return this;\r\n }\r\n\r\n drawerKick() {\r\n this.buffer.push(...CMD.DRAWER_KICK);\r\n return this;\r\n }\r\n\r\n raw(bytes) {\r\n this.buffer.push(...bytes);\r\n return this;\r\n }\r\n\r\n // --- Image & Thai Text Logic ---\r\n\r\n image(imageData) {\r\n const bytes = convertCanvasToEscPos(imageData);\r\n this.buffer.push(...bytes);\r\n return this;\r\n }\r\n\r\n /**\r\n * พิมพ์ข้อความภาษาไทย (Render เป็นรูปภาพอัตโนมัติ)\r\n *\r\n * ⚠️ REQUIRED: Sarabun font must be loaded in your HTML file:\r\n * <link href=\"https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap\" rel=\"stylesheet\">\r\n *\r\n * Without this link, the system will use the default font instead.\r\n *\r\n * @param {string} text - Thai text to print\r\n * @param {number} fontSize - Font size in pixels (default: 22)\r\n */\r\n printThaiText(text, fontSize = 22) {\r\n if (typeof document === \"undefined\") {\r\n console.error(\"Browser environment required for printThaiText\");\r\n return this;\r\n }\r\n\r\n const canvas = document.createElement(\"canvas\");\r\n const ctx = canvas.getContext(\"2d\");\r\n\r\n // ตั้งค่า Font\r\n const fontFamily = \"'Sarabun', sans-serif\";\r\n const lineHeight = fontSize + 12;\r\n const lines = text.split(\"\\n\");\r\n\r\n // คำนวณความสูง\r\n const height = lines.length * lineHeight + 20;\r\n\r\n canvas.width = this.printerWidth;\r\n canvas.height = height;\r\n\r\n // วาดพื้นหลังขาว\r\n ctx.fillStyle = \"white\";\r\n ctx.fillRect(0, 0, canvas.width, canvas.height);\r\n\r\n // วาดข้อความ\r\n ctx.fillStyle = \"black\";\r\n ctx.font = `${fontSize}px ${fontFamily}`;\r\n ctx.textBaseline = \"top\";\r\n\r\n let y = 10;\r\n for (let line of lines) {\r\n ctx.fillText(line, 10, y);\r\n y += lineHeight;\r\n }\r\n\r\n const imageData = ctx.getImageData(0, 0, this.printerWidth, height);\r\n return this.image(imageData);\r\n }\r\n\r\n // --- Execution ---\r\n\r\n clear() {\r\n this.buffer = [];\r\n return this;\r\n }\r\n\r\n getBuffer() {\r\n return new Uint8Array(this.buffer);\r\n }\r\n\r\n // --- 🔥 Auto Discovery & Print Logic 🔥 ---\r\n\r\n /**\r\n * วิ่งหา Driver ใน Port 9123-9130\r\n * (Lazy Mode: เรียกใช้เมื่อจำเป็น)\r\n */\r\n async findDriver() {\r\n // 1. ถ้ามี URL เดิมอยู่ ลองเช็คว่ายังอยู่ไหม (Ping)\r\n if (this.driverApi) {\r\n try {\r\n const res = await fetch(`${this.driverApi}/health`);\r\n if (res.ok) return true; // ยังอยู่ดี\r\n } catch (e) {\r\n console.warn(\"Driver connection lost, rescanning...\");\r\n this.driverApi = null;\r\n }\r\n }\r\n\r\n // 2. ถ้าไม่มี หรือหลุด ให้วนหาใหม่\r\n for (let port = 9123; port <= 9130; port++) {\r\n const url = `http://localhost:${port}`;\r\n try {\r\n // ตั้ง Timeout สั้นๆ (100ms) เพื่อความเร็ว\r\n const controller = new AbortController();\r\n const timeoutId = setTimeout(() => controller.abort(), 100);\r\n\r\n const res = await fetch(`${url}/health`, {\r\n signal: controller.signal,\r\n });\r\n clearTimeout(timeoutId);\r\n\r\n if (res.ok) {\r\n const data = await res.json();\r\n // เช็ค Signature ว่าใช่ Driver ของเราไหม\r\n if (data.service === \"CDH-Driver\") {\r\n this.driverApi = url;\r\n console.log(`✅ Driver found at: ${url}`);\r\n return true;\r\n }\r\n }\r\n } catch (e) {\r\n // Port ไม่ว่าง หรือไม่ใช่ของเรา -> ข้าม\r\n continue;\r\n }\r\n }\r\n console.error(\"❌ CDH Driver not found (Is .exe running?)\");\r\n return false;\r\n }\r\n\r\n async print(printerName) {\r\n // 1. หา Driver ก่อน (Lazy Discovery)\r\n const found = await this.findDriver();\r\n if (!found) {\r\n throw new Error(\r\n \"Cannot connect to Printer Driver. Please run the application.\",\r\n );\r\n }\r\n\r\n // 2. ถ้าไม่ระบุชื่อ Printer ให้ดึงตัวแรก\r\n if (!printerName) {\r\n const printers = await ThermalPrinter.getPrinters(this.driverApi);\r\n if (printers.length > 0) printerName = printers[0].name;\r\n else throw new Error(\"No printer found in Windows settings.\");\r\n }\r\n\r\n try {\r\n const bufferData = this.getBuffer();\r\n const encodedName = encodeURIComponent(printerName);\r\n\r\n const res = await fetch(\r\n `${this.driverApi}/print?printer=${encodedName}`,\r\n {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/octet-stream\" },\r\n body: bufferData,\r\n },\r\n );\r\n\r\n if (!res.ok) throw new Error(\"Driver returned error\");\r\n\r\n this.clear();\r\n return { success: true };\r\n } catch (e) {\r\n console.error(\"Print Failed:\", e);\r\n throw e;\r\n }\r\n }\r\n\r\n // Static Method: ต้องสร้าง instance ชั่วคราวไปหา driver\r\n static async getPrinters(overrideUrl = null) {\r\n let apiUrl = overrideUrl;\r\n\r\n if (!apiUrl) {\r\n const temp = new ThermalPrinter();\r\n const found = await temp.findDriver();\r\n if (found) apiUrl = temp.driverApi;\r\n else return [];\r\n }\r\n\r\n try {\r\n const res = await fetch(`${apiUrl}/printers`);\r\n return await res.json();\r\n } catch (e) {\r\n console.error(\"Get Printers Failed:\", e);\r\n return [];\r\n }\r\n }\r\n}\r\n"],"names":["CMD","duration","convertCanvasToEscPos","imageData","brightnessThreshold","width","height","pixels","xBytes","command","y","x","byte","bit","pixelX","pxIndex","textToImageData","text","fontSize","canvas","ctx","fontFamily","lineHeight","lines","line","ThermalPrinter","alignment","cmd","enable","i","partial","bytes","count","char","times","port","url","controller","timeoutId","res","printerName","printers","bufferData","encodedName","overrideUrl","apiUrl","temp"],"mappings":"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"}
|