@naphatjm/cdh-thermal-printer 1.0.3 → 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 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
- const printer = new ThermalPrinter("http://localhost:9123", 384);
19
- // 384 is papar width
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
- const printers = await ThermalPrinter.getPrinters("http://localhost:9123");
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
- ["EPSON TM-58 (Model 109)", "Star Micronics TSP100", "Generic ESC/POS Printer"]
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("http://localhost:9123", 384);
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("http://localhost:9123", 384);
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
- ### Convert Thai Text to Image (Client-side Helper)
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
- #### Client-side (React/Next.js):
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
- #### Server-side (Next.js API Route):
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
- ## ⚠️ Important: Font Setup for Thai Text
237
+ ## ImageDataLike Interface
170
238
 
171
- When using `printThaiText()` or `textToImageData()`, you **must** load the Sarabun font in your HTML file:
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
- Without this link, the printer will use the default system font instead of Sarabun.
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((driverApiUrl = "http://localhost:9123"), (width = 384));
612
+ new ThermalPrinter((width = 384));
187
613
  ```
188
614
 
189
- - `driverApiUrl`: URL of the thermal printer driver API
190
- - `width`: Printer width in dots (384 = 58mm, 576 = 80mm)
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
- - `print(printerName)` - Send buffered commands to printer
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(apiUrl)` - Get list of available printers
674
+ - `static async getPrinters(overrideUrl)` - Get list of available printers from Windows/system
247
675
 
248
- **Returns:** Array of printer names (strings)
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("http://localhost:9123");
252
- // ["EPSON TM-58", "Star Micronics TSP100", ...]
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
- ## Usage Patterns
724
+ ## Environment Notes
272
725
 
273
- ### Pattern 1: Client-side Direct Printing
726
+ ### Client-side (Browser)
274
727
 
275
- ```javascript
276
- // Simple, all processing on client
277
- printer.init().printThaiText("สวัสดี").feed(2).cut().print("Printer Name");
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
- ```javascript
283
- // 1. Client renders
284
- const imageData = textToImageData("สวัสดี", 384, 22);
734
+ ### Server-side (Node.js)
285
735
 
286
- // 2. Send to server
287
- await fetch("/api/print", { body: JSON.stringify(imageData) });
288
-
289
- // 3. Server prints (more secure & flexible)
290
- printer.image(imageDataLike).print("Printer Name");
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
- ## Requirements
742
+ ## License
306
743
 
307
- - Modern browser with Canvas API support (for `printThaiText()` and `textToImageData()`)
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
@@ -12,33 +12,87 @@ export interface PrintResponse {
12
12
  }
13
13
 
14
14
  /**
15
- * ThermalPrinter Service
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
+
24
+ /**
25
+ * Convert Thai text to ImageData (Client-side only)
26
+ *
27
+ * ⚠️ REQUIRED: Sarabun font must be loaded in your HTML:
28
+ * ```html
29
+ * <link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@400;700&display=swap" rel="stylesheet">
30
+ * ```
31
+ *
32
+ * @param text - Thai text to convert
33
+ * @param width - Canvas width in pixels (default: 384 = 58mm)
34
+ * @param fontSize - Font size in pixels (default: 22)
35
+ * @returns ImageData object ready for printing
36
+ * @throws Error if not in browser environment
37
+ *
38
+ * @example
39
+ * ```javascript
40
+ * // Client-side
41
+ * const imageData = textToImageData("สวัสดี", 384, 22);
42
+ * await fetch('/api/print', {
43
+ * body: JSON.stringify({
44
+ * data: Array.from(imageData.data),
45
+ * width: imageData.width,
46
+ * height: imageData.height,
47
+ * })
48
+ * });
49
+ *
50
+ * // Server-side (API route)
51
+ * const printer = new ThermalPrinter();
52
+ * printer
53
+ * .init()
54
+ * .image({
55
+ * data: new Uint8ClampedArray(req.body.data),
56
+ * width: req.body.width,
57
+ * height: req.body.height,
58
+ * })
59
+ * .print("Printer Name");
60
+ * ```
61
+ */
62
+ export function textToImageData(
63
+ text: string,
64
+ width?: number,
65
+ fontSize?: number,
66
+ ): ImageData;
67
+
68
+ /**
69
+ * ThermalPrinter Service with Auto-Discovery
16
70
  * Fluent API for controlling thermal printer with ESC/POS commands
71
+ * Automatically discovers printer driver on ports 9123-9130
17
72
  *
18
73
  * @example
19
74
  * ```javascript
20
- * const printer = new ThermalPrinter("http://localhost:9123", 384);
75
+ * const printer = new ThermalPrinter(384); // 384 dots = 58mm (default)
21
76
  * printer
22
77
  * .init()
23
78
  * .align(1)
24
79
  * .printThaiText("สวัสดี")
25
80
  * .feed(2)
26
81
  * .cut()
27
- * .print("Printer Name");
82
+ * .print("Printer Name"); // Auto-discovers driver
28
83
  * ```
29
84
  */
