@amitkhare/capacitor-cat-printer 0.5.0 → 0.5.2

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.
@@ -3,15 +3,27 @@ package khare.catprinter.plugin;
3
3
  import android.graphics.Bitmap;
4
4
  import android.graphics.Canvas;
5
5
  import android.graphics.Color;
6
+ import android.graphics.ColorMatrix;
7
+ import android.graphics.ColorMatrixColorFilter;
6
8
  import android.graphics.Paint;
7
9
  import android.graphics.Typeface;
8
10
  import android.text.Layout;
11
+ import android.text.SpannableStringBuilder;
12
+ import android.text.Spanned;
9
13
  import android.text.StaticLayout;
10
14
  import android.text.TextPaint;
15
+ import android.text.style.AbsoluteSizeSpan;
16
+ import android.text.style.StrikethroughSpan;
17
+ import android.text.style.StyleSpan;
18
+ import android.text.style.UnderlineSpan;
19
+ import android.util.Base64;
20
+
21
+ import java.io.ByteArrayOutputStream;
22
+ import java.io.File;
11
23
 
12
24
  /**
13
25
  * Image processing utilities for thermal printing.
14
- * Handles resizing, dithering, and text rendering.
26
+ * Handles resizing, dithering, text rendering, and image adjustments.
15
27
  */
