taro-bluetooth-print 2.5.0 → 2.6.0

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.
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Barcode Generator
3
3
  *
4
- * Generates ESC/POS commands for printing 1D barcodes.
5
- * Supports Code128, Code39, EAN-13, EAN-8, UPC-A, ITF, and CODABAR formats.
4
+ * Generates ESC/POS commands for printing 1D barcodes and 2D codes.
5
+ * Supports Code128, Code39, EAN-13, EAN-8, UPC-A, ITF, CODABAR, QR_CODE, and PDF417 formats.
6
6
  *
7
7
  * @example
8
8
  * ```typescript
@@ -33,6 +33,10 @@ export enum BarcodeFormat {
33
33
  ITF = 'ITF',
34
34
  /** CODABAR - Numeric with special start/stop chars */
35
35
  CODABAR = 'CODABAR',
36
+ /** QR Code - 2D matrix code */
37
+ QR_CODE = 'QR_CODE',
38
+ /** PDF417 - 2D stacked barcode */
39
+ PDF417 = 'PDF417',
36
40
  }
37
41
 
38
42
  /**
@@ -49,6 +53,16 @@ export interface BarcodeOptions {
49
53
  showText?: boolean;
50
54
  /** Text position */
51
55
  textPosition?: 'above' | 'below' | 'both' | 'none';
56
+ /** QR code error correction level (L/M/Q/H, default: M) */
57
+ errorCorrection?: 'L' | 'M' | 'Q' | 'H';
58
+ /** QR code model (1 or 2, default: 2) */
59
+ qrModel?: 1 | 2;
60
+ /** PDF417 compression mode (0-3, default: 2) */
61
+ pdf417Compression?: 0 | 1 | 2 | 3;
62
+ /** PDF417 security level (0-8, default: 2) */
63
+ pdf417Security?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
64
+ /** PDF417 columns (1-30, default: 2) */
65
+ pdf417Columns?: number;
52
66
  }
53
67
 
54
68
  /**
@@ -81,9 +95,10 @@ export interface IBarcodeGenerator {
81
95
  * ESC/POS command constants
82
96
  */
83
97
  const GS = 0x1d;
98
+ const ESC = 0x1b;
84
99
 
85
100
  /**
86
- * ESC/POS barcode type codes
101
+ * ESC/POS barcode type codes (GS k m)
87
102
  */
88
103
  const BARCODE_TYPES: Record<BarcodeFormat, number> = {
89
104
  [BarcodeFormat.UPCA]: 65, // GS k 65
@@ -93,6 +108,26 @@ const BARCODE_TYPES: Record<BarcodeFormat, number> = {
93
108
  [BarcodeFormat.ITF]: 70, // GS k 70
94
109
  [BarcodeFormat.CODABAR]: 71, // GS k 71
95
110
  [BarcodeFormat.CODE128]: 73, // GS k 73
111
+ [BarcodeFormat.QR_CODE]: 80, // GS k 80 (PDF moved to 81)
112
+ [BarcodeFormat.PDF417]: 81, // GS k 81
113
+ };
114
+
115
+ /**
116
+ * QR code error correction levels
117
+ */
118
+ const QR_ERROR_CORRECTION_CODES: Record<string, number> = {
119
+ L: 48, // 0x30
120
+ M: 49, // 0x31
121
+ Q: 50, // 0x32
122
+ H: 51, // 0x33
123
+ };
124
+
125
+ /**
126
+ * QR code models
127
+ */
128
+ const QR_MODEL_CODES: Record<number, number> = {
129
+ 1: 49, // 0x31
130
+ 2: 50, // 0x32
96
131
  };
97
132
 
98
133
  /**
@@ -105,9 +140,19 @@ const TEXT_POSITION_CODES: Record<string, number> = {
105
140
  both: 3,
106
141
  };
107
142
 
143
+ /**
144
+ * PDF417 compression modes
145
+ */
146
+ const PDF417_COMPRESSION_CODES: Record<number, number> = {
147
+ 0: 48, // 0x30 - linear
148
+ 1: 49, // 0x31 - compressed
149
+ 2: 50, // 0x32 - enhanced
150
+ 3: 51, // 0x33 - auto
151
+ };
152
+
108
153
  /**
109
154
  * Barcode Generator class
110
- * Generates ESC/POS commands for 1D barcodes
155
+ * Generates ESC/POS commands for 1D barcodes and 2D codes
111
156
  */
112
157
  export class BarcodeGenerator implements IBarcodeGenerator {
113
158
  /**
@@ -124,6 +169,23 @@ export class BarcodeGenerator implements IBarcodeGenerator {
124
169
  return [];
125
170
  }
126
171
 
172
+ // Handle 2D codes separately
173
+ if (options.format === BarcodeFormat.QR_CODE) {
174
+ return this.generateQRCode(content, options);
175
+ }
176
+
177
+ if (options.format === BarcodeFormat.PDF417) {
178
+ return this.generatePDF417(content, options);
179
+ }
180
+
181
+ // 1D barcode generation
182
+ return this.generate1DBarcode(content, options);
183
+ }
184
+
185
+ /**
186
+ * Generate 1D barcode commands
187
+ */
188
+ private generate1DBarcode(content: string, options: BarcodeOptions): Uint8Array[] {
127
189
  const commands: Uint8Array[] = [];
128
190
 
129
191
  // 1. Set barcode height: GS h n (n = 1-255)
@@ -148,6 +210,106 @@ export class BarcodeGenerator implements IBarcodeGenerator {
148
210
  return commands;
149
211
  }
150
212
 
213
+ /**
214
+ * Generate QR code commands using ESC/POS GS k 80-83
215
+ *
216
+ * @param content - QR code content
217
+ * @param options - QR code options
218
+ * @returns Array of ESC/POS command buffers
219
+ */
220
+ private generateQRCode(content: string, options: BarcodeOptions): Uint8Array[] {
221
+ const commands: Uint8Array[] = [];
222
+ const model = options.qrModel ?? 2;
223
+ const size = this.clampQRSize(options.height ?? 6);
224
+ const errorLevel = options.errorCorrection ?? 'M';
225
+
226
+ // 1. Set QR code model: GS ( k 04 00 31 41 50 n
227
+ // pH=0, pL=4, cn=31 (1), fn=65 (A), 50=0, n=model(1 or 2)
228
+ const qrModelCode = QR_MODEL_CODES[model] ?? 50;
229
+ commands.push(new Uint8Array([GS, 0x28, 0x6b, 0x04, 0x00, 0x31, 0x41, 0x50, qrModelCode]));
230
+
231
+ // 2. Set QR code size (module size): GS ( k 03 00 31 43 n
232
+ commands.push(new Uint8Array([GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x43, size]));
233
+
234
+ // 3. Set error correction level: GS ( k 03 00 31 45 n
235
+ const qrErrorCode = QR_ERROR_CORRECTION_CODES[errorLevel] ?? 49;
236
+ commands.push(new Uint8Array([GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x45, qrErrorCode]));
237
+
238
+ // 4. Store data: GS ( k pL pH 31 50 30 d1...dk
239
+ const dataBytes = this.encodeUTF8(content);
240
+ const storeLen = dataBytes.length + 3;
241
+ commands.push(
242
+ new Uint8Array([
243
+ GS,
244
+ 0x28,
245
+ 0x6b,
246
+ storeLen & 0xff,
247
+ (storeLen >> 8) & 0xff,
248
+ 0x31,
249
+ 0x50,
250
+ 0x30,
251
+ ...dataBytes,
252
+ ])
253
+ );
254
+
255
+ // 5. Print QR code: GS ( k 03 00 31 51 30
256
+ commands.push(new Uint8Array([GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x51, 0x30]));
257
+
258
+ return commands;
259
+ }
260
+
261
+ /**
262
+ * Generate PDF417 commands using ESC/POS GS k 81
263
+ *
264
+ * PDF417 is a 2D stacked barcode format supported by many thermal printers.
265
+ * The printer will handle the actual encoding and generation of the PDF417 symbol.
266
+ *
267
+ * @param content - PDF417 content (text data)
268
+ * @param options - PDF417 options
269
+ * @returns Array of ESC/POS command buffers
270
+ */
271
+ private generatePDF417(content: string, options: BarcodeOptions): Uint8Array[] {
272
+ const commands: Uint8Array[] = [];
273
+ const compression = options.pdf417Compression ?? 2;
274
+ const security = options.pdf417Security ?? 2;
275
+ const columns = this.clampPDF417Columns(options.pdf417Columns ?? 2);
276
+
277
+ // 1. Set PDF417 parameters: GS ( k 07 00 31 30 n1 n2 n3 n4
278
+ // n1 = columns (1-30), n2 = security level (0-8), n3 = truncation (0=none), n4 = reserved
279
+ commands.push(
280
+ new Uint8Array([
281
+ GS,
282
+ 0x28,
283
+ 0x6b,
284
+ 0x07,
285
+ 0x00,
286
+ 0x31,
287
+ 0x30, // Function 30 (PDF417 set)
288
+ columns,
289
+ security,
290
+ 0, // truncation off
291
+ 0, // reserved
292
+ ])
293
+ );
294
+
295
+ // 2. Set data compression mode: ESC ] 04 n
296
+ const pdfCompressionCode = PDF417_COMPRESSION_CODES[compression] ?? 50;
297
+ commands.push(new Uint8Array([ESC, 0x5d, 0x04, pdfCompressionCode]));
298
+
299
+ // 3. Store PDF417 data: GS k 81 pL pH d1...dk
300
+ const dataBytes = this.encodeUTF8(content);
301
+ const storeLen = dataBytes.length + 3;
302
+ commands.push(
303
+ new Uint8Array([GS, 0x6b, 81, storeLen & 0xff, (storeLen >> 8) & 0xff, ...dataBytes])
304
+ );
305
+
306
+ // 4. Print PDF417: GS k 81 00 (alternative form if needed)
307
+ // Actually for PDF417 print: GS ( k 03 00 31 51 31
308
+ commands.push(new Uint8Array([GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x51, 0x31]));
309
+
310
+ return commands;
311
+ }
312
+
151
313
  /**
152
314
  * Validate barcode content for the specified format
153
315
  *
@@ -158,7 +320,8 @@ export class BarcodeGenerator implements IBarcodeGenerator {
158
320
  validate(content: string, format: BarcodeFormat): ValidationResult {
159
321
  const errors: ValidationResult['errors'] = [];
160
322
 
161
- if (!content || typeof content !== 'string') {
323
+ // Common validation for all formats
324
+ if (content === null || content === undefined) {
162
325
  errors.push({
163
326
  field: 'content',
164
327
  message: 'Barcode content is required',
@@ -167,6 +330,41 @@ export class BarcodeGenerator implements IBarcodeGenerator {
167
330
  return { valid: false, errors };
168
331
  }
169
332
 
333
+ if (typeof content !== 'string') {
334
+ errors.push({
335
+ field: 'content',
336
+ message: 'Barcode content must be a string',
337
+ code: 'INVALID_TYPE',
338
+ });
339
+ return { valid: false, errors };
340
+ }
341
+
342
+ if (content.length === 0) {
343
+ errors.push({
344
+ field: 'content',
345
+ message: 'Barcode content cannot be empty',
346
+ code: 'REQUIRED',
347
+ });
348
+ return { valid: false, errors };
349
+ }
350
+
351
+ // Validate minimum content length for 2D codes
352
+ if (format === BarcodeFormat.QR_CODE && content.length > 7089) {
353
+ errors.push({
354
+ field: 'content',
355
+ message: 'QR code content exceeds maximum length (7089 bytes for binary mode)',
356
+ code: 'CONTENT_TOO_LONG',
357
+ });
358
+ }
359
+
360
+ if (format === BarcodeFormat.PDF417 && content.length > 1850) {
361
+ errors.push({
362
+ field: 'content',
363
+ message: 'PDF417 content exceeds maximum length (~1850 bytes)',
364
+ code: 'CONTENT_TOO_LONG',
365
+ });
366
+ }
367
+
170
368
  switch (format) {
171
369
  case BarcodeFormat.EAN13:
172
370
  this.validateEAN13(content, errors);
@@ -189,8 +387,13 @@ export class BarcodeGenerator implements IBarcodeGenerator {
189
387
  case BarcodeFormat.CODABAR:
190
388
  this.validateCodabar(content, errors);
191
389
  break;
390
+ case BarcodeFormat.QR_CODE:
391
+ this.validateQRCode(content, errors);
392
+ break;
393
+ case BarcodeFormat.PDF417:
394
+ this.validatePDF417(content, errors);
395
+ break;
192
396
  default: {
193
- // This should never happen if all enum cases are handled
194
397
  errors.push({
195
398
  field: 'format',
196
399
  message: `Unsupported barcode format: ${String(format)}`,
@@ -202,6 +405,69 @@ export class BarcodeGenerator implements IBarcodeGenerator {
202
405
  return { valid: errors.length === 0, errors };
203
406
  }
204
407
 
408
+ /**
409
+ * Validate QR code content
410
+ */
411
+ private validateQRCode(content: string, errors: ValidationResult['errors']): void {
412
+ if (content.length === 0) {
413
+ errors.push({
414
+ field: 'content',
415
+ message: 'QR code content cannot be empty',
416
+ code: 'REQUIRED',
417
+ });
418
+ }
419
+
420
+ // Check for invalid characters in numeric/alphanumeric mode (if we were using those)
421
+ // For byte mode, all characters 0-255 are valid
422
+ if (content.length > 7089) {
423
+ errors.push({
424
+ field: 'content',
425
+ message: 'QR code content exceeds maximum binary mode length',
426
+ code: 'CONTENT_TOO_LONG',
427
+ });
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Validate PDF417 content
433
+ */
434
+ private validatePDF417(content: string, errors: ValidationResult['errors']): void {
435
+ if (content.length === 0) {
436
+ errors.push({
437
+ field: 'content',
438
+ message: 'PDF417 content cannot be empty',
439
+ code: 'REQUIRED',
440
+ });
441
+ }
442
+
443
+ if (content.length > 1850) {
444
+ errors.push({
445
+ field: 'content',
446
+ message: 'PDF417 content exceeds maximum length',
447
+ code: 'CONTENT_TOO_LONG',
448
+ });
449
+ }
450
+
451
+ // PDF417 supports ASCII 0-127 in text mode
452
+ let hasHighChars = false;
453
+ for (let i = 0; i < content.length; i++) {
454
+ const code = content.charCodeAt(i);
455
+ if (code > 127) {
456
+ hasHighChars = true;
457
+ break;
458
+ }
459
+ }
460
+
461
+ if (hasHighChars) {
462
+ errors.push({
463
+ field: 'content',
464
+ message:
465
+ 'PDF417 standard mode only supports ASCII characters. Consider using compression mode.',
466
+ code: 'INVALID_CHARACTERS',
467
+ });
468
+ }
469
+ }
470
+
205
471
  /**
206
472
  * Get list of supported barcode formats
207
473
  *
@@ -450,6 +716,32 @@ export class BarcodeGenerator implements IBarcodeGenerator {
450
716
  return content.split('').map(c => c.charCodeAt(0));
451
717
  }
452
718
 
719
+ /**
720
+ * Encode string to UTF-8 bytes
721
+ */
722
+ private encodeUTF8(str: string): number[] {
723
+ const result: number[] = [];
724
+ for (let i = 0; i < str.length; i++) {
725
+ const charCode = str.charCodeAt(i);
726
+ if (charCode < 0x80) {
727
+ result.push(charCode);
728
+ } else if (charCode < 0x800) {
729
+ result.push(0xc0 | (charCode >> 6));
730
+ result.push(0x80 | (charCode & 0x3f));
731
+ } else if (charCode < 0x10000) {
732
+ result.push(0xe0 | (charCode >> 12));
733
+ result.push(0x80 | ((charCode >> 6) & 0x3f));
734
+ result.push(0x80 | (charCode & 0x3f));
735
+ } else {
736
+ result.push(0xf0 | (charCode >> 18));
737
+ result.push(0x80 | ((charCode >> 12) & 0x3f));
738
+ result.push(0x80 | ((charCode >> 6) & 0x3f));
739
+ result.push(0x80 | (charCode & 0x3f));
740
+ }
741
+ }
742
+ return result;
743
+ }
744
+
453
745
  /**
454
746
  * Get text position code
455
747
  */
@@ -475,6 +767,20 @@ export class BarcodeGenerator implements IBarcodeGenerator {
475
767
  private clampWidth(width: number): number {
476
768
  return Math.max(2, Math.min(6, Math.floor(width)));
477
769
  }
770
+
771
+ /**
772
+ * Clamp QR code size to valid range (1-16)
773
+ */
774
+ private clampQRSize(size: number): number {
775
+ return Math.max(1, Math.min(16, Math.floor(size)));
776
+ }
777
+
778
+ /**
779
+ * Clamp PDF417 columns to valid range (1-30)
780
+ */
781
+ private clampPDF417Columns(columns: number): number {
782
+ return Math.max(1, Math.min(30, Math.floor(columns)));
783
+ }
478
784
  }
479
785
 
480
786
  // Export singleton instance for convenience
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Barcode Generator Module
3
- * 条码生成模块 - 负责一维条码生成
3
+ * 条码生成模块 - 负责一维条码和二维条码生成
4
4
  */
5
5
 
6
6
  export { BarcodeGenerator, barcodeGenerator, BarcodeFormat } from './BarcodeGenerator';
@@ -275,14 +275,14 @@ export class StarPrinter implements IPrinterDriver {
275
275
  return [];
276
276
  }
277
277
 
278
- const model = (options?.model ?? 2) as 1 | 2;
278
+ const model = options?.model ?? 2;
279
279
  // STAR uses cellSize instead of 'size' - extract safely
280
280
  let cellSize = 4;
281
281
  if (options) {
282
282
  if ('cellSize' in options) {
283
- cellSize = (options as StarQrOptions).cellSize ?? 4;
283
+ cellSize = options.cellSize ?? 4;
284
284
  } else if ('size' in options) {
285
- cellSize = (options as IQrOptions).size ?? 4;
285
+ cellSize = options.size ?? 4;
286
286
  }
287
287
  }
288
288
  const ecLevel = options?.errorCorrection ?? 'M';
@@ -310,9 +310,10 @@ export class StarPrinter implements IPrinterDriver {
310
310
 
311
311
  // Step 4: QR Data - Calculate length and send
312
312
  // Get encoded data
313
- const encoded = this.useEncodingService && this.encodingService.isSupported('UTF-8')
314
- ? this.encodingService.encode(content, 'UTF-8')
315
- : new TextEncoder().encode(content);
313
+ const encoded =
314
+ this.useEncodingService && this.encodingService.isSupported('UTF-8')
315
+ ? this.encodingService.encode(content, 'UTF-8')
316
+ : new TextEncoder().encode(content);
316
317
 
317
318
  // Send QR data: Length prefix (2 bytes big-endian) + data
318
319
  const len = encoded.length;
@@ -360,10 +361,10 @@ export class StarPrinter implements IPrinterDriver {
360
361
 
361
362
  // HRI position mapping
362
363
  const hriMap: Record<string, number> = {
363
- 'none': 0,
364
- 'above': 1,
365
- 'below': 2,
366
- 'both': 3,
364
+ none: 0,
365
+ above: 1,
366
+ below: 2,
367
+ both: 3,
367
368
  };
368
369
  const hriValue = hriMap[hri] ?? 2;
369
370
 
@@ -544,9 +545,9 @@ export class StarPrinter implements IPrinterDriver {
544
545
  // ESC a n - Select justification
545
546
  // n=0: Left, n=1: Center, n=2: Right
546
547
  const alignMap: Record<Alignment, number> = {
547
- 'left': 0x00,
548
- 'center': 0x01,
549
- 'right': 0x02,
548
+ left: 0x00,
549
+ center: 0x01,
550
+ right: 0x02,
550
551
  };
551
552
 
552
553
  const alignValue = alignMap[align] ?? 0x00;
@@ -279,10 +279,19 @@ export class EncodingService {
279
279
 
280
280
  const normalized = this.normalizeEncoding(encoding);
281
281
  return [
282
- 'GBK', 'GB2312', 'BIG5', 'UTF8', 'UTF-8',
283
- 'EUCKR', 'EUC-KR',
284
- 'SHIFTJIS', 'SHIFT-JIS', 'SJIS',
285
- 'ISO2022JP', 'ISO-2022-JP', 'JIS',
282
+ 'GBK',
283
+ 'GB2312',
284
+ 'BIG5',
285
+ 'UTF8',
286
+ 'UTF-8',
287
+ 'EUCKR',
288
+ 'EUC-KR',
289
+ 'SHIFTJIS',
290
+ 'SHIFT-JIS',
291
+ 'SJIS',
292
+ 'ISO2022JP',
293
+ 'ISO-2022-JP',
294
+ 'JIS',
286
295
  ].includes(normalized);
287
296
  }
288
297
 
@@ -487,8 +496,6 @@ export class EncodingService {
487
496
  // Try native TextEncoder first (full coverage)
488
497
  try {
489
498
  const native = new TextEncoder();
490
- // @ts-ignore - TextEncoder supports EUC-KR in some environments
491
- const encoded = native.encodeInto ? null : null;
492
499
  // Use native EUC-KR encoding if available
493
500
  const test = native.encode('가');
494
501
  if (test.length > 0) {