@naphatjm/cdh-thermal-printer 1.0.4 → 1.0.5
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 +494 -59
- package/dist/index.d.ts +45 -14
- package/dist/thermal-printer.es.js +113 -77
- 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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NaphatJM
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -15,8 +15,9 @@ npm install @naphatjm/cdh-thermal-printer
|
|
|
15
15
|
```javascript
|
|
16
16
|
import { ThermalPrinter } from "@naphatjm/cdh-thermal-printer";
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// ✨ No need to specify driver URL - auto-discovers on localhost:9123-9130
|
|
19
|
+
const printer = new ThermalPrinter(384);
|
|
20
|
+
// 384 is paper width (58mm)
|
|
20
21
|
|
|
21
22
|
printer
|
|
22
23
|
.init()
|
|
@@ -31,24 +32,44 @@ printer
|
|
|
31
32
|
### Get Available Printers
|
|
32
33
|
|
|
33
34
|
```javascript
|
|
34
|
-
|
|
35
|
+
// Auto-discovers driver automatically
|
|
36
|
+
const printers = await ThermalPrinter.getPrinters();
|
|
35
37
|
console.log(printers);
|
|
36
38
|
```
|
|
37
39
|
|
|
38
40
|
**Response Example:**
|
|
39
41
|
|
|
40
42
|
```json
|
|
41
|
-
[
|
|
43
|
+
[
|
|
44
|
+
{
|
|
45
|
+
"name": "thermal printer",
|
|
46
|
+
"driver": "Generic / Text Only",
|
|
47
|
+
"port": "USB001",
|
|
48
|
+
"status_text": "Ready"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "HP LaserJet M1530 MFP Series PCL 6",
|
|
52
|
+
"driver": "HP LaserJet M1530 MFP Series PCL 6",
|
|
53
|
+
"port": "HPLaserJetM1536dnfMFP",
|
|
54
|
+
"status_text": "Ready"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "Microsoft Print to PDF",
|
|
58
|
+
"driver": "Microsoft Print To PDF",
|
|
59
|
+
"port": "PORTPROMPT:",
|
|
60
|
+
"status_text": "Ready"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
42
63
|
```
|
|
43
64
|
|
|
44
65
|
### Print Receipt (Full Example)
|
|
45
66
|
|
|
46
67
|
```javascript
|
|
47
|
-
const printer = new ThermalPrinter(
|
|
68
|
+
const printer = new ThermalPrinter(384);
|
|
48
69
|
|
|
49
|
-
// Get available printers
|
|
70
|
+
// Get available printers (auto-discovers driver)
|
|
50
71
|
const printers = await ThermalPrinter.getPrinters();
|
|
51
|
-
const printerName = printers[0];
|
|
72
|
+
const printerName = printers[0].name;
|
|
52
73
|
|
|
53
74
|
// Build receipt
|
|
54
75
|
printer
|
|
@@ -71,14 +92,14 @@ printer
|
|
|
71
92
|
.feed(3)
|
|
72
93
|
.cut();
|
|
73
94
|
|
|
74
|
-
// Send to printer
|
|
95
|
+
// Send to printer (auto-discovers driver)
|
|
75
96
|
await printer.print(printerName);
|
|
76
97
|
```
|
|
77
98
|
|
|
78
99
|
### Print Image (QR Code, Logo, etc.)
|
|
79
100
|
|
|
80
101
|
```javascript
|
|
81
|
-
const printer = new ThermalPrinter(
|
|
102
|
+
const printer = new ThermalPrinter(384);
|
|
82
103
|
|
|
83
104
|
// Create canvas and draw image
|
|
84
105
|
const canvas = document.createElement("canvas");
|
|
@@ -98,7 +119,7 @@ ctx.drawImage(qrImg, 150, 20, 100, 100);
|
|
|
98
119
|
// Get image data and print
|
|
99
120
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
100
121
|
|
|
101
|
-
// Send to printer
|
|
122
|
+
// Send to printer (auto-discovers driver)
|
|
102
123
|
const printers = await ThermalPrinter.getPrinters();
|
|
103
124
|
printer
|
|
104
125
|
.init()
|
|
@@ -106,14 +127,61 @@ printer
|
|
|
106
127
|
.image(imageData) // Print the image
|
|
107
128
|
.feed(2)
|
|
108
129
|
.cut()
|
|
109
|
-
.print(printers[0]);
|
|
130
|
+
.print(printers[0].name);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## ⚠️ Important: Font Setup for Thai Text
|
|
134
|
+
|
|
135
|
+
When using `printThaiText()` or `textToImageData()`, you **must** load the Sarabun font in your HTML file:
|
|
136
|
+
|
|
137
|
+
```html
|
|
138
|
+
<link
|
|
139
|
+
href="https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap"
|
|
140
|
+
rel="stylesheet" />
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Without this link, the printer will use the default system font instead of Sarabun.
|
|
144
|
+
|
|
145
|
+
## How Auto-Discovery Works
|
|
146
|
+
|
|
147
|
+
The library automatically scans for the printer driver on ports 9123-9130:
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
const printer = new ThermalPrinter(384);
|
|
151
|
+
|
|
152
|
+
// First call to print() triggers auto-discovery
|
|
153
|
+
// Scans localhost:9123 → 9130 for CDH-Driver service
|
|
154
|
+
// Caches the discovered URL for future calls
|
|
155
|
+
await printer.print("Printer Name");
|
|
156
|
+
|
|
157
|
+
// Subsequent calls reuse the cached URL
|
|
158
|
+
await printer.print("Another Printer");
|
|
159
|
+
|
|
160
|
+
// Reconnects automatically if connection is lost
|
|
110
161
|
```
|
|
111
162
|
|
|
112
|
-
|
|
163
|
+
**Manual Discovery (if needed):**
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
const printer = new ThermalPrinter(384);
|
|
167
|
+
|
|
168
|
+
// Explicitly find driver
|
|
169
|
+
const found = await printer.findDriver();
|
|
170
|
+
if (!found) {
|
|
171
|
+
console.error(
|
|
172
|
+
"Driver not found. Please run the printer driver application.",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Now proceed with printing
|
|
177
|
+
await printer.print("Printer Name");
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Converting Thai Text (Client-side Helper)
|
|
113
181
|
|
|
114
182
|
For **server-side printing** with Thai text, use the `textToImageData()` utility function to render text on the client, then send the image data to your server API.
|
|
115
183
|
|
|
116
|
-
|
|
184
|
+
### Client-side (React/Next.js):
|
|
117
185
|
|
|
118
186
|
```javascript
|
|
119
187
|
import { textToImageData } from "@naphatjm/cdh-thermal-printer";
|
|
@@ -136,7 +204,7 @@ const response = await fetch("/api/print", {
|
|
|
136
204
|
});
|
|
137
205
|
```
|
|
138
206
|
|
|
139
|
-
|
|
207
|
+
### Server-side (Next.js API Route):
|
|
140
208
|
|
|
141
209
|
```javascript
|
|
142
210
|
// pages/api/print.js
|
|
@@ -154,7 +222,7 @@ export default function handler(req, res) {
|
|
|
154
222
|
height: imageData.height,
|
|
155
223
|
};
|
|
156
224
|
|
|
157
|
-
// Print using server-side library
|
|
225
|
+
// Print using server-side library (auto-discovers driver)
|
|
158
226
|
const printer = new ThermalPrinter();
|
|
159
227
|
try {
|
|
160
228
|
printer.init().image(imageDataLike).feed(3).cut().print(printerName);
|
|
@@ -166,28 +234,387 @@ export default function handler(req, res) {
|
|
|
166
234
|
}
|
|
167
235
|
```
|
|
168
236
|
|
|
169
|
-
##
|
|
237
|
+
## ImageDataLike Interface
|
|
170
238
|
|
|
171
|
-
|
|
239
|
+
For **server-side usage**, the `image()` method accepts an object that matches this structure instead of requiring the native ImageData:
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
interface ImageDataLike {
|
|
243
|
+
data: Uint8ClampedArray | number[];
|
|
244
|
+
width: number;
|
|
245
|
+
height: number;
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Why?** Node.js doesn't have the `ImageData` constructor, so use a plain object instead:
|
|
250
|
+
|
|
251
|
+
```javascript
|
|
252
|
+
const imageDataLike = {
|
|
253
|
+
data: new Uint8ClampedArray([...pixelData]),
|
|
254
|
+
width: 384,
|
|
255
|
+
height: 100,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
printer.image(imageDataLike).print("Printer Name");
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Complete Next.js Example
|
|
262
|
+
|
|
263
|
+
### Step 1: Load Font in Layout
|
|
264
|
+
|
|
265
|
+
```html
|
|
266
|
+
<!-- app/layout.tsx -->
|
|
267
|
+
<head>
|
|
268
|
+
<link
|
|
269
|
+
href="https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap"
|
|
270
|
+
rel="stylesheet" />
|
|
271
|
+
</head>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Step 2: Client Page Component
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// app/print/page.tsx
|
|
278
|
+
"use client";
|
|
279
|
+
import { textToImageData } from "@naphatjm/cdh-thermal-printer";
|
|
280
|
+
import { useState } from "react";
|
|
281
|
+
|
|
282
|
+
export default function PrintPage() {
|
|
283
|
+
const [loading, setLoading] = useState(false);
|
|
284
|
+
|
|
285
|
+
const handlePrint = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
286
|
+
e.preventDefault();
|
|
287
|
+
setLoading(true);
|
|
288
|
+
|
|
289
|
+
const text = (e.currentTarget.elements.namedItem("text") as HTMLInputElement).value;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
// 1. Client-side: Convert Thai text to image
|
|
293
|
+
const imageData = textToImageData(text, 384, 22);
|
|
294
|
+
|
|
295
|
+
// 2. Send to API
|
|
296
|
+
const response = await fetch("/api/print", {
|
|
297
|
+
method: "POST",
|
|
298
|
+
headers: { "Content-Type": "application/json" },
|
|
299
|
+
body: JSON.stringify({
|
|
300
|
+
imageData: {
|
|
301
|
+
data: Array.from(imageData.data),
|
|
302
|
+
width: imageData.width,
|
|
303
|
+
height: imageData.height,
|
|
304
|
+
},
|
|
305
|
+
printerName: "EPSON TM-58",
|
|
306
|
+
}),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const result = await response.json();
|
|
310
|
+
alert(result.message || "✅ Print successful");
|
|
311
|
+
} catch (error) {
|
|
312
|
+
alert(`❌ Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
313
|
+
} finally {
|
|
314
|
+
setLoading(false);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<div style={{ padding: "20px" }}>
|
|
320
|
+
<h1>🖨️ Print Thai Text</h1>
|
|
321
|
+
<form onSubmit={handlePrint}>
|
|
322
|
+
<textarea
|
|
323
|
+
name="text"
|
|
324
|
+
placeholder="Enter Thai text to print..."
|
|
325
|
+
defaultValue="สวัสดีครับ"
|
|
326
|
+
style={{ width: "100%", height: "100px" }}
|
|
327
|
+
/>
|
|
328
|
+
<br />
|
|
329
|
+
<button type="submit" disabled={loading}>
|
|
330
|
+
{loading ? "⏳ Printing..." : "🖨️ Print"}
|
|
331
|
+
</button>
|
|
332
|
+
</form>
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Step 3: API Route Handler
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
// app/api/print/route.ts
|
|
342
|
+
import { ThermalPrinter } from "@naphatjm/cdh-thermal-printer";
|
|
343
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
344
|
+
|
|
345
|
+
export async function POST(req: NextRequest) {
|
|
346
|
+
try {
|
|
347
|
+
const { imageData, printerName } = await req.json();
|
|
348
|
+
|
|
349
|
+
// Convert JSON to ImageDataLike
|
|
350
|
+
const imageDataLike = {
|
|
351
|
+
data: new Uint8ClampedArray(imageData.data),
|
|
352
|
+
width: imageData.width,
|
|
353
|
+
height: imageData.height,
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// Use server-side printer
|
|
357
|
+
const printer = new ThermalPrinter();
|
|
358
|
+
printer.init().image(imageDataLike).feed(3).cut();
|
|
359
|
+
|
|
360
|
+
await printer.print(printerName);
|
|
361
|
+
|
|
362
|
+
return NextResponse.json({ message: "✅ Print successful" });
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error("Print error:", error);
|
|
365
|
+
return NextResponse.json(
|
|
366
|
+
{ error: error instanceof Error ? error.message : "Unknown error" },
|
|
367
|
+
{ status: 500 },
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Customization Options
|
|
374
|
+
|
|
375
|
+
### What You Can Customize
|
|
376
|
+
|
|
377
|
+
| Feature | Parameter/Method | Options | Default |
|
|
378
|
+
| --------------------- | ---------------------------------------- | -------------------------------- | --------------------- |
|
|
379
|
+
| **Paper Width** | Constructor `width` | 384 (58mm) / 576 (80mm) / custom | 384 |
|
|
380
|
+
| **API URL** | Constructor `driverApiUrl` | Any URL | http://localhost:9123 |
|
|
381
|
+
| **Font Size (Thai)** | `printThaiText(text, fontSize)` | Any number (px) | 22 |
|
|
382
|
+
| **Font Size (Image)** | `textToImageData(text, width, fontSize)` | Any number (px) | 22 |
|
|
383
|
+
| **Canvas Width** | `textToImageData(text, width)` | Any number (px) | 384 |
|
|
384
|
+
| **Text Alignment** | `align(alignment)` | 0=Left, 1=Center, 2=Right | 1 (Center) |
|
|
385
|
+
| **Bold Text** | `bold(enable)` | true / false | true |
|
|
386
|
+
| **Paper Feed** | `feed(lines)` | Any number | 1 |
|
|
387
|
+
| **Cut Type** | `cut(partial)` | true=Partial, false=Full | false (Full) |
|
|
388
|
+
| **Divider Style** | `divider(char, width)` | Any char + count | "-", 32 |
|
|
389
|
+
| **Custom Text** | `line(text)` | Any text | "" |
|
|
390
|
+
| **Blank Lines** | `newline(count)` | Any number | 1 |
|
|
391
|
+
| **Beep Sound** | `beep(times, duration)` | times + ms | 1, 100ms |
|
|
392
|
+
| **Custom Image** | `image(imageData)` | ImageData / ImageDataLike | - |
|
|
393
|
+
| **Raw Commands** | `raw(bytes)` | ESC/POS bytes | - |
|
|
394
|
+
|
|
395
|
+
### What's Fixed (For Compatibility)
|
|
396
|
+
|
|
397
|
+
| Feature | Value | Reason |
|
|
398
|
+
| ------------------------------ | ---------------- | ------------------------- |
|
|
399
|
+
| **Thai Font** | Sarabun only | Consistency & readability |
|
|
400
|
+
| **Line Height** | fontSize + 12px | Standard spacing |
|
|
401
|
+
| **Pixel Brightness Threshold** | 380 | Black/White conversion |
|
|
402
|
+
| **Cash Drawer Pins** | 0x00, 0x0c, 0x78 | ESC/POS standard |
|
|
403
|
+
|
|
404
|
+
### Examples of Customization
|
|
405
|
+
|
|
406
|
+
#### Custom Paper Width (80mm)
|
|
407
|
+
|
|
408
|
+
```javascript
|
|
409
|
+
const printer = new ThermalPrinter(576); // 80mm instead of 58mm (default 384)
|
|
410
|
+
|
|
411
|
+
printer.init().line("Wide receipt").feed(2).cut().print("Printer");
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
#### Custom Font Size for Thai Text
|
|
415
|
+
|
|
416
|
+
```javascript
|
|
417
|
+
const printer = new ThermalPrinter();
|
|
418
|
+
|
|
419
|
+
printer
|
|
420
|
+
.init()
|
|
421
|
+
.bold(true)
|
|
422
|
+
.printThaiText("ขนาดใหญ่", 28) // 28px instead of default 22px
|
|
423
|
+
.bold(false)
|
|
424
|
+
.divider("*", 40) // Custom divider
|
|
425
|
+
.feed(3)
|
|
426
|
+
.cut()
|
|
427
|
+
.print("Printer");
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
#### Custom Divider and Alignment
|
|
431
|
+
|
|
432
|
+
```javascript
|
|
433
|
+
const printer = new ThermalPrinter();
|
|
434
|
+
|
|
435
|
+
printer
|
|
436
|
+
.init()
|
|
437
|
+
.align(2) // Right align
|
|
438
|
+
.divider("=", 30) // Different style
|
|
439
|
+
.line("Item 1 100.-")
|
|
440
|
+
.line("Item 2 200.-")
|
|
441
|
+
.divider("-", 20) // Different width
|
|
442
|
+
.align(1) // Center align
|
|
443
|
+
.line("Total: 300.-")
|
|
444
|
+
.feed(5)
|
|
445
|
+
.cut(true) // Partial cut instead of full
|
|
446
|
+
.print("Printer");
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
#### Mixed Customizations
|
|
450
|
+
|
|
451
|
+
```javascript
|
|
452
|
+
const printer = new ThermalPrinter(576); // 80mm (auto-discovers driver)
|
|
453
|
+
|
|
454
|
+
printer
|
|
455
|
+
.init()
|
|
456
|
+
.align(1) // Center
|
|
457
|
+
.bold(true)
|
|
458
|
+
.printThaiText("ร้านค้า XYZ", 26) // Bigger font
|
|
459
|
+
.bold(false)
|
|
460
|
+
.divider("=", 50)
|
|
461
|
+
.line("พิมพ์บันทึก")
|
|
462
|
+
.divider("-", 40)
|
|
463
|
+
.line("สินค้า 1 150.-")
|
|
464
|
+
.line("สินค้า 2 250.-")
|
|
465
|
+
.line("สินค้า 3 100.-")
|
|
466
|
+
.divider("-", 40)
|
|
467
|
+
.align(2) // Right align total
|
|
468
|
+
.bold(true)
|
|
469
|
+
.line("รวม: 500.-")
|
|
470
|
+
.bold(false)
|
|
471
|
+
.align(1)
|
|
472
|
+
.newline(2)
|
|
473
|
+
.line("ขอบคุณที่มาใช้บริการ")
|
|
474
|
+
.feed(4)
|
|
475
|
+
.cut(true) // Partial cut
|
|
476
|
+
.beep(1, 200) // Single beep
|
|
477
|
+
.print("Receipt Printer");
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
## Usage Patterns
|
|
481
|
+
|
|
482
|
+
### Pattern 1: Client-side Direct Printing
|
|
483
|
+
|
|
484
|
+
```javascript
|
|
485
|
+
// Simple, all processing on client
|
|
486
|
+
printer.init().printThaiText("สวัสดี").feed(2).cut().print("Printer Name");
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Pattern 2: Client Render → Server Print (Recommended for production)
|
|
490
|
+
|
|
491
|
+
```javascript
|
|
492
|
+
// 1. Client renders
|
|
493
|
+
const imageData = textToImageData("สวัสดี", 384, 22);
|
|
494
|
+
|
|
495
|
+
// 2. Send to server
|
|
496
|
+
await fetch("/api/print", { body: JSON.stringify(imageData) });
|
|
497
|
+
|
|
498
|
+
// 3. Server prints (more secure & flexible)
|
|
499
|
+
printer.image(imageDataLike).print("Printer Name");
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Pattern 3: Custom Image Printing
|
|
503
|
+
|
|
504
|
+
```javascript
|
|
505
|
+
// Use with QR codes, logos, or custom images
|
|
506
|
+
const canvas = document.createElement("canvas");
|
|
507
|
+
const ctx = canvas.getContext("2d");
|
|
508
|
+
// ... draw custom content ...
|
|
509
|
+
const imageData = ctx.getImageData(0, 0, 384, 200);
|
|
510
|
+
|
|
511
|
+
printer.image(imageData).print("Printer Name");
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
## Usage Matrix (Quick Reference)
|
|
515
|
+
|
|
516
|
+
| Scenario | Method | Location | Example |
|
|
517
|
+
| ---------------------- | ------------------------------- | ------------------------------ | ------------------------------------ |
|
|
518
|
+
| **English text** | `line()` | Server or Client | `printer.line("Hello")` |
|
|
519
|
+
| **Thai text (direct)** | `printThaiText()` | Client only | `printer.printThaiText("สวัสดี")` |
|
|
520
|
+
| **Thai text (API)** | `textToImageData()` + `image()` | Client converts, Server prints | See Complete Example |
|
|
521
|
+
| **Image/QR Code** | `image()` | Server or Client | `printer.image(imageData)` |
|
|
522
|
+
| **Get printers** | `getPrinters()` | Server or Client | `await ThermalPrinter.getPrinters()` |
|
|
523
|
+
|
|
524
|
+
## ⚠️ Common Issues & Troubleshooting
|
|
525
|
+
|
|
526
|
+
### Issue: "Browser environment required for printThaiText"
|
|
527
|
+
|
|
528
|
+
**Problem:** Using `printThaiText()` on server-side (Node.js)
|
|
529
|
+
|
|
530
|
+
**Solution:** Use `textToImageData()` on client instead
|
|
531
|
+
|
|
532
|
+
```javascript
|
|
533
|
+
// ❌ WRONG - Server can't use printThaiText
|
|
534
|
+
printer.printThaiText("สวัสดี");
|
|
535
|
+
|
|
536
|
+
// ✅ RIGHT - Client converts, server prints
|
|
537
|
+
const imageData = textToImageData("สวัสดี");
|
|
538
|
+
// ... send to server ...
|
|
539
|
+
printer.image(imageDataLike);
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Issue: Only "@" symbols print instead of Thai text
|
|
543
|
+
|
|
544
|
+
**Problem:** Sending Thai text directly without rendering to image
|
|
545
|
+
|
|
546
|
+
**Solution:** Always convert Thai text to image first
|
|
547
|
+
|
|
548
|
+
```javascript
|
|
549
|
+
// ❌ WRONG
|
|
550
|
+
printer.line("สวัสดี"); // Will print as gibberish
|
|
551
|
+
|
|
552
|
+
// ✅ RIGHT
|
|
553
|
+
printer.printThaiText("สวัสดี"); // Client-side
|
|
554
|
+
// OR
|
|
555
|
+
const imageData = textToImageData("สวัสดี"); // Convert first
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Issue: Thai text looks like default font, not Sarabun
|
|
559
|
+
|
|
560
|
+
**Problem:** Sarabun font not loaded in HTML
|
|
561
|
+
|
|
562
|
+
**Solution:** Add font link to your HTML `<head>`
|
|
172
563
|
|
|
173
564
|
```html
|
|
565
|
+
<!-- ✅ Add this to your HTML -->
|
|
174
566
|
<link
|
|
175
567
|
href="https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap"
|
|
176
568
|
rel="stylesheet" />
|
|
177
569
|
```
|
|
178
570
|
|
|
179
|
-
|
|
571
|
+
### Issue: "ImageData is not defined" in Node.js
|
|
572
|
+
|
|
573
|
+
**Problem:** Trying to use browser ImageData in Node.js
|
|
574
|
+
|
|
575
|
+
**Solution:** Use ImageDataLike object instead
|
|
576
|
+
|
|
577
|
+
```javascript
|
|
578
|
+
// ❌ WRONG - ImageData doesn't exist in Node.js
|
|
579
|
+
const img = new ImageData(...);
|
|
580
|
+
|
|
581
|
+
// ✅ RIGHT - Use plain object
|
|
582
|
+
const imageDataLike = {
|
|
583
|
+
data: new Uint8ClampedArray([...]),
|
|
584
|
+
width: 384,
|
|
585
|
+
height: 100,
|
|
586
|
+
};
|
|
587
|
+
printer.image(imageDataLike);
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### Issue: TextToImageData throws error on server
|
|
591
|
+
|
|
592
|
+
**Problem:** `textToImageData()` requires browser Canvas API
|
|
593
|
+
|
|
594
|
+
**Solution:** Always call `textToImageData()` on client-side only
|
|
595
|
+
|
|
596
|
+
```javascript
|
|
597
|
+
// ❌ WRONG - Server has no Canvas
|
|
598
|
+
// (server-side code)
|
|
599
|
+
const imageData = textToImageData("สวัสดี");
|
|
600
|
+
|
|
601
|
+
// ✅ RIGHT - Client calls it
|
|
602
|
+
// (client-side React component)
|
|
603
|
+
const imageData = textToImageData("สวัสดี");
|
|
604
|
+
await fetch("/api/print", { body: JSON.stringify(imageData) });
|
|
605
|
+
```
|
|
180
606
|
|
|
181
607
|
## API Reference
|
|
182
608
|
|
|
183
609
|
### Constructor
|
|
184
610
|
|
|
185
611
|
```javascript
|
|
186
|
-
new ThermalPrinter((
|
|
612
|
+
new ThermalPrinter((width = 384));
|
|
187
613
|
```
|
|
188
614
|
|
|
189
|
-
- `
|
|
190
|
-
|
|
615
|
+
- `width`: Printer width in dots (384 = 58mm, 576 = 80mm, default: 384)
|
|
616
|
+
|
|
617
|
+
The driver URL is **auto-discovered** on ports 9123-9130 when first needed.
|
|
191
618
|
|
|
192
619
|
### Utility Functions
|
|
193
620
|
|
|
@@ -239,17 +666,43 @@ printer.image(imageData).print();
|
|
|
239
666
|
|
|
240
667
|
- `clear()` - Clear all buffered commands
|
|
241
668
|
- `getBuffer()` - Get current buffer as Uint8Array
|
|
242
|
-
- `
|
|
669
|
+
- `findDriver()` - Auto-discover printer driver on localhost:9123-9130, caches result
|
|
670
|
+
- `print(printerName)` - Send buffered commands to printer (triggers auto-discovery if needed)
|
|
243
671
|
|
|
244
672
|
### Static Methods
|
|
245
673
|
|
|
246
|
-
- `static async getPrinters(
|
|
674
|
+
- `static async getPrinters(overrideUrl)` - Get list of available printers from Windows/system
|
|
247
675
|
|
|
248
|
-
**Returns:** Array of
|
|
676
|
+
**Returns:** Array of PrinterInfo objects
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
interface PrinterInfo {
|
|
680
|
+
name: string;
|
|
681
|
+
driver: string;
|
|
682
|
+
port: string;
|
|
683
|
+
status_text: string;
|
|
684
|
+
}
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
**Example:**
|
|
249
688
|
|
|
250
689
|
```javascript
|
|
251
|
-
const printers = await ThermalPrinter.getPrinters(
|
|
252
|
-
// [
|
|
690
|
+
const printers = await ThermalPrinter.getPrinters();
|
|
691
|
+
// [
|
|
692
|
+
// { name: "thermal printer", driver: "Generic / Text Only", port: "USB001", status_text: "Ready" },
|
|
693
|
+
// { name: "HP LaserJet M1530 MFP Series PCL 6", driver: "HP LaserJet M1530 MFP Series PCL 6", ... },
|
|
694
|
+
// ...
|
|
695
|
+
// ]
|
|
696
|
+
|
|
697
|
+
// Filter for thermal printers only
|
|
698
|
+
const thermalPrinters = printers.filter((p) =>
|
|
699
|
+
p.name.toLowerCase().includes("thermal"),
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
// Or with override URL (for custom server)
|
|
703
|
+
const printers = await ThermalPrinter.getPrinters(
|
|
704
|
+
"http://custom-driver:9123",
|
|
705
|
+
);
|
|
253
706
|
```
|
|
254
707
|
|
|
255
708
|
## Fluent API
|
|
@@ -268,42 +721,24 @@ printer
|
|
|
268
721
|
.cut();
|
|
269
722
|
```
|
|
270
723
|
|
|
271
|
-
##
|
|
724
|
+
## Environment Notes
|
|
272
725
|
|
|
273
|
-
###
|
|
726
|
+
### Client-side (Browser)
|
|
274
727
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
### Pattern 2: Client Render → Server Print (Recommended for production)
|
|
728
|
+
✅ Has Canvas API
|
|
729
|
+
✅ Has DOM (document, window)
|
|
730
|
+
✅ Has Fonts (Sarabun)
|
|
731
|
+
✅ Has ImageData constructor
|
|
732
|
+
**Use:** `printThaiText()`, `textToImageData()`
|
|
281
733
|
|
|
282
|
-
|
|
283
|
-
// 1. Client renders
|
|
284
|
-
const imageData = textToImageData("สวัสดี", 384, 22);
|
|
734
|
+
### Server-side (Node.js)
|
|
285
735
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
### Pattern 3: Custom Image Printing
|
|
294
|
-
|
|
295
|
-
```javascript
|
|
296
|
-
// Use with QR codes, logos, or custom images
|
|
297
|
-
const canvas = document.createElement("canvas");
|
|
298
|
-
const ctx = canvas.getContext("2d");
|
|
299
|
-
// ... draw custom content ...
|
|
300
|
-
const imageData = ctx.getImageData(0, 0, 384, 200);
|
|
301
|
-
|
|
302
|
-
printer.image(imageData).print("Printer Name");
|
|
303
|
-
```
|
|
736
|
+
❌ No Canvas API
|
|
737
|
+
❌ No DOM
|
|
738
|
+
❌ No Fonts
|
|
739
|
+
❌ No ImageData constructor
|
|
740
|
+
**Use:** `image()` with ImageDataLike, `image()` with image data from client
|
|
304
741
|
|
|
305
|
-
##
|
|
742
|
+
## License
|
|
306
743
|
|
|
307
|
-
|
|
308
|
-
- Active connection to thermal printer driver API (default: http://localhost:9123)
|
|
309
|
-
- Sarabun font loaded in HTML for Thai text rendering
|
|
744
|
+
MIT
|
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 h = {
|
|
2
2
|
ESC: 27,
|
|
3
3
|
GS: 29,
|
|
4
4
|
LF: 10,
|
|
@@ -18,86 +18,86 @@ 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
|
|
23
|
+
function x(l) {
|
|
24
|
+
const t = l.width, r = l.height, e = l.data, n = t / 8;
|
|
25
|
+
let i = [];
|
|
26
|
+
i.push(29, 118, 48, 0), i.push(n % 256, Math.floor(n / 256)), i.push(r % 256, Math.floor(r / 256));
|
|
27
|
+
for (let s = 0; s < r; s++)
|
|
28
28
|
for (let c = 0; c < n; c++) {
|
|
29
|
-
let
|
|
30
|
-
for (let
|
|
31
|
-
const
|
|
32
|
-
|
|
29
|
+
let a = 0;
|
|
30
|
+
for (let o = 0; o < 8; o++) {
|
|
31
|
+
const f = (s * t + c * 8 + o) * 4;
|
|
32
|
+
e[f] + e[f + 1] + e[f + 2] < 380 && (a |= 1 << 7 - o);
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
i.push(a);
|
|
35
35
|
}
|
|
36
|
-
return
|
|
36
|
+
return i;
|
|
37
37
|
}
|
|
38
|
-
function p(
|
|
38
|
+
function p(l, t = 384, r = 22) {
|
|
39
39
|
if (typeof document > "u")
|
|
40
40
|
throw new Error(
|
|
41
41
|
"textToImageData() requires browser environment with Canvas API"
|
|
42
42
|
);
|
|
43
|
-
const
|
|
44
|
-
`),
|
|
45
|
-
|
|
46
|
-
let
|
|
47
|
-
for (let
|
|
48
|
-
n.fillText(
|
|
49
|
-
return n.getImageData(0, 0,
|
|
43
|
+
const e = document.createElement("canvas"), n = e.getContext("2d"), i = "'Sarabun', sans-serif", s = r + 12, c = l.split(`
|
|
44
|
+
`), a = c.length * s + 20;
|
|
45
|
+
e.width = t, e.height = a, n.fillStyle = "white", n.fillRect(0, 0, e.width, e.height), n.fillStyle = "black", n.font = `${r}px ${i}`, n.textBaseline = "top";
|
|
46
|
+
let o = 10;
|
|
47
|
+
for (let f of c)
|
|
48
|
+
n.fillText(f, 10, o), o += s;
|
|
49
|
+
return n.getImageData(0, 0, t, a);
|
|
50
50
|
}
|
|
51
|
-
class
|
|
52
|
-
constructor(
|
|
53
|
-
this.driverApi =
|
|
51
|
+
class u {
|
|
52
|
+
constructor(t = 384) {
|
|
53
|
+
this.driverApi = null, this.printerWidth = t, this.buffer = [];
|
|
54
54
|
}
|
|
55
55
|
// --- Basic Commands ---
|
|
56
56
|
init() {
|
|
57
|
-
return this.buffer.push(...
|
|
57
|
+
return this.buffer.push(...h.INIT), this;
|
|
58
58
|
}
|
|
59
|
-
align(
|
|
60
|
-
const
|
|
61
|
-
return
|
|
59
|
+
align(t = 1) {
|
|
60
|
+
const r = [...h.ALIGN_LEFT];
|
|
61
|
+
return r[2] = t, this.buffer.push(...r), this;
|
|
62
62
|
}
|
|
63
|
-
bold(
|
|
64
|
-
return this.buffer.push(...
|
|
63
|
+
bold(t = !0) {
|
|
64
|
+
return this.buffer.push(...t ? h.BOLD_ON : h.BOLD_OFF), this;
|
|
65
65
|
}
|
|
66
|
-
feed(
|
|
67
|
-
for (let
|
|
66
|
+
feed(t = 1) {
|
|
67
|
+
for (let r = 0; r < t; r++) this.buffer.push(h.LF);
|
|
68
68
|
return this;
|
|
69
69
|
}
|
|
70
|
-
cut(
|
|
71
|
-
return this.buffer.push(...
|
|
70
|
+
cut(t = !1) {
|
|
71
|
+
return this.buffer.push(...t ? h.CUT_PARTIAL : h.CUT_FULL), this;
|
|
72
72
|
}
|
|
73
|
-
line(
|
|
74
|
-
const
|
|
73
|
+
line(t = "") {
|
|
74
|
+
const e = new TextEncoder().encode(t + `
|
|
75
75
|
`);
|
|
76
|
-
return this.buffer.push(...
|
|
76
|
+
return this.buffer.push(...e), this;
|
|
77
77
|
}
|
|
78
|
-
newline(
|
|
79
|
-
for (let
|
|
80
|
-
this.buffer.push(
|
|
78
|
+
newline(t = 1) {
|
|
79
|
+
for (let r = 0; r < t; r++)
|
|
80
|
+
this.buffer.push(h.LF);
|
|
81
81
|
return this;
|
|
82
82
|
}
|
|
83
|
-
divider(
|
|
84
|
-
return this.line(
|
|
83
|
+
divider(t = "-", r = 32) {
|
|
84
|
+
return this.line(t.repeat(r)), this;
|
|
85
85
|
}
|
|
86
|
-
beep(
|
|
87
|
-
for (let
|
|
88
|
-
this.buffer.push(...
|
|
86
|
+
beep(t = 1, r = 100) {
|
|
87
|
+
for (let e = 0; e < t; e++)
|
|
88
|
+
this.buffer.push(...h.BEEP(r));
|
|
89
89
|
return this;
|
|
90
90
|
}
|
|
91
91
|
drawerKick() {
|
|
92
|
-
return this.buffer.push(...
|
|
92
|
+
return this.buffer.push(...h.DRAWER_KICK), this;
|
|
93
93
|
}
|
|
94
|
-
raw(
|
|
95
|
-
return this.buffer.push(...
|
|
94
|
+
raw(t) {
|
|
95
|
+
return this.buffer.push(...t), this;
|
|
96
96
|
}
|
|
97
97
|
// --- Image & Thai Text Logic ---
|
|
98
|
-
image(
|
|
99
|
-
const
|
|
100
|
-
return this.buffer.push(...
|
|
98
|
+
image(t) {
|
|
99
|
+
const r = x(t);
|
|
100
|
+
return this.buffer.push(...r), this;
|
|
101
101
|
}
|
|
102
102
|
/**
|
|
103
103
|
* พิมพ์ข้อความภาษาไทย (Render เป็นรูปภาพอัตโนมัติ)
|
|
@@ -110,17 +110,17 @@ class x {
|
|
|
110
110
|
* @param {string} text - Thai text to print
|
|
111
111
|
* @param {number} fontSize - Font size in pixels (default: 22)
|
|
112
112
|
*/
|
|
113
|
-
printThaiText(
|
|
113
|
+
printThaiText(t, r = 22) {
|
|
114
114
|
if (typeof document > "u")
|
|
115
115
|
return console.error("Browser environment required for printThaiText"), this;
|
|
116
|
-
const
|
|
117
|
-
`),
|
|
118
|
-
|
|
119
|
-
let
|
|
120
|
-
for (let
|
|
121
|
-
n.fillText(
|
|
122
|
-
const
|
|
123
|
-
return this.image(
|
|
116
|
+
const e = document.createElement("canvas"), n = e.getContext("2d"), i = "'Sarabun', sans-serif", s = r + 12, c = t.split(`
|
|
117
|
+
`), a = c.length * s + 20;
|
|
118
|
+
e.width = this.printerWidth, e.height = a, n.fillStyle = "white", n.fillRect(0, 0, e.width, e.height), n.fillStyle = "black", n.font = `${r}px ${i}`, n.textBaseline = "top";
|
|
119
|
+
let o = 10;
|
|
120
|
+
for (let d of c)
|
|
121
|
+
n.fillText(d, 10, o), o += s;
|
|
122
|
+
const f = n.getImageData(0, 0, this.printerWidth, a);
|
|
123
|
+
return this.image(f);
|
|
124
124
|
}
|
|
125
125
|
// --- Execution ---
|
|
126
126
|
clear() {
|
|
@@ -129,38 +129,74 @@ class x {
|
|
|
129
129
|
getBuffer() {
|
|
130
130
|
return new Uint8Array(this.buffer);
|
|
131
131
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
132
|
+
// --- 🔥 Auto Discovery & Print Logic 🔥 ---
|
|
133
|
+
/**
|
|
134
|
+
* วิ่งหา Driver ใน Port 9123-9130
|
|
135
|
+
* (Lazy Mode: เรียกใช้เมื่อจำเป็น)
|
|
136
|
+
*/
|
|
137
|
+
async findDriver() {
|
|
138
|
+
if (this.driverApi)
|
|
139
|
+
try {
|
|
140
|
+
if ((await fetch(`${this.driverApi}/health`)).ok) return !0;
|
|
141
|
+
} catch {
|
|
142
|
+
console.warn("Driver connection lost, rescanning..."), this.driverApi = null;
|
|
143
|
+
}
|
|
144
|
+
for (let t = 9123; t <= 9130; t++) {
|
|
145
|
+
const r = `http://localhost:${t}`;
|
|
146
|
+
try {
|
|
147
|
+
const e = new AbortController(), n = setTimeout(() => e.abort(), 100), i = await fetch(`${r}/health`, {
|
|
148
|
+
signal: e.signal
|
|
149
|
+
});
|
|
150
|
+
if (clearTimeout(n), i.ok && (await i.json()).service === "CDH-Driver")
|
|
151
|
+
return this.driverApi = r, console.log(`✅ Driver found at: ${r}`), !0;
|
|
152
|
+
} catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return console.error("❌ CDH Driver not found (Is .exe running?)"), !1;
|
|
157
|
+
}
|
|
158
|
+
async print(t) {
|
|
159
|
+
if (!await this.findDriver())
|
|
160
|
+
throw new Error(
|
|
161
|
+
"Cannot connect to Printer Driver. Please run the application."
|
|
162
|
+
);
|
|
163
|
+
if (!t) {
|
|
164
|
+
const e = await u.getPrinters(this.driverApi);
|
|
165
|
+
if (e.length > 0) t = e[0].name;
|
|
166
|
+
else throw new Error("No printer found in Windows settings.");
|
|
137
167
|
}
|
|
138
168
|
try {
|
|
139
|
-
const
|
|
169
|
+
const e = this.getBuffer(), n = encodeURIComponent(t);
|
|
140
170
|
if (!(await fetch(
|
|
141
|
-
`${this.driverApi}/print?printer=${
|
|
171
|
+
`${this.driverApi}/print?printer=${n}`,
|
|
142
172
|
{
|
|
143
173
|
method: "POST",
|
|
144
174
|
headers: { "Content-Type": "application/octet-stream" },
|
|
145
|
-
body:
|
|
175
|
+
body: e
|
|
146
176
|
}
|
|
147
|
-
)).ok) throw new Error("Driver
|
|
177
|
+
)).ok) throw new Error("Driver returned error");
|
|
148
178
|
return this.clear(), { success: !0 };
|
|
149
|
-
} catch (
|
|
150
|
-
throw console.error("Print Failed:",
|
|
179
|
+
} catch (e) {
|
|
180
|
+
throw console.error("Print Failed:", e), e;
|
|
151
181
|
}
|
|
152
182
|
}
|
|
153
|
-
//
|
|
154
|
-
static async getPrinters(
|
|
183
|
+
// Static Method: ต้องสร้าง instance ชั่วคราวไปหา driver
|
|
184
|
+
static async getPrinters(t = null) {
|
|
185
|
+
let r = t;
|
|
186
|
+
if (!r) {
|
|
187
|
+
const e = new u();
|
|
188
|
+
if (await e.findDriver()) r = e.driverApi;
|
|
189
|
+
else return [];
|
|
190
|
+
}
|
|
155
191
|
try {
|
|
156
|
-
return await (await fetch(`${
|
|
157
|
-
} catch (
|
|
158
|
-
return console.error("
|
|
192
|
+
return await (await fetch(`${r}/printers`)).json();
|
|
193
|
+
} catch (e) {
|
|
194
|
+
return console.error("Get Printers Failed:", e), [];
|
|
159
195
|
}
|
|
160
196
|
}
|
|
161
197
|
}
|
|
162
198
|
export {
|
|
163
|
-
|
|
199
|
+
u as ThermalPrinter,
|
|
164
200
|
p as textToImageData
|
|
165
201
|
};
|
|
166
202
|
//# 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 * @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(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","width","height","pixels","xBytes","command","y","x","byte","bit","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 p(l){const t=l.width,r=l.height,e=l.data,n=t/8;let s=[];s.push(29,118,48,0),s.push(n%256,Math.floor(n/256)),s.push(r%256,Math.floor(r/256));for(let o=0;o<r;o++)for(let c=0;c<n;c++){let a=0;for(let h=0;h<8;h++){const u=(o*t+c*8+h)*4;e[u]+e[u+1]+e[u+2]<380&&(a|=1<<7-h)}s.push(a)}return s}function b(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"),s="'Sarabun', sans-serif",o=r+12,c=l.split(`
|
|
2
|
+
`),a=c.length*o+20;e.width=t,e.height=a,n.fillStyle="white",n.fillRect(0,0,e.width,e.height),n.fillStyle="black",n.font=`${r}px ${s}`,n.textBaseline="top";let h=10;for(let u of c)n.fillText(u,10,h),h+=o;return n.getImageData(0,0,t,a)}class d{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=p(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"),s="'Sarabun', sans-serif",o=r+12,c=t.split(`
|
|
4
|
+
`),a=c.length*o+20;e.width=this.printerWidth,e.height=a,n.fillStyle="white",n.fillRect(0,0,e.width,e.height),n.fillStyle="black",n.font=`${r}px ${s}`,n.textBaseline="top";let h=10;for(let x of c)n.fillText(x,10,h),h+=o;const u=n.getImageData(0,0,this.printerWidth,a);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),s=await fetch(`${r}/health`,{signal:e.signal});if(clearTimeout(n),s.ok&&(await s.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 d.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 d;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=d,f.textToImageData=b,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 * @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(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","width","height","pixels","xBytes","command","y","x","byte","bit","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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"}
|