@scr2em/capacitor-scanner 6.0.7 → 6.0.9
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 +12 -3
- package/android/src/main/java/com/leadliaion/capacitorscanner/CapacitorScannerPlugin.java +130 -4
- package/dist/docs.json +21 -3
- package/dist/esm/definitions.d.ts +9 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorScannerPlugin/CapacitorScannerPlugin.swift +18 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npx cap sync
|
|
|
16
16
|
* [`startScanning(...)`](#startscanning)
|
|
17
17
|
* [`stopScanning()`](#stopscanning)
|
|
18
18
|
* [`openSettings()`](#opensettings)
|
|
19
|
-
* [`capturePhoto()`](#capturephoto)
|
|
19
|
+
* [`capturePhoto(...)`](#capturephoto)
|
|
20
20
|
* [`checkPermissions()`](#checkpermissions)
|
|
21
21
|
* [`requestPermissions()`](#requestpermissions)
|
|
22
22
|
* [`flipCamera()`](#flipcamera)
|
|
@@ -62,12 +62,16 @@ openSettings() => Promise<void>
|
|
|
62
62
|
--------------------
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
### capturePhoto()
|
|
65
|
+
### capturePhoto(...)
|
|
66
66
|
|
|
67
67
|
```typescript
|
|
68
|
-
capturePhoto() => Promise<CapturePhotoResult>
|
|
68
|
+
capturePhoto(options?: CapturePhotoOptions | undefined) => Promise<CapturePhotoResult>
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
| Param | Type |
|
|
72
|
+
| ------------- | ------------------------------------------------------------------- |
|
|
73
|
+
| **`options`** | <code><a href="#capturephotooptions">CapturePhotoOptions</a></code> |
|
|
74
|
+
|
|
71
75
|
**Returns:** <code>Promise<<a href="#capturephotoresult">CapturePhotoResult</a>></code>
|
|
72
76
|
|
|
73
77
|
--------------------
|
|
@@ -151,6 +155,11 @@ removeAllListeners() => Promise<void>
|
|
|
151
155
|
<code>{ imageBase64: string }</code>
|
|
152
156
|
|
|
153
157
|
|
|
158
|
+
#### CapturePhotoOptions
|
|
159
|
+
|
|
160
|
+
<code>{ /** * The desired quality of the captured image, expressed as a value between 0.0 (lowest quality, smallest file size) * and 1.0 (highest quality, largest file size). Defaults to 1.0. * This parameter directly influences the compression level of the resulting JPEG image. */ qualityRatio?: number; }</code>
|
|
161
|
+
|
|
162
|
+
|
|
154
163
|
#### PermissionsResult
|
|
155
164
|
|
|
156
165
|
<code>{ camera: 'prompt' | 'denied' | 'granted' }</code>
|
|
@@ -3,8 +3,13 @@ package com.leadliaion.capacitorscanner;
|
|
|
3
3
|
import android.Manifest;
|
|
4
4
|
import android.content.Intent;
|
|
5
5
|
import android.content.pm.PackageManager;
|
|
6
|
+
import android.graphics.Bitmap;
|
|
7
|
+
import android.graphics.BitmapFactory;
|
|
6
8
|
import android.graphics.Color;
|
|
9
|
+
import android.graphics.Matrix;
|
|
10
|
+
import androidx.exifinterface.media.ExifInterface;
|
|
7
11
|
import android.net.Uri;
|
|
12
|
+
import android.os.Build;
|
|
8
13
|
import android.provider.Settings;
|
|
9
14
|
import android.util.Base64;
|
|
10
15
|
import android.util.DisplayMetrics;
|
|
@@ -49,8 +54,10 @@ import com.google.mlkit.vision.barcode.BarcodeScannerOptions;
|
|
|
49
54
|
import com.google.mlkit.vision.barcode.BarcodeScanning;
|
|
50
55
|
import com.google.mlkit.vision.common.InputImage;
|
|
51
56
|
|
|
57
|
+
import java.io.ByteArrayInputStream;
|
|
52
58
|
import java.io.ByteArrayOutputStream;
|
|
53
59
|
import java.io.IOException;
|
|
60
|
+
import java.io.InputStream;
|
|
54
61
|
import java.util.concurrent.ExecutionException;
|
|
55
62
|
import java.util.concurrent.Executor;
|
|
56
63
|
import java.util.concurrent.Executors;
|
|
@@ -390,6 +397,10 @@ public class CapacitorScannerPlugin extends Plugin {
|
|
|
390
397
|
return;
|
|
391
398
|
}
|
|
392
399
|
|
|
400
|
+
Double qualityRatioObj = call.getDouble("qualityRatio", 1.0);
|
|
401
|
+
double qualityRatio = qualityRatioObj != null ? qualityRatioObj : 1.0;
|
|
402
|
+
|
|
403
|
+
// Temporary stream to receive the captured image data.
|
|
393
404
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
394
405
|
|
|
395
406
|
ImageCapture.OutputFileOptions outputOptions =
|
|
@@ -400,17 +411,112 @@ public class CapacitorScannerPlugin extends Plugin {
|
|
|
400
411
|
@Override
|
|
401
412
|
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
|
|
402
413
|
try {
|
|
403
|
-
|
|
414
|
+
// Get the original image bytes.
|
|
415
|
+
byte[] originalBytes = outputStream.toByteArray();
|
|
416
|
+
int originalSize = originalBytes.length; // in bytes
|
|
417
|
+
|
|
418
|
+
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
419
|
+
options.inJustDecodeBounds = true;
|
|
420
|
+
BitmapFactory.decodeByteArray(originalBytes, 0, originalSize, options);
|
|
421
|
+
|
|
422
|
+
// If qualityRatio is between 0 and 1 exclusively, perform quality reduction.
|
|
423
|
+
if (qualityRatio > 0 && qualityRatio < 1) {
|
|
424
|
+
// Calculate the target size in bytes.
|
|
425
|
+
int targetSize = (int) (originalSize * qualityRatio);
|
|
426
|
+
|
|
427
|
+
// Read EXIF orientation data from original image
|
|
428
|
+
int orientation = ExifInterface.ORIENTATION_UNDEFINED;
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
InputStream inputStream = new ByteArrayInputStream(originalBytes);
|
|
432
|
+
ExifInterface exif = null;
|
|
433
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
434
|
+
exif = new ExifInterface(inputStream);
|
|
435
|
+
orientation = exif.getAttributeInt(
|
|
436
|
+
ExifInterface.TAG_ORIENTATION,
|
|
437
|
+
ExifInterface.ORIENTATION_UNDEFINED);
|
|
438
|
+
}
|
|
439
|
+
inputStream.close();
|
|
440
|
+
} catch (IOException e) {
|
|
441
|
+
echo("Warning: Could not read EXIF orientation: " + e.getMessage());
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
// Decode the captured image into a Bitmap.
|
|
446
|
+
options.inJustDecodeBounds = false;
|
|
447
|
+
options.inSampleSize = calculateInSampleSize(options, 1024, 1024); // Max size of 1024x1024
|
|
448
|
+
Bitmap originalBitmap = BitmapFactory.decodeByteArray(originalBytes, 0, originalSize, options);
|
|
449
|
+
if (originalBitmap == null) {
|
|
450
|
+
call.reject("Failed to decode captured image.");
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Apply rotation based on EXIF orientation
|
|
455
|
+
Matrix matrix = new Matrix();
|
|
456
|
+
switch (orientation) {
|
|
457
|
+
case ExifInterface.ORIENTATION_ROTATE_90:
|
|
458
|
+
matrix.postRotate(90);
|
|
459
|
+
break;
|
|
460
|
+
case ExifInterface.ORIENTATION_ROTATE_180:
|
|
461
|
+
matrix.postRotate(180);
|
|
462
|
+
break;
|
|
463
|
+
case ExifInterface.ORIENTATION_ROTATE_270:
|
|
464
|
+
matrix.postRotate(270);
|
|
465
|
+
break;
|
|
466
|
+
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
|
|
467
|
+
matrix.postScale(-1, 1);
|
|
468
|
+
break;
|
|
469
|
+
case ExifInterface.ORIENTATION_FLIP_VERTICAL:
|
|
470
|
+
matrix.postScale(1, -1);
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Apply the rotation if needed
|
|
475
|
+
if (!matrix.isIdentity()) {
|
|
476
|
+
Bitmap rotatedBitmap = Bitmap.createBitmap(
|
|
477
|
+
originalBitmap, 0, 0,
|
|
478
|
+
originalBitmap.getWidth(), originalBitmap.getHeight(),
|
|
479
|
+
matrix, true);
|
|
480
|
+
originalBitmap.recycle(); // Recycle the old bitmap to free memory.
|
|
481
|
+
originalBitmap = rotatedBitmap;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Prepare a stream to hold the compressed bitmap.
|
|
485
|
+
ByteArrayOutputStream compressedStream = new ByteArrayOutputStream();
|
|
486
|
+
int quality = 100; // Start with maximum quality (100)
|
|
487
|
+
|
|
488
|
+
// Iteratively compress the bitmap until it reaches the target file size or quality drops to 10%.
|
|
489
|
+
int maxIterations = 10; // Limit iterations to prevent excessive looping.
|
|
490
|
+
int iterations = 0;
|
|
491
|
+
do {
|
|
492
|
+
compressedStream.reset();
|
|
493
|
+
originalBitmap.compress(Bitmap.CompressFormat.JPEG, quality, compressedStream);
|
|
494
|
+
quality -= 5; // Reduce quality step by step.
|
|
495
|
+
iterations++;
|
|
496
|
+
} while (compressedStream.toByteArray().length > targetSize && quality > 10 && iterations < maxIterations);
|
|
497
|
+
|
|
498
|
+
// Use the compressed image bytes.
|
|
499
|
+
originalBytes = compressedStream.toByteArray();
|
|
500
|
+
|
|
501
|
+
Bitmap finalBitmap = BitmapFactory.decodeByteArray(originalBytes, 0, originalBytes.length);
|
|
502
|
+
if (finalBitmap != null) {
|
|
503
|
+
finalBitmap.recycle();
|
|
504
|
+
}
|
|
404
505
|
|
|
405
|
-
|
|
506
|
+
// Cleanup: recycle the bitmap and close the compressed stream.
|
|
507
|
+
originalBitmap.recycle();
|
|
508
|
+
compressedStream.close();
|
|
509
|
+
}
|
|
406
510
|
|
|
511
|
+
// Convert the final image bytes to a Base64 string.
|
|
512
|
+
String base64 = Base64.encodeToString(originalBytes, Base64.NO_WRAP);
|
|
407
513
|
JSObject ret = new JSObject();
|
|
408
514
|
ret.put("imageBase64", "data:image/jpeg;base64," + base64);
|
|
409
|
-
|
|
410
515
|
call.resolve(ret);
|
|
411
516
|
} catch (Exception e) {
|
|
412
517
|
call.reject("Failed to process image: " + e.getMessage());
|
|
413
518
|
} finally {
|
|
519
|
+
// Ensure resources are cleaned up.
|
|
414
520
|
try {
|
|
415
521
|
outputStream.close();
|
|
416
522
|
} catch (IOException e) {
|
|
@@ -426,12 +532,32 @@ public class CapacitorScannerPlugin extends Plugin {
|
|
|
426
532
|
try {
|
|
427
533
|
outputStream.close();
|
|
428
534
|
} catch (IOException e) {
|
|
429
|
-
|
|
535
|
+
echo("Failed to close output stream: " + e.getMessage());
|
|
430
536
|
}
|
|
431
537
|
}
|
|
432
538
|
});
|
|
433
539
|
}
|
|
434
540
|
|
|
541
|
+
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
|
|
542
|
+
// Calculate the largest inSampleSize that is a power of 2 and keeps the image smaller than the requested width/height.
|
|
543
|
+
final int height = options.outHeight;
|
|
544
|
+
final int width = options.outWidth;
|
|
545
|
+
int inSampleSize = 1;
|
|
546
|
+
|
|
547
|
+
if (height > reqHeight || width > reqWidth) {
|
|
548
|
+
final int halfHeight = height / 2;
|
|
549
|
+
final int halfWidth = width / 2;
|
|
550
|
+
|
|
551
|
+
// Calculate the largest inSampleSize value that is a power of 2 and keeps the image smaller than the requested width/height.
|
|
552
|
+
while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
|
|
553
|
+
inSampleSize *= 2;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return inSampleSize;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
|
|
435
561
|
|
|
436
562
|
@PluginMethod
|
|
437
563
|
public void checkPermissions(PluginCall call) {
|
package/dist/docs.json
CHANGED
|
@@ -45,13 +45,20 @@
|
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"name": "capturePhoto",
|
|
48
|
-
"signature": "() => Promise<CapturePhotoResult>",
|
|
49
|
-
"parameters": [
|
|
48
|
+
"signature": "(options?: CapturePhotoOptions | undefined) => Promise<CapturePhotoResult>",
|
|
49
|
+
"parameters": [
|
|
50
|
+
{
|
|
51
|
+
"name": "options",
|
|
52
|
+
"docs": "",
|
|
53
|
+
"type": "CapturePhotoOptions | undefined"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
50
56
|
"returns": "Promise<CapturePhotoResult>",
|
|
51
57
|
"tags": [],
|
|
52
58
|
"docs": "",
|
|
53
59
|
"complexTypes": [
|
|
54
|
-
"CapturePhotoResult"
|
|
60
|
+
"CapturePhotoResult",
|
|
61
|
+
"CapturePhotoOptions"
|
|
55
62
|
],
|
|
56
63
|
"slug": "capturephoto"
|
|
57
64
|
},
|
|
@@ -237,6 +244,17 @@
|
|
|
237
244
|
}
|
|
238
245
|
]
|
|
239
246
|
},
|
|
247
|
+
{
|
|
248
|
+
"name": "CapturePhotoOptions",
|
|
249
|
+
"slug": "capturephotooptions",
|
|
250
|
+
"docs": "",
|
|
251
|
+
"types": [
|
|
252
|
+
{
|
|
253
|
+
"text": "{\n /**\n * The desired quality of the captured image, expressed as a value between 0.0 (lowest quality, smallest file size)\n * and 1.0 (highest quality, largest file size). Defaults to 1.0.\n * This parameter directly influences the compression level of the resulting JPEG image.\n */\n qualityRatio?: number;\n}",
|
|
254
|
+
"complexTypes": []
|
|
255
|
+
}
|
|
256
|
+
]
|
|
257
|
+
},
|
|
240
258
|
{
|
|
241
259
|
"name": "PermissionsResult",
|
|
242
260
|
"slug": "permissionsresult",
|
|
@@ -2,7 +2,7 @@ export interface CapacitorScannerPlugin {
|
|
|
2
2
|
startScanning(options?: ScannerOptions): Promise<void>;
|
|
3
3
|
stopScanning(): Promise<void>;
|
|
4
4
|
openSettings(): Promise<void>;
|
|
5
|
-
capturePhoto(): Promise<CapturePhotoResult>;
|
|
5
|
+
capturePhoto(options?: CapturePhotoOptions): Promise<CapturePhotoResult>;
|
|
6
6
|
checkPermissions(): Promise<PermissionsResult>;
|
|
7
7
|
requestPermissions(): Promise<PermissionsResult>;
|
|
8
8
|
flipCamera(): Promise<void>;
|
|
@@ -19,6 +19,14 @@ export declare type BarcodeScannedEvent = {
|
|
|
19
19
|
scannedCode: string;
|
|
20
20
|
format: string;
|
|
21
21
|
};
|
|
22
|
+
export declare type CapturePhotoOptions = {
|
|
23
|
+
/**
|
|
24
|
+
* The desired quality of the captured image, expressed as a value between 0.0 (lowest quality, smallest file size)
|
|
25
|
+
* and 1.0 (highest quality, largest file size). Defaults to 1.0.
|
|
26
|
+
* This parameter directly influences the compression level of the resulting JPEG image.
|
|
27
|
+
*/
|
|
28
|
+
qualityRatio?: number;
|
|
29
|
+
};
|
|
22
30
|
export declare type PermissionsResult = {
|
|
23
31
|
camera: 'prompt' | 'denied' | 'granted';
|
|
24
32
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAoCA,MAAM,CAAN,IAAY,aAYX;AAZD,WAAY,aAAa;IACvB,gCAAe,CAAA;IACf,mCAAkB,CAAA;IAClB,mCAAkB,CAAA;IAClB,qCAAoB,CAAA;IACpB,2CAA0B,CAAA;IAC1B,+BAAc,CAAA;IACd,iCAAgB,CAAA;IAChB,gCAAe,CAAA;IACf,mCAAkB,CAAA;IAClB,mCAAkB,CAAA;IAClB,+BAAc,CAAA;AAChB,CAAC,EAZW,aAAa,KAAb,aAAa,QAYxB","sourcesContent":["export interface CapacitorScannerPlugin {\n startScanning(options?: ScannerOptions): Promise<void>;\n stopScanning(): Promise<void>;\n openSettings(): Promise<void>;\n capturePhoto(options?: CapturePhotoOptions): Promise<CapturePhotoResult>;\n checkPermissions(): Promise<PermissionsResult>;\n requestPermissions(): Promise<PermissionsResult>;\n flipCamera(): Promise<void>;\n toggleFlash(): Promise<FlashResult>;\n addListener(event: 'barcodeScanned', listenerFunc: (result: BarcodeScannedEvent) => void): Promise<void>;\n removeAllListeners(): Promise<void>;\n}\n\nexport type ScannerOptions = {\n formats?: BarcodeFormat[];\n cameraDirection?: 'BACK' | 'FRONT';\n debounceTimeInMilli?: number\n};\n\nexport type BarcodeScannedEvent = { scannedCode: string; format: string };\n\nexport type CapturePhotoOptions = {\n /**\n * The desired quality of the captured image, expressed as a value between 0.0 (lowest quality, smallest file size)\n * and 1.0 (highest quality, largest file size). Defaults to 1.0.\n * This parameter directly influences the compression level of the resulting JPEG image.\n */\n qualityRatio?: number;\n};\n\nexport type PermissionsResult = { camera: 'prompt' | 'denied' | 'granted' };\n\nexport type CapturePhotoResult = { imageBase64: string };\n\nexport type FlashResult = { enabled: boolean };\n\nexport enum BarcodeFormat {\n Aztec = 'AZTEC',\n Code39 = 'CODE_39',\n Code93 = 'CODE_93',\n Code128 = 'CODE_128',\n DataMatrix = 'DATA_MATRIX',\n Ean8 = 'EAN_8',\n Ean13 = 'EAN_13',\n Itf14 = 'ITF14',\n Pdf417 = 'PDF_417',\n QrCode = 'QR_CODE',\n UpcE = 'UPC_E',\n}\n\ndeclare global {\n interface PluginRegistry {\n QRScanner: CapacitorScannerPlugin;\n }\n}"]}
|
|
@@ -56,7 +56,24 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
|
|
|
56
56
|
return
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
// Get quality ratio from options, default to 1.0, clamp between 0.0 and 1.0
|
|
60
|
+
let quality = call.getFloat("qualityRatio", 1.0)
|
|
61
|
+
let clampedQuality = max(0.0, min(1.0, quality))
|
|
62
|
+
let qualityNumber = NSNumber(value: clampedQuality) // Convert to NSNumber
|
|
63
|
+
|
|
64
|
+
var photoSettings = AVCapturePhotoSettings() // Initialize with default settings
|
|
65
|
+
|
|
66
|
+
// Check if JPEG is supported and apply compression settings if it is
|
|
67
|
+
if photoOutput.supportedPhotoCodecTypes(for: .jpg).contains(.jpeg) {
|
|
68
|
+
// Create settings specific for JPEG with quality
|
|
69
|
+
photoSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg, // Use the type directly
|
|
70
|
+
AVVideoCompressionPropertiesKey: [AVVideoQualityKey: qualityNumber]])
|
|
71
|
+
photoSettings.photoQualityPrioritization = .quality // Prioritize quality
|
|
72
|
+
} else {
|
|
73
|
+
print("JPEG codec not supported for photo capture. Using default settings.")
|
|
74
|
+
// photoSettings remains the default initialized one
|
|
75
|
+
}
|
|
76
|
+
|
|
60
77
|
self.capturePhotoCall = call
|
|
61
78
|
photoOutput.capturePhoto(with: photoSettings, delegate: self)
|
|
62
79
|
}
|