16
28
  public class ImageProcessor {
17
29
 
@@ -29,15 +41,72 @@ public class ImageProcessor {
29
41
  return Bitmap.createScaledBitmap(source, targetWidth, targetHeight, true);
30
42
  }
31
43
 
44
+ /**
45
+ * Adjust brightness and contrast of an image
46
+ * @param source Source bitmap
47
+ * @param brightness -100 to 100 (0 = no change)
48
+ * @param contrast -100 to 100 (0 = no change)
49
+ * @return Adjusted bitmap
50
+ */
51
+ public static Bitmap adjustBrightnessContrast(Bitmap source, int brightness, int contrast) {
52
+ if (brightness == 0 && contrast == 0) {
53
+ return source;
54
+ }
55
+
56
+ Bitmap result = Bitmap.createBitmap(source.getWidth(), source.getHeight(), source.getConfig());
57
+ Canvas canvas = new Canvas(result);
58
+ Paint paint = new Paint();
59
+
60
+ // Normalize values
61
+ float brightnessValue = brightness / 100f;
62
+ float contrastValue = (contrast + 100) / 100f;
63
+ contrastValue = contrastValue * contrastValue; // Square for better response
64
+
65
+ // Create color matrix for brightness/contrast
66
+ float[] matrix = new float[] {
67
+ contrastValue, 0, 0, 0, brightnessValue * 255,
68
+ 0, contrastValue, 0, 0, brightnessValue * 255,
69
+ 0, 0, contrastValue, 0, brightnessValue * 255,
70
+ 0, 0, 0, 1, 0
71
+ };
72
+
73
+ ColorMatrix colorMatrix = new ColorMatrix(matrix);
74
+ paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
75
+ canvas.drawBitmap(source, 0, 0, paint);
76
+
77
+ return result;
78
+ }
79
+
80
+ /**
81
+ * Invert colors of an image
82
+ */
83
+ public static Bitmap invertColors(Bitmap source) {
84
+ Bitmap result = Bitmap.createBitmap(source.getWidth(), source.getHeight(), source.getConfig());
85
+ Canvas canvas = new Canvas(result);
86
+ Paint paint = new Paint();
87
+
88
+ // Invert color matrix
89
+ float[] matrix = new float[] {
90
+ -1, 0, 0, 0, 255,
91
+ 0, -1, 0, 0, 255,
92
+ 0, 0, -1, 0, 255,
93
+ 0, 0, 0, 1, 0
94
+ };
95
+
96
+ ColorMatrix colorMatrix = new ColorMatrix(matrix);
97
+ paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
98
+ canvas.drawBitmap(source, 0, 0, paint);
99
+
100
+ return result;
101
+ }
102
+
32
103
  /**
33
104
  * Convert image to 1-bit monochrome bitmap data
34
- * Uses simple threshold dithering (good for receipts)
35
105
  */
36
106
  public static byte[] toMonochrome(Bitmap source, int threshold) {
37
107
  int width = source.getWidth();
38
108
  int height = source.getHeight();
39
109
 
40
- // Ensure width is multiple of 8
41
110
  int bytesPerLine = (width + 7) / 8;
42
111
  byte[] result = new byte[bytesPerLine * height];
43
112
 
@@ -48,13 +117,11 @@ public class ImageProcessor {
48
117
  for (int x = 0; x < width; x++) {
49
118
  int pixel = pixels[y * width + x];
50
119
 
51
- // Convert to grayscale
52
120
  int r = Color.red(pixel);
53
121
  int g = Color.green(pixel);
54
122
  int b = Color.blue(pixel);
55
123
  int gray = (r * 299 + g * 587 + b * 114) / 1000;
56
124
 
57
- // Apply threshold (black = 1, white = 0 for thermal printer)
58
125
  if (gray < threshold) {
59
126
  int byteIndex = y * bytesPerLine + (x / 8);
60
127
  int bitIndex = 7 - (x % 8);
@@ -68,7 +135,6 @@ public class ImageProcessor {
68
135
 
69
136
  /**
70
137
  * Convert image to 1-bit using Floyd-Steinberg dithering
71
- * Better for images with gradients
72
138
  */
73
139
  public static byte[] toMonochromeDithered(Bitmap source, int threshold) {
74
140
  int width = source.getWidth();
@@ -77,7 +143,6 @@ public class ImageProcessor {
77
143
  int bytesPerLine = (width + 7) / 8;
78
144
  byte[] result = new byte[bytesPerLine * height];
79
145
 
80
- // Get pixels and convert to grayscale float array for error diffusion
81
146
  int[] pixels = new int[width * height];
82
147
  source.getPixels(pixels, 0, width, 0, 0, width, height);
83
148
 
@@ -90,7 +155,6 @@ public class ImageProcessor {
90
155
  gray[i] = (r * 299 + g * 587 + b * 114) / 1000f;
91
156
  }
92
157
 
93
- // Floyd-Steinberg dithering
94
158
  for (int y = 0; y < height; y++) {
95
159
  for (int x = 0; x < width; x++) {
96
160
  int idx = y * width + x;
@@ -98,14 +162,12 @@ public class ImageProcessor {
98
162
  float newPixel = oldPixel < threshold ? 0 : 255;
99
163
  float error = oldPixel - newPixel;
100
164
 
101
- // Set bit if black
102
165
  if (newPixel == 0) {
103
166
  int byteIndex = y * bytesPerLine + (x / 8);
104
167
  int bitIndex = 7 - (x % 8);
105
168
  result[byteIndex] |= (1 << bitIndex);
106
169
  }
107
170
 
108
- // Distribute error to neighbors
109
171
  if (x + 1 < width) {
110
172
  gray[idx + 1] += error * 7 / 16f;
111
173
  }
@@ -124,16 +186,46 @@ public class ImageProcessor {
124
186
  return result;
125
187
  }
126
188
 
189
+
127
190
  /**
128
- * Render text to a bitmap
191
+ * Render text to a bitmap with full styling support
129
192
  */
130
193
  public static Bitmap renderText(String text, int paperWidth, int fontSize,
131
- String align, boolean bold, float lineSpacing) {
132
- // Create paint for text
194
+ String align, boolean bold, boolean italic,
195
+ boolean underline, boolean strikethrough,
196
+ float lineSpacing, boolean wordWrap, boolean rtl,
197
+ String fontPath) {
133
198
  TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
134
199
  textPaint.setColor(Color.BLACK);
135
200
  textPaint.setTextSize(fontSize);
136
- textPaint.setTypeface(bold ? Typeface.DEFAULT_BOLD : Typeface.MONOSPACE);
201
+
202
+ // Set typeface with custom font or system font
203
+ Typeface typeface = Typeface.MONOSPACE;
204
+ if (fontPath != null && !fontPath.isEmpty()) {
205
+ try {
206
+ File fontFile = new File(fontPath);
207
+ if (fontFile.exists()) {
208
+ typeface = Typeface.createFromFile(fontFile);
209
+ }
210
+ } catch (Exception e) {
211
+ // Fall back to default
212
+ }
213
+ }
214
+
215
+ // Apply bold/italic style
216
+ int style = Typeface.NORMAL;
217
+ if (bold && italic) {
218
+ style = Typeface.BOLD_ITALIC;
219
+ } else if (bold) {
220
+ style = Typeface.BOLD;
221
+ } else if (italic) {
222
+ style = Typeface.ITALIC;
223
+ }
224
+ textPaint.setTypeface(Typeface.create(typeface, style));
225
+
226
+ // Apply underline and strikethrough
227
+ textPaint.setUnderlineText(underline);
228
+ textPaint.setStrikeThruText(strikethrough);
137
229
 
138
230
  // Determine alignment
139
231
  Layout.Alignment alignment;
@@ -148,9 +240,25 @@ public class ImageProcessor {
148
240
  alignment = Layout.Alignment.ALIGN_NORMAL;
149
241
  }
150
242
 
151
- // Create static layout to measure and render text
243
+ // Handle RTL text
244
+ String processedText = text;
245
+ if (rtl) {
246
+ StringBuilder sb = new StringBuilder();
247
+ String[] lines = text.split("\n");
248
+ for (int i = 0; i < lines.length; i++) {
249
+ if (i > 0) sb.append("\n");
250
+ sb.append(new StringBuilder(lines[i]).reverse().toString());
251
+ }
252
+ processedText = sb.toString();
253
+ if (alignment == Layout.Alignment.ALIGN_NORMAL) {
254
+ alignment = Layout.Alignment.ALIGN_OPPOSITE;
255
+ } else if (alignment == Layout.Alignment.ALIGN_OPPOSITE) {
256
+ alignment = Layout.Alignment.ALIGN_NORMAL;
257
+ }
258
+ }
259
+
152
260
  StaticLayout.Builder builder = StaticLayout.Builder.obtain(
153
- text, 0, text.length(), textPaint, paperWidth
261
+ processedText, 0, processedText.length(), textPaint, paperWidth
154
262
  );
155
263
  builder.setAlignment(alignment);
156
264
  builder.setLineSpacing(0, lineSpacing);
@@ -158,21 +266,218 @@ public class ImageProcessor {
158
266
 
159
267
  StaticLayout layout = builder.build();
160
268
 
161
- // Create bitmap with calculated height
162
- int height = layout.getHeight();
163
- // Ensure height is at least 1 pixel
164
- height = Math.max(height, 1);
269
+ int height = Math.max(layout.getHeight(), 1);
165
270
 
166
271
  Bitmap bitmap = Bitmap.createBitmap(paperWidth, height, Bitmap.Config.ARGB_8888);
167
272
  Canvas canvas = new Canvas(bitmap);
168
273
  canvas.drawColor(Color.WHITE);
274
+ layout.draw(canvas);
275
+
276
+ return bitmap;
277
+ }
278
+
279
+ /**
280
+ * Render text to a bitmap (backward compatible overload)
281
+ */
282
+ public static Bitmap renderText(String text, int paperWidth, int fontSize,
283
+ String align, boolean bold, float lineSpacing,
284
+ boolean wordWrap, boolean rtl) {
285
+ return renderText(text, paperWidth, fontSize, align, bold, false, false, false,
286
+ lineSpacing, wordWrap, rtl, null);
287
+ }
288
+
289
+ /**
290
+ * Render text to a bitmap (simple overload)
291
+ */
292
+ public static Bitmap renderText(String text, int paperWidth, int fontSize,
293
+ String align, boolean bold, float lineSpacing) {
294
+ return renderText(text, paperWidth, fontSize, align, bold, false, false, false,
295
+ lineSpacing, true, false, null);
296
+ }
297
+
298
+ /**
299
+ * Render rich text with mixed formatting (different styles per segment)
300
+ * Uses SpannableStringBuilder to apply different styles to different parts of text
301
+ */
302
+ public static Bitmap renderRichText(String[] texts, boolean[] bolds, boolean[] italics,
303
+ boolean[] underlines, boolean[] strikethroughs,
304
+ int[] fontSizes, int defaultFontSize,
305
+ int paperWidth, String align, float lineSpacing,
306
+ boolean rtl, String fontPath) {
307
+ SpannableStringBuilder builder = new SpannableStringBuilder();
308
+
309
+ for (int i = 0; i < texts.length; i++) {
310
+ String text = texts[i];
311
+ if (text == null || text.isEmpty()) continue;
312
+
313
+ int start = builder.length();
314
+ builder.append(text);
315
+ int end = builder.length();
316
+
317
+ // Apply bold/italic style
318
+ boolean bold = i < bolds.length && bolds[i];
319
+ boolean italic = i < italics.length && italics[i];
320
+ if (bold && italic) {
321
+ builder.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
322
+ } else if (bold) {
323
+ builder.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
324
+ } else if (italic) {
325
+ builder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
326
+ }
327
+
328
+ // Apply underline
329
+ if (i < underlines.length && underlines[i]) {
330
+ builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
331
+ }
332
+
333
+ // Apply strikethrough
334
+ if (i < strikethroughs.length && strikethroughs[i]) {
335
+ builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
336
+ }
337
+
338
+ // Apply font size if different from default
339
+ int fontSize = i < fontSizes.length ? fontSizes[i] : defaultFontSize;
340
+ if (fontSize != defaultFontSize) {
341
+ builder.setSpan(new AbsoluteSizeSpan(fontSize, false), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
342
+ }
343
+ }
344
+
345
+ TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
346
+ textPaint.setColor(Color.BLACK);
347
+ textPaint.setTextSize(defaultFontSize);
348
+
349
+ // Set typeface with custom font or system font
350
+ Typeface typeface = Typeface.MONOSPACE;
351
+ if (fontPath != null && !fontPath.isEmpty()) {
352
+ try {
353
+ File fontFile = new File(fontPath);
354
+ if (fontFile.exists()) {
355
+ typeface = Typeface.createFromFile(fontFile);
356
+ }
357
+ } catch (Exception e) {
358
+ // Fall back to default
359
+ }
360
+ }
361
+ textPaint.setTypeface(typeface);
169
362
 
170
- // Draw text
363
+ // Determine alignment
364
+ Layout.Alignment alignment;
365
+ switch (align) {
366
+ case "center":
367
+ alignment = Layout.Alignment.ALIGN_CENTER;
368
+ break;
369
+ case "right":
370
+ alignment = Layout.Alignment.ALIGN_OPPOSITE;
371
+ break;
372
+ default:
373
+ alignment = Layout.Alignment.ALIGN_NORMAL;
374
+ }
375
+
376
+ // Handle RTL
377
+ CharSequence processedText = builder;
378
+ if (rtl) {
379
+ // For RTL, reverse each line
380
+ String fullText = builder.toString();
381
+ StringBuilder sb = new StringBuilder();
382
+ String[] lines = fullText.split("\n");
383
+ for (int i = 0; i < lines.length; i++) {
384
+ if (i > 0) sb.append("\n");
385
+ sb.append(new StringBuilder(lines[i]).reverse().toString());
386
+ }
387
+ // Note: RTL with spans is complex, this is a simplified approach
388
+ processedText = sb.toString();
389
+ if (alignment == Layout.Alignment.ALIGN_NORMAL) {
390
+ alignment = Layout.Alignment.ALIGN_OPPOSITE;
391
+ } else if (alignment == Layout.Alignment.ALIGN_OPPOSITE) {
392
+ alignment = Layout.Alignment.ALIGN_NORMAL;
393
+ }
394
+ }
395
+
396
+ StaticLayout.Builder layoutBuilder = StaticLayout.Builder.obtain(
397
+ processedText, 0, processedText.length(), textPaint, paperWidth
398
+ );
399
+ layoutBuilder.setAlignment(alignment);
400
+ layoutBuilder.setLineSpacing(0, lineSpacing);
401
+ layoutBuilder.setIncludePad(true);
402
+
403
+ StaticLayout layout = layoutBuilder.build();
404
+
405
+ int height = Math.max(layout.getHeight(), 1);
406
+
407
+ Bitmap bitmap = Bitmap.createBitmap(paperWidth, height, Bitmap.Config.ARGB_8888);
408
+ Canvas canvas = new Canvas(bitmap);
409
+ canvas.drawColor(Color.WHITE);
171
410
  layout.draw(canvas);
172
411
 
173
412
  return bitmap;
174
413
  }
175
414
 
415
+ /**
416
+ * Render a table/receipt format
417
+ */
418
+ public static Bitmap renderTable(String[][] rows, int[] columnWidths, String[] alignments,
419
+ boolean[] boldRows, int paperWidth, int fontSize,
420
+ boolean showLines, String lineChar) {
421
+ TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
422
+ textPaint.setColor(Color.BLACK);
423
+ textPaint.setTextSize(fontSize);
424
+ textPaint.setTypeface(Typeface.MONOSPACE);
425
+
426
+ float lineHeight = fontSize * 1.3f;
427
+ int totalHeight = (int) (rows.length * lineHeight);
428
+ if (showLines) {
429
+ totalHeight += (rows.length - 1) * (int) lineHeight; // Add space for lines
430
+ }
431
+ totalHeight = Math.max(totalHeight, 1);
432
+
433
+ Bitmap bitmap = Bitmap.createBitmap(paperWidth, totalHeight, Bitmap.Config.ARGB_8888);
434
+ Canvas canvas = new Canvas(bitmap);
435
+ canvas.drawColor(Color.WHITE);
436
+
437
+ float y = fontSize;
438
+
439
+ for (int rowIdx = 0; rowIdx < rows.length; rowIdx++) {
440
+ String[] row = rows[rowIdx];
441
+ boolean isBold = boldRows != null && rowIdx < boldRows.length && boldRows[rowIdx];
442
+ textPaint.setTypeface(isBold ? Typeface.DEFAULT_BOLD : Typeface.MONOSPACE);
443
+
444
+ int x = 0;
445
+ for (int colIdx = 0; colIdx < row.length; colIdx++) {
446
+ String cellText = row[colIdx];
447
+ int colWidth = (colIdx < columnWidths.length) ? columnWidths[colIdx] : paperWidth / row.length;
448
+ String colAlign = (colIdx < alignments.length) ? alignments[colIdx] : "left";
449
+
450
+ float textWidth = textPaint.measureText(cellText);
451
+ float textX = x;
452
+
453
+ if ("center".equals(colAlign)) {
454
+ textX = x + (colWidth - textWidth) / 2;
455
+ } else if ("right".equals(colAlign)) {
456
+ textX = x + colWidth - textWidth;
457
+ }
458
+
459
+ canvas.drawText(cellText, textX, y, textPaint);
460
+ x += colWidth;
461
+ }
462
+
463
+ y += lineHeight;
464
+
465
+ // Draw separator line if needed
466
+ if (showLines && rowIdx < rows.length - 1) {
467
+ StringBuilder line = new StringBuilder();
468
+ int charWidth = (int) textPaint.measureText(lineChar);
469
+ int numChars = paperWidth / Math.max(charWidth, 1);
470
+ for (int i = 0; i < numChars; i++) {
471
+ line.append(lineChar);
472
+ }
473
+ canvas.drawText(line.toString(), 0, y, textPaint);
474
+ y += lineHeight;
475
+ }
476
+ }
477
+
478
+ return bitmap;
479
+ }
480
+
176
481
  /**
177
482
  * Flip bitmap horizontally
178
483
  */
@@ -183,12 +488,10 @@ public class ImageProcessor {
183
488
  for (int y = 0; y < height; y++) {
184
489
  int lineStart = y * bytesPerLine;
185
490
 
186
- // Copy line to temp with reversed bytes and bits
187
491
  for (int x = 0; x < bytesPerLine; x++) {
188
492
  temp[bytesPerLine - 1 - x] = PrinterProtocol.reverseBits(data[lineStart + x]);
189
493
  }
190
494
 
191
- // Copy back
192
495
  System.arraycopy(temp, 0, data, lineStart, bytesPerLine);
193
496
  }
194
497
  }
@@ -204,10 +507,41 @@ public class ImageProcessor {
204
507
  int topLine = y * bytesPerLine;
205
508
  int bottomLine = (height - 1 - y) * bytesPerLine;
206
509
 
207
- // Swap lines
208
510
  System.arraycopy(data, topLine, temp, 0, bytesPerLine);
209
511
  System.arraycopy(data, bottomLine, data, topLine, bytesPerLine);
210
512
  System.arraycopy(temp, 0, data, bottomLine, bytesPerLine);
211
513
  }
212
514
  }
515
+
516
+ /**
517
+ * Convert bitmap to base64 PNG for preview
518
+ */
519
+ public static String bitmapToBase64(Bitmap bitmap) {
520
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
521
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
522
+ byte[] bytes = baos.toByteArray();
523
+ return Base64.encodeToString(bytes, Base64.NO_WRAP);
524
+ }
525
+
526
+ /**
527
+ * Convert monochrome byte array back to bitmap for preview
528
+ */
529
+ public static Bitmap monochromeToPreviewBitmap(byte[] data, int width, int height) {
530
+ int bytesPerLine = width / 8;
531
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
532
+
533
+ int[] pixels = new int[width * height];
534
+
535
+ for (int y = 0; y < height; y++) {
536
+ for (int x = 0; x < width; x++) {
537
+ int byteIndex = y * bytesPerLine + (x / 8);
538
+ int bitIndex = 7 - (x % 8);
539
+ boolean isBlack = (data[byteIndex] & (1 << bitIndex)) != 0;
540
+ pixels[y * width + x] = isBlack ? Color.BLACK : Color.WHITE;
541
+ }
542
+ }
543
+
544
+ bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
545
+ return bitmap;
546
+ }
213
547
  }
@@ -208,6 +208,14 @@ public class PrinterProtocol {
208
208
  return makeCommand(0xa0, intToBytes(pixels, 2));
209
209
  }
210
210
 
211
+ /**
212
+ * Get device info command (0xa8)
213
+ * Queries printer for model/firmware information
214
+ */
215
+ public static byte[] cmdGetDeviceInfo() {
216
+ return makeCommand(0xa8, new byte[]{0x00});
217
+ }
218
+
211
219
  /**
212
220
  * Create bitmap line command with bit reversal
213
221
  */
@@ -219,6 +227,17 @@ public class PrinterProtocol {
219
227
  return makeCommand(0xa2, reversed);
220
228
  }
221
229
 
230
+ /**
231
+ * Create compressed bitmap line command for newer printers (GB03+)
232
+ * Currently uses same format as regular bitmap - compression not yet implemented
233
+ * TODO: Implement actual compression algorithm from reference
234
+ */
235
+ public static byte[] cmdDrawCompressedBitmap(byte[] lineData) {
236
+ // For now, fall back to regular bitmap command
237
+ // The reference implementation also does this: draw_compressed_bitmap just calls draw_bitmap
238
+ return cmdDrawBitmap(lineData);
239
+ }
240
+
222
241
  /**
223
242
  * Check if data matches flow pause signal
224
243
  */
@@ -240,4 +259,77 @@ public class PrinterProtocol {
240
259
  }
241
260
  return true;
242
261
  }
262
+
263
+ // ==================== MODEL DETECTION ====================
264
+
265
+ /**
266
+ * Known printer models from reference implementation
267
+ */
268
+ public static final String[] KNOWN_MODELS = {
269
+ "_ZZ00", "GB01", "GB02", "GB03", "GT01",
270
+ "MX05", "MX06", "MX08", "MX09", "MX10", "MX11", "YT01"
271
+ };
272
+
273
+ /**
274
+ * Models that support compressed data (new kind)
275
+ */
276
+ public static final String[] NEW_KIND_MODELS = {"GB03"};
277
+
278
+ /**
279
+ * Models with feeding problems that need workarounds
280
+ */
281
+ public static final String[] PROBLEM_FEEDING_MODELS = {"MX05", "MX06", "MX08", "MX09", "MX10"};
282
+
283
+ /**
284
+ * Detect printer model from device name
285
+ * @param deviceName Bluetooth device name
286
+ * @return Model string (e.g., "GB01", "GT01") or "_ZZ00" if unknown
287
+ */
288
+ public static String detectModel(String deviceName) {
289
+ if (deviceName == null || deviceName.isEmpty()) {
290
+ return "_ZZ00";
291
+ }
292
+
293
+ String upperName = deviceName.toUpperCase();
294
+
295
+ // Check for known model prefixes
296
+ for (String model : KNOWN_MODELS) {
297
+ if (upperName.contains(model)) {
298
+ return model;
299
+ }
300
+ }
301
+
302
+ // Try to extract model pattern (2 letters + 2 digits)
303
+ for (int i = 0; i <= upperName.length() - 4; i++) {
304
+ String sub = upperName.substring(i, i + 4);
305
+ if (Character.isLetter(sub.charAt(0)) &&
306
+ Character.isLetter(sub.charAt(1)) &&
307
+ Character.isDigit(sub.charAt(2)) &&
308
+ Character.isDigit(sub.charAt(3))) {
309
+ return sub;
310
+ }
311
+ }
312
+
313
+ return "_ZZ00";
314
+ }
315
+
316
+ /**
317
+ * Check if model is "new kind" (supports compressed data)
318
+ */
319
+ public static boolean isNewKind(String model) {
320
+ for (String m : NEW_KIND_MODELS) {
321
+ if (m.equals(model)) return true;
322
+ }
323
+ return false;
324
+ }
325
+
326
+ /**
327
+ * Check if model has feeding problems
328
+ */
329
+ public static boolean hasFeedingProblems(String model) {
330
+ for (String m : PROBLEM_FEEDING_MODELS) {
331
+ if (m.equals(model)) return true;
332
+ }
333
+ return false;
334
+ }
243
335
  }