@amitkhare/capacitor-cat-printer 0.5.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.
@@ -0,0 +1,348 @@
1
+ package khare.catprinter.plugin;
2
+
3
+ import android.Manifest;
4
+ import android.annotation.SuppressLint;
5
+ import android.os.Build;
6
+ import android.util.Log;
7
+
8
+ import com.getcapacitor.JSArray;
9
+ import com.getcapacitor.JSObject;
10
+ import com.getcapacitor.PermissionState;
11
+ import com.getcapacitor.Plugin;
12
+ import com.getcapacitor.PluginCall;
13
+ import com.getcapacitor.PluginMethod;
14
+ import com.getcapacitor.annotation.CapacitorPlugin;
15
+ import com.getcapacitor.annotation.Permission;
16
+ import com.getcapacitor.annotation.PermissionCallback;
17
+
18
+ import java.util.List;
19
+
20
+ /**
21
+ * Capacitor plugin for Cat thermal printers.
22
+ * Bridges JavaScript API to native Android BLE functionality.
23
+ */
24
+ @CapacitorPlugin(
25
+ name = "CatPrinter",
26
+ permissions = {
27
+ @Permission(
28
+ alias = "bluetooth",
29
+ strings = {
30
+ Manifest.permission.BLUETOOTH,
31
+ Manifest.permission.BLUETOOTH_ADMIN
32
+ }
33
+ ),
34
+ @Permission(
35
+ alias = "bluetoothScan",
36
+ strings = {
37
+ Manifest.permission.BLUETOOTH_SCAN
38
+ }
39
+ ),
40
+ @Permission(
41
+ alias = "bluetoothConnect",
42
+ strings = {
43
+ Manifest.permission.BLUETOOTH_CONNECT
44
+ }
45
+ ),
46
+ @Permission(
47
+ alias = "location",
48
+ strings = {
49
+ Manifest.permission.ACCESS_FINE_LOCATION,
50
+ Manifest.permission.ACCESS_COARSE_LOCATION
51
+ }
52
+ )
53
+ }
54
+ )
55
+ public class CatPrinterPlugin extends Plugin {
56
+ private static final String TAG = "CatPrinterPlugin";
57
+
58
+ private CatPrinterCore printerCore;
59
+
60
+ @Override
61
+ public void load() {
62
+ printerCore = new CatPrinterCore(getContext());
63
+ }
64
+
65
+ // ==================== PERMISSION HANDLING ====================
66
+
67
+ private boolean hasBluetoothPermissions() {
68
+ // Location is required for BLE scanning on all Android versions
69
+ boolean hasLocation = getPermissionState("location") == PermissionState.GRANTED;
70
+ Log.d(TAG, "Location permission: " + hasLocation);
71
+
72
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
73
+ // Android 12+ also needs new Bluetooth permissions
74
+ boolean hasScan = getPermissionState("bluetoothScan") == PermissionState.GRANTED;
75
+ boolean hasConnect = getPermissionState("bluetoothConnect") == PermissionState.GRANTED;
76
+ Log.d(TAG, "Android 12+ permissions - Scan: " + hasScan + ", Connect: " + hasConnect + ", Location: " + hasLocation);
77
+ return hasScan && hasConnect && hasLocation;
78
+ } else {
79
+ // Android 11 and below
80
+ boolean hasBluetooth = getPermissionState("bluetooth") == PermissionState.GRANTED;
81
+ Log.d(TAG, "Android <12 permissions - Location: " + hasLocation + ", Bluetooth: " + hasBluetooth);
82
+ return hasLocation && hasBluetooth;
83
+ }
84
+ }
85
+
86
+ private void requestBluetoothPermissions(PluginCall call, String callbackName) {
87
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
88
+ // Android 12+ needs Bluetooth + Location permissions
89
+ Log.d(TAG, "Requesting Android 12+ Bluetooth + Location permissions");
90
+ requestPermissionForAliases(new String[]{"bluetoothScan", "bluetoothConnect", "location"}, call, callbackName);
91
+ } else {
92
+ Log.d(TAG, "Requesting Android <12 Bluetooth + Location permissions");
93
+ requestPermissionForAliases(new String[]{"bluetooth", "location"}, call, callbackName);
94
+ }
95
+ }
96
+
97
+ // ==================== SCANNING ====================
98
+
99
+ @PluginMethod
100
+ public void scan(PluginCall call) {
101
+ Log.d(TAG, "scan() called, checking permissions...");
102
+ if (!hasBluetoothPermissions()) {
103
+ Log.d(TAG, "Permissions not granted, requesting...");
104
+ requestBluetoothPermissions(call, "scanPermissionCallback");
105
+ return;
106
+ }
107
+
108
+ Log.d(TAG, "Permissions granted, starting scan...");
109
+ performScan(call);
110
+ }
111
+
112
+ @PermissionCallback
113
+ private void scanPermissionCallback(PluginCall call) {
114
+ Log.d(TAG, "Permission callback received");
115
+ if (hasBluetoothPermissions()) {
116
+ Log.d(TAG, "Permissions now granted, starting scan...");
117
+ performScan(call);
118
+ } else {
119
+ Log.e(TAG, "Permissions still not granted");
120
+ call.reject("Bluetooth permissions required");
121
+ }
122
+ }
123
+
124
+ private void performScan(PluginCall call) {
125
+ int duration = call.getInt("duration", 4000);
126
+
127
+ printerCore.startScan(duration, new CatPrinterCore.ScanResultCallback() {
128
+ @Override
129
+ public void onDeviceFound(String name, String address, int rssi) {
130
+ JSObject device = new JSObject();
131
+ device.put("name", name);
132
+ device.put("address", address);
133
+ device.put("rssi", rssi);
134
+ notifyListeners("scanResult", device);
135
+ }
136
+
137
+ @Override
138
+ public void onScanComplete(List<CatPrinterCore.BleDeviceInfo> devices) {
139
+ JSObject result = new JSObject();
140
+ JSArray devicesArray = new JSArray();
141
+
142
+ for (CatPrinterCore.BleDeviceInfo device : devices) {
143
+ JSObject deviceObj = new JSObject();
144
+ deviceObj.put("name", device.name);
145
+ deviceObj.put("address", device.address);
146
+ deviceObj.put("rssi", device.rssi);
147
+ devicesArray.put(deviceObj);
148
+ }
149
+
150
+ result.put("devices", devicesArray);
151
+ call.resolve(result);
152
+ }
153
+
154
+ @Override
155
+ public void onScanError(String error) {
156
+ call.reject(error);
157
+ }
158
+ });
159
+ }
160
+
161
+ @PluginMethod
162
+ public void stopScan(PluginCall call) {
163
+ printerCore.stopScan();
164
+ call.resolve();
165
+ }
166
+
167
+ // ==================== CONNECTION ====================
168
+
169
+ @PluginMethod
170
+ public void connect(PluginCall call) {
171
+ if (!hasBluetoothPermissions()) {
172
+ requestBluetoothPermissions(call, "connectPermissionCallback");
173
+ return;
174
+ }
175
+
176
+ performConnect(call);
177
+ }
178
+
179
+ @PermissionCallback
180
+ private void connectPermissionCallback(PluginCall call) {
181
+ if (hasBluetoothPermissions()) {
182
+ performConnect(call);
183
+ } else {
184
+ call.reject("Bluetooth permissions required");
185
+ }
186
+ }
187
+
188
+ private void performConnect(PluginCall call) {
189
+ String address = call.getString("address");
190
+ if (address == null || address.isEmpty()) {
191
+ call.reject("Address is required");
192
+ return;
193
+ }
194
+
195
+ int paperWidth = call.getInt("paperWidth", PrinterProtocol.WIDTH_58MM);
196
+
197
+ printerCore.connect(address, paperWidth, new CatPrinterCore.ConnectionCallback() {
198
+ @Override
199
+ public void onConnected() {
200
+ JSObject state = new JSObject();
201
+ state.put("connected", true);
202
+ state.put("address", address);
203
+ notifyListeners("connectionState", state);
204
+ call.resolve();
205
+ }
206
+
207
+ @Override
208
+ public void onDisconnected() {
209
+ JSObject state = new JSObject();
210
+ state.put("connected", false);
211
+ notifyListeners("connectionState", state);
212
+ }
213
+
214
+ @Override
215
+ public void onError(String error) {
216
+ call.reject(error);
217
+ }
218
+ });
219
+ }
220
+
221
+ @PluginMethod
222
+ public void disconnect(PluginCall call) {
223
+ printerCore.disconnect();
224
+
225
+ JSObject state = new JSObject();
226
+ state.put("connected", false);
227
+ notifyListeners("connectionState", state);
228
+
229
+ call.resolve();
230
+ }
231
+
232
+ @PluginMethod
233
+ public void isConnected(PluginCall call) {
234
+ JSObject result = new JSObject();
235
+ result.put("connected", printerCore.isConnected());
236
+
237
+ if (printerCore.isConnected()) {
238
+ result.put("address", printerCore.getConnectedAddress());
239
+ result.put("paperWidth", printerCore.getPaperWidth());
240
+ }
241
+
242
+ call.resolve(result);
243
+ }
244
+
245
+ // ==================== PRINTING ====================
246
+
247
+ @PluginMethod
248
+ public void printImage(PluginCall call) {
249
+ String imageBase64 = call.getString("imageBase64");
250
+ if (imageBase64 == null || imageBase64.isEmpty()) {
251
+ call.reject("imageBase64 is required");
252
+ return;
253
+ }
254
+
255
+ Float energyObj = call.getFloat("energy", 0.5f);
256
+ float energy = energyObj != null ? energyObj : 0.5f;
257
+ int quality = call.getInt("quality", 3);
258
+ int feedAfter = call.getInt("feedAfter", 100);
259
+ int threshold = call.getInt("threshold", 127);
260
+ Boolean ditherObj = call.getBoolean("dither", true);
261
+ boolean dither = ditherObj != null ? ditherObj : true;
262
+
263
+ printerCore.printImage(imageBase64, energy, quality, feedAfter, threshold, dither,
264
+ new CatPrinterCore.PrintCallback() {
265
+ @Override
266
+ public void onProgress(int percent, String status, String message) {
267
+ JSObject progress = new JSObject();
268
+ progress.put("percent", percent);
269
+ progress.put("status", status);
270
+ progress.put("message", message);
271
+ notifyListeners("printProgress", progress);
272
+ }
273
+
274
+ @Override
275
+ public void onComplete() {
276
+ call.resolve();
277
+ }
278
+
279
+ @Override
280
+ public void onError(String error) {
281
+ call.reject(error);
282
+ }
283
+ });
284
+ }
285
+
286
+ @PluginMethod
287
+ public void printText(PluginCall call) {
288
+ String text = call.getString("text");
289
+ if (text == null || text.isEmpty()) {
290
+ call.reject("text is required");
291
+ return;
292
+ }
293
+
294
+ int fontSize = call.getInt("fontSize", 24);
295
+ String align = call.getString("align", "left");
296
+ Boolean boldObj = call.getBoolean("bold", false);
297
+ boolean bold = boldObj != null ? boldObj : false;
298
+ Float lineSpacingObj = call.getFloat("lineSpacing", 1.2f);
299
+ float lineSpacing = lineSpacingObj != null ? lineSpacingObj : 1.2f;
300
+ Float energyObj = call.getFloat("energy", 0.6f);
301
+ float energy = energyObj != null ? energyObj : 0.6f;
302
+ int quality = call.getInt("quality", 3);
303
+ int feedAfter = call.getInt("feedAfter", 100);
304
+
305
+ printerCore.printText(text, fontSize, align, bold, lineSpacing,
306
+ energy, quality, feedAfter,
307
+ new CatPrinterCore.PrintCallback() {
308
+ @Override
309
+ public void onProgress(int percent, String status, String message) {
310
+ JSObject progress = new JSObject();
311
+ progress.put("percent", percent);
312
+ progress.put("status", status);
313
+ progress.put("message", message);
314
+ notifyListeners("printProgress", progress);
315
+ }
316
+
317
+ @Override
318
+ public void onComplete() {
319
+ call.resolve();
320
+ }
321
+
322
+ @Override
323
+ public void onError(String error) {
324
+ call.reject(error);
325
+ }
326
+ });
327
+ }
328
+
329
+ @PluginMethod
330
+ public void feedPaper(PluginCall call) {
331
+ int pixels = call.getInt("pixels", 100);
332
+
333
+ printerCore.feedPaper(pixels, new CatPrinterCore.PrintCallback() {
334
+ @Override
335
+ public void onProgress(int percent, String status, String message) {}
336
+
337
+ @Override
338
+ public void onComplete() {
339
+ call.resolve();
340
+ }
341
+
342
+ @Override
343
+ public void onError(String error) {
344
+ call.reject(error);
345
+ }
346
+ });
347
+ }
348
+ }
@@ -0,0 +1,213 @@
1
+ package khare.catprinter.plugin;
2
+
3
+ import android.graphics.Bitmap;
4
+ import android.graphics.Canvas;
5
+ import android.graphics.Color;
6
+ import android.graphics.Paint;
7
+ import android.graphics.Typeface;
8
+ import android.text.Layout;
9
+ import android.text.StaticLayout;
10
+ import android.text.TextPaint;
11
+
12
+ /**
13
+ * Image processing utilities for thermal printing.
14
+ * Handles resizing, dithering, and text rendering.
15
+ */
16
+ public class ImageProcessor {
17
+
18
+ /**
19
+ * Resize image to fit paper width while maintaining aspect ratio
20
+ */
21
+ public static Bitmap resizeToWidth(Bitmap source, int targetWidth) {
22
+ if (source.getWidth() == targetWidth) {
23
+ return source;
24
+ }
25
+
26
+ float ratio = (float) targetWidth / source.getWidth();
27
+ int targetHeight = Math.round(source.getHeight() * ratio);
28
+
29
+ return Bitmap.createScaledBitmap(source, targetWidth, targetHeight, true);
30
+ }
31
+
32
+ /**
33
+ * Convert image to 1-bit monochrome bitmap data
34
+ * Uses simple threshold dithering (good for receipts)
35
+ */
36
+ public static byte[] toMonochrome(Bitmap source, int threshold) {
37
+ int width = source.getWidth();
38
+ int height = source.getHeight();
39
+
40
+ // Ensure width is multiple of 8
41
+ int bytesPerLine = (width + 7) / 8;
42
+ byte[] result = new byte[bytesPerLine * height];
43
+
44
+ int[] pixels = new int[width * height];
45
+ source.getPixels(pixels, 0, width, 0, 0, width, height);
46
+
47
+ for (int y = 0; y < height; y++) {
48
+ for (int x = 0; x < width; x++) {
49
+ int pixel = pixels[y * width + x];
50
+
51
+ // Convert to grayscale
52
+ int r = Color.red(pixel);
53
+ int g = Color.green(pixel);
54
+ int b = Color.blue(pixel);
55
+ int gray = (r * 299 + g * 587 + b * 114) / 1000;
56
+
57
+ // Apply threshold (black = 1, white = 0 for thermal printer)
58
+ if (gray < threshold) {
59
+ int byteIndex = y * bytesPerLine + (x / 8);
60
+ int bitIndex = 7 - (x % 8);
61
+ result[byteIndex] |= (1 << bitIndex);
62
+ }
63
+ }
64
+ }
65
+
66
+ return result;
67
+ }
68
+
69
+ /**
70
+ * Convert image to 1-bit using Floyd-Steinberg dithering
71
+ * Better for images with gradients
72
+ */
73
+ public static byte[] toMonochromeDithered(Bitmap source, int threshold) {
74
+ int width = source.getWidth();
75
+ int height = source.getHeight();
76
+
77
+ int bytesPerLine = (width + 7) / 8;
78
+ byte[] result = new byte[bytesPerLine * height];
79
+
80
+ // Get pixels and convert to grayscale float array for error diffusion
81
+ int[] pixels = new int[width * height];
82
+ source.getPixels(pixels, 0, width, 0, 0, width, height);
83
+
84
+ float[] gray = new float[width * height];
85
+ for (int i = 0; i < pixels.length; i++) {
86
+ int pixel = pixels[i];
87
+ int r = Color.red(pixel);
88
+ int g = Color.green(pixel);
89
+ int b = Color.blue(pixel);
90
+ gray[i] = (r * 299 + g * 587 + b * 114) / 1000f;
91
+ }
92
+
93
+ // Floyd-Steinberg dithering
94
+ for (int y = 0; y < height; y++) {
95
+ for (int x = 0; x < width; x++) {
96
+ int idx = y * width + x;
97
+ float oldPixel = gray[idx];
98
+ float newPixel = oldPixel < threshold ? 0 : 255;
99
+ float error = oldPixel - newPixel;
100
+
101
+ // Set bit if black
102
+ if (newPixel == 0) {
103
+ int byteIndex = y * bytesPerLine + (x / 8);
104
+ int bitIndex = 7 - (x % 8);
105
+ result[byteIndex] |= (1 << bitIndex);
106
+ }
107
+
108
+ // Distribute error to neighbors
109
+ if (x + 1 < width) {
110
+ gray[idx + 1] += error * 7 / 16f;
111
+ }
112
+ if (y + 1 < height) {
113
+ if (x > 0) {
114
+ gray[idx + width - 1] += error * 3 / 16f;
115
+ }
116
+ gray[idx + width] += error * 5 / 16f;
117
+ if (x + 1 < width) {
118
+ gray[idx + width + 1] += error * 1 / 16f;
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ return result;
125
+ }
126
+
127
+ /**
128
+ * Render text to a bitmap
129
+ */
130
+ public static Bitmap renderText(String text, int paperWidth, int fontSize,
131
+ String align, boolean bold, float lineSpacing) {
132
+ // Create paint for text
133
+ TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
134
+ textPaint.setColor(Color.BLACK);
135
+ textPaint.setTextSize(fontSize);
136
+ textPaint.setTypeface(bold ? Typeface.DEFAULT_BOLD : Typeface.MONOSPACE);
137
+
138
+ // Determine alignment
139
+ Layout.Alignment alignment;
140
+ switch (align) {
141
+ case "center":
142
+ alignment = Layout.Alignment.ALIGN_CENTER;
143
+ break;
144
+ case "right":
145
+ alignment = Layout.Alignment.ALIGN_OPPOSITE;
146
+ break;
147
+ default:
148
+ alignment = Layout.Alignment.ALIGN_NORMAL;
149
+ }
150
+
151
+ // Create static layout to measure and render text
152
+ StaticLayout.Builder builder = StaticLayout.Builder.obtain(
153
+ text, 0, text.length(), textPaint, paperWidth
154
+ );
155
+ builder.setAlignment(alignment);
156
+ builder.setLineSpacing(0, lineSpacing);
157
+ builder.setIncludePad(true);
158
+
159
+ StaticLayout layout = builder.build();
160
+
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);
165
+
166
+ Bitmap bitmap = Bitmap.createBitmap(paperWidth, height, Bitmap.Config.ARGB_8888);
167
+ Canvas canvas = new Canvas(bitmap);
168
+ canvas.drawColor(Color.WHITE);
169
+
170
+ // Draw text
171
+ layout.draw(canvas);
172
+
173
+ return bitmap;
174
+ }
175
+
176
+ /**
177
+ * Flip bitmap horizontally
178
+ */
179
+ public static void flipHorizontal(byte[] data, int width, int height) {
180
+ int bytesPerLine = width / 8;
181
+ byte[] temp = new byte[bytesPerLine];
182
+
183
+ for (int y = 0; y < height; y++) {
184
+ int lineStart = y * bytesPerLine;
185
+
186
+ // Copy line to temp with reversed bytes and bits
187
+ for (int x = 0; x < bytesPerLine; x++) {
188
+ temp[bytesPerLine - 1 - x] = PrinterProtocol.reverseBits(data[lineStart + x]);
189
+ }
190
+
191
+ // Copy back
192
+ System.arraycopy(temp, 0, data, lineStart, bytesPerLine);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Flip bitmap vertically
198
+ */
199
+ public static void flipVertical(byte[] data, int width, int height) {
200
+ int bytesPerLine = width / 8;
201
+ byte[] temp = new byte[bytesPerLine];
202
+
203
+ for (int y = 0; y < height / 2; y++) {
204
+ int topLine = y * bytesPerLine;
205
+ int bottomLine = (height - 1 - y) * bytesPerLine;
206
+
207
+ // Swap lines
208
+ System.arraycopy(data, topLine, temp, 0, bytesPerLine);
209
+ System.arraycopy(data, bottomLine, data, topLine, bytesPerLine);
210
+ System.arraycopy(temp, 0, data, bottomLine, bytesPerLine);
211
+ }
212
+ }
213
+ }