30
85
  export class ThermalPrinter {
31
86
  /**
32
- * Initialize ThermalPrinter instance
33
- * @param driverApiUrl - URL of the thermal printer driver API (default: http://localhost:9123)
87
+ * Initialize ThermalPrinter instance with auto-discovery
34
88
  * @param width - Printer width in dots (384 = 58mm, 576 = 80mm, default: 384)
35
89
  */
36
- constructor(driverApiUrl?: string, width?: number);
90
+ constructor(width?: number);
37
91
 
38
92
  /**
39
- * Driver API URL
93
+ * Driver API URL (null until driver is discovered)
40
94
  */
41
- readonly driverApi: string;
95
+ driverApi: string | null;
42
96
 
43
97
  /**
44
98
  * Printer width in dots
@@ -167,21 +221,42 @@ export class ThermalPrinter {
167
221
  getBuffer(): Uint8Array;
168
222
 
169
223
  /**
170
- * Print to specific printer
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)
171
235
  * @param printerName - Name of printer to print to (optional, will use first available if not provided)
172
236
  * @returns Promise resolving to print response
173
- * @throws Error if printer not found or print failed
237
+ * @throws Error if driver not found or print failed
174
238
  */
175
239
  print(printerName?: string): Promise<PrintResponse>;
176
240
 
177
241
  // --- Static Methods ---
178
242
 
179
243
  /**
180
- * Get list of available printers
181
- * @param apiUrl - Driver API URL (default: http://localhost:9123)
182
- * @returns Promise resolving to array of printer names
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
+ * ```
183
258
  */
184
- static getPrinters(apiUrl?: string): Promise<string[]>;
259
+ static getPrinters(overrideUrl?: string): Promise<PrinterInfo[]>;
185
260
  }
186
261
 
187
262
  export {};
