@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.
- package/LICENSE +21 -0
- package/README.md +324 -0
- package/android/build.gradle +44 -0
- package/android/src/main/AndroidManifest.xml +21 -0
- package/android/src/main/java/khare/catprinter/plugin/CatPrinterCore.java +661 -0
- package/android/src/main/java/khare/catprinter/plugin/CatPrinterPlugin.java +348 -0
- package/android/src/main/java/khare/catprinter/plugin/ImageProcessor.java +213 -0
- package/android/src/main/java/khare/catprinter/plugin/PrinterProtocol.java +243 -0
- package/dist/esm/definitions.d.ts +189 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +8 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +13 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +28 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +51 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +54 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +52 -0
|
@@ -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
|
+
}
|