@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.
- package/README.md +212 -229
- package/android/build.gradle +4 -0
- package/android/src/main/java/khare/catprinter/plugin/BarcodeGenerator.java +188 -0
- package/android/src/main/java/khare/catprinter/plugin/CatPrinterCore.java +697 -14
- package/android/src/main/java/khare/catprinter/plugin/CatPrinterPlugin.java +350 -4
- package/android/src/main/java/khare/catprinter/plugin/ImageProcessor.java +358 -24
- package/android/src/main/java/khare/catprinter/plugin/PrinterProtocol.java +92 -0
- package/dist/esm/definitions.d.ts +476 -1
- package/dist/esm/definitions.d.ts.map +1 -1
- package/dist/esm/definitions.js +30 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +13 -2
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +33 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +65 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +65 -0
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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,
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
}
|