@@ -1,4 +1,4 @@
1
- const i = {
1
+ const h = {
2
2
  ESC: 27,
3
3
  GS: 29,
4
4
  LF: 10,
@@ -18,73 +18,86 @@ const i = {
18
18
  // Cash Drawer
19
19
  DRAWER_KICK: [27, 112, 0, 12, 120],
20
20
  // Beep (ESC B - default 100ms)
21
- BEEP: (f = 100) => [27, 66, f]
21
+ BEEP: (l = 100) => [27, 66, l]
22
22
  };
23
- function p(f) {
24
- const e = f.width, t = f.height, r = f.data, s = e / 8;
25
- let n = [];
26
- n.push(29, 118, 48, 0), n.push(s % 256, Math.floor(s / 256)), n.push(t % 256, Math.floor(t / 256));
27
- for (let o = 0; o < t; o++)
28
- for (let c = 0; c < s; c++) {
29
- let u = 0;
30
- for (let h = 0; h < 8; h++) {
31
- const a = (o * e + c * 8 + h) * 4;
32
- r[a] + r[a + 1] + r[a + 2] < 380 && (u |= 1 << 7 - h);
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
+ for (let c = 0; c < n; c++) {
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
- n.push(u);
34
+ i.push(a);
35
35
  }
36
- return n;
36
+ return i;
37
37
  }
38
- class l {
39
- constructor(e = "http://localhost:9123", t = 384) {
40
- this.driverApi = e, this.printerWidth = t, this.buffer = [];
38
+ function p(l, t = 384, r = 22) {
39
+ if (typeof document > "u")
40
+ throw new Error(
41
+ "textToImageData() requires browser environment with Canvas API"
42
+ );
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
+ }
51
+ class u {
52
+ constructor(t = 384) {
53
+ this.driverApi = null, this.printerWidth = t, this.buffer = [];
41
54
  }
42
55
  // --- Basic Commands ---
43
56
  init() {
44
- return this.buffer.push(...i.INIT), this;
57
+ return this.buffer.push(...h.INIT), this;
45
58
  }
46
- align(e = 1) {
47
- const t = [...i.ALIGN_LEFT];
48
- return t[2] = e, this.buffer.push(...t), this;
59
+ align(t = 1) {
60
+ const r = [...h.ALIGN_LEFT];
61
+ return r[2] = t, this.buffer.push(...r), this;
49
62
  }
50
- bold(e = !0) {
51
- return this.buffer.push(...e ? i.BOLD_ON : i.BOLD_OFF), this;
63
+ bold(t = !0) {
64
+ return this.buffer.push(...t ? h.BOLD_ON : h.BOLD_OFF), this;
52
65
  }
53
- feed(e = 1) {
54
- for (let t = 0; t < e; t++) this.buffer.push(i.LF);
66
+ feed(t = 1) {
67
+ for (let r = 0; r < t; r++) this.buffer.push(h.LF);
55
68
  return this;
56
69
  }
57
- cut(e = !1) {
58
- return this.buffer.push(...e ? i.CUT_PARTIAL : i.CUT_FULL), this;
70
+ cut(t = !1) {
71
+ return this.buffer.push(...t ? h.CUT_PARTIAL : h.CUT_FULL), this;
59
72
  }
60
- line(e = "") {
61
- const r = new TextEncoder().encode(e + `
73
+ line(t = "") {
74
+ const e = new TextEncoder().encode(t + `
62
75
  `);
63
- return this.buffer.push(...r), this;
76
+ return this.buffer.push(...e), this;
64
77
  }
65
- newline(e = 1) {
66
- for (let t = 0; t < e; t++)
67
- this.buffer.push(i.LF);
78
+ newline(t = 1) {
79
+ for (let r = 0; r < t; r++)
80
+ this.buffer.push(h.LF);
68
81
  return this;
69
82
  }
70
- divider(e = "-", t = 32) {
71
- return this.line(e.repeat(t)), this;
83
+ divider(t = "-", r = 32) {
84
+ return this.line(t.repeat(r)), this;
72
85
  }
73
- beep(e = 1, t = 100) {
74
- for (let r = 0; r < e; r++)
75
- this.buffer.push(...i.BEEP(t));
86
+ beep(t = 1, r = 100) {
87
+ for (let e = 0; e < t; e++)
88
+ this.buffer.push(...h.BEEP(r));
76
89
  return this;
77
90
  }
78
91
  drawerKick() {
79
- return this.buffer.push(...i.DRAWER_KICK), this;
92
+ return this.buffer.push(...h.DRAWER_KICK), this;
80
93
  }
81
- raw(e) {
82
- return this.buffer.push(...e), this;
94
+ raw(t) {
95
+ return this.buffer.push(...t), this;
83
96
  }
84
97
  // --- Image & Thai Text Logic ---
85
- image(e) {
86
- const t = p(e);
87
- return this.buffer.push(...t), this;
98
+ image(t) {
99
+ const r = x(t);
100
+ return this.buffer.push(...r), this;
88
101
  }
89
102
  /**
90
103
  * พิมพ์ข้อความภาษาไทย (Render เป็นรูปภาพอัตโนมัติ)
@@ -97,17 +110,17 @@ class l {
97
110
  * @param {string} text - Thai text to print
98
111
  * @param {number} fontSize - Font size in pixels (default: 22)
99
112
  */
100
- printThaiText(e, t = 22) {
113
+ printThaiText(t, r = 22) {
101
114
  if (typeof document > "u")
102
115
  return console.error("Browser environment required for printThaiText"), this;
103
- const r = document.createElement("canvas"), s = r.getContext("2d"), n = "'Sarabun', sans-serif", o = t + 12, c = e.split(`
104
- `), u = c.length * o + 20;
105
- r.width = this.printerWidth, r.height = u, s.fillStyle = "white", s.fillRect(0, 0, r.width, r.height), s.fillStyle = "black", s.font = `${t}px ${n}`, s.textBaseline = "top";
106
- let h = 10;
107
- for (let x of c)
108
- s.fillText(x, 10, h), h += o;
109
- const a = s.getImageData(0, 0, this.printerWidth, u);
110
- return this.image(a);
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);
111
124
  }
112
125
  // --- Execution ---
113
126
  clear() {
@@ -116,37 +129,74 @@ class l {
116
129
  getBuffer() {
117
130
  return new Uint8Array(this.buffer);
118
131
  }
119
- async print(e) {
120
- if (!e) {
121
- const t = await l.getPrinters(this.driverApi);
122
- if (t.length > 0) e = t[0];
123
- else throw new Error("No printer found.");
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.");
124
167
  }
125
168
  try {
126
- const t = this.getBuffer(), r = encodeURIComponent(e);
169
+ const e = this.getBuffer(), n = encodeURIComponent(t);
127
170
  if (!(await fetch(
128
- `${this.driverApi}/print?printer=${r}`,
171
+ `${this.driverApi}/print?printer=${n}`,
129
172
  {
130
173
  method: "POST",
131
174
  headers: { "Content-Type": "application/octet-stream" },
132
- body: t
175
+ body: e
133
176
  }
134
- )).ok) throw new Error("Driver Error");
177
+ )).ok) throw new Error("Driver returned error");
135
178
  return this.clear(), { success: !0 };
136
- } catch (t) {
137
- throw console.error("Print Failed:", t), t;
179
+ } catch (e) {
180
+ throw console.error("Print Failed:", e), e;
138
181
  }
139
182
  }
140
- // --- Static Methods ---
141
- static async getPrinters(e = "http://localhost:9123") {
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
+ }
142
191
  try {
143
- return await (await fetch(`${e}/printers`)).json();
144
- } catch (t) {
145
- return console.error("Connection Error:", t), [];
192
+ return await (await fetch(`${r}/printers`)).json();
193
+ } catch (e) {
194
+ return console.error("Get Printers Failed:", e), [];
146
195
  }
147
196
  }
148
197
  }
149
198
  export {
150
- l as ThermalPrinter
199
+ u as ThermalPrinter,
200
+ p as textToImageData
151
201
  };
152
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","// src/index.js\r\nimport { CMD } from \"./constants\";\r\nimport { convertCanvasToEscPos } from \"./utils\";\r\n\r\n/**\r\n * Thermal Printer Library for ESC/POS printers\r\n *\r\n * IMPORTANT: When using printThaiText(), 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 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","ThermalPrinter","driverApiUrl","alignment","cmd","enable","lines","i","partial","text","bytes","count","char","times","fontSize","canvas","ctx","fontFamily","lineHeight","line","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,4 +1,5 @@
1
- (function(h,i){typeof exports=="object"&&typeof module<"u"?i(exports):typeof define=="function"&&define.amd?define(["exports"],i):(h=typeof globalThis<"u"?globalThis:h||self,i(h.ThermalPrinter={}))})(this,(function(h){"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:(u=100)=>[27,66,u]};function p(u){const e=u.width,t=u.height,r=u.data,n=e/8;let s=[];s.push(29,118,48,0),s.push(n%256,Math.floor(n/256)),s.push(t%256,Math.floor(t/256));for(let f=0;f<t;f++)for(let c=0;c<n;c++){let l=0;for(let o=0;o<8;o++){const a=(f*e+c*8+o)*4;r[a]+r[a+1]+r[a+2]<380&&(l|=1<<7-o)}s.push(l)}return s}class x{constructor(e="http://localhost:9123",t=384){this.driverApi=e,this.printerWidth=t,this.buffer=[]}init(){return this.buffer.push(...i.INIT),this}align(e=1){const t=[...i.ALIGN_LEFT];return t[2]=e,this.buffer.push(...t),this}bold(e=!0){return this.buffer.push(...e?i.BOLD_ON:i.BOLD_OFF),this}feed(e=1){for(let t=0;t<e;t++)this.buffer.push(i.LF);return this}cut(e=!1){return this.buffer.push(...e?i.CUT_PARTIAL:i.CUT_FULL),this}line(e=""){const r=new TextEncoder().encode(e+`
2
- `);return this.buffer.push(...r),this}newline(e=1){for(let t=0;t<e;t++)this.buffer.push(i.LF);return this}divider(e="-",t=32){return this.line(e.repeat(t)),this}beep(e=1,t=100){for(let r=0;r<e;r++)this.buffer.push(...i.BEEP(t));return this}drawerKick(){return this.buffer.push(...i.DRAWER_KICK),this}raw(e){return this.buffer.push(...e),this}image(e){const t=p(e);return this.buffer.push(...t),this}printThaiText(e,t=22){if(typeof document>"u")return console.error("Browser environment required for printThaiText"),this;const r=document.createElement("canvas"),n=r.getContext("2d"),s="'Sarabun', sans-serif",f=t+12,c=e.split(`
3
- `),l=c.length*f+20;r.width=this.printerWidth,r.height=l,n.fillStyle="white",n.fillRect(0,0,r.width,r.height),n.fillStyle="black",n.font=`${t}px ${s}`,n.textBaseline="top";let o=10;for(let d of c)n.fillText(d,10,o),o+=f;const a=n.getImageData(0,0,this.printerWidth,l);return this.image(a)}clear(){return this.buffer=[],this}getBuffer(){return new Uint8Array(this.buffer)}async print(e){if(!e){const t=await x.getPrinters(this.driverApi);if(t.length>0)e=t[0];else throw new Error("No printer found.")}try{const t=this.getBuffer(),r=encodeURIComponent(e);if(!(await fetch(`${this.driverApi}/print?printer=${r}`,{method:"POST",headers:{"Content-Type":"application/octet-stream"},body:t})).ok)throw new Error("Driver Error");return this.clear(),{success:!0}}catch(t){throw console.error("Print Failed:",t),t}}static async getPrinters(e="http://localhost:9123"){try{return await(await fetch(`${e}/printers`)).json()}catch(t){return console.error("Connection Error:",t),[]}}}h.ThermalPrinter=x,Object.defineProperty(h,Symbol.toStringTag,{value:"Module"})}));
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"})}));
4
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","// src/index.js\r\nimport { CMD } from \"./constants\";\r\nimport { convertCanvasToEscPos } from \"./utils\";\r\n\r\n/**\r\n * Thermal Printer Library for ESC/POS printers\r\n *\r\n * IMPORTANT: When using printThaiText(), 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 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","ThermalPrinter","driverApiUrl","alignment","cmd","enable","lines","i","partial","text","bytes","count","char","times","fontSize","canvas","ctx","fontFamily","lineHeight","line","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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naphatjm/cdh-thermal-printer",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Thai Thermal Printer Library via Local Driver",
5
5
  "type": "module",
6
6
  "files": [
@@ -11,9 +11,9 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "exports": {
13
13
  ".": {
14
+ "types": "./dist/index.d.ts",
14
15
  "import": "./dist/thermal-printer.es.js",
15
- "require": "./dist/thermal-printer.umd.js",
16
- "types": "./dist/index.d.ts"
16
+ "require": "./dist/thermal-printer.umd.js"
17
17
  }
18
18
  },
19
19
  "scripts": {