@capgo/capacitor-document-scanner 7.0.6 → 7.0.7

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.
@@ -1,22 +1,292 @@
1
- package app.capgo.plugin.document_scanner;
1
+ package app.capgo.plugin.documentscanner;
2
2
 
3
+ import android.app.Activity;
4
+ import android.content.Context;
5
+ import android.content.Intent;
6
+ import android.graphics.Bitmap;
7
+ import android.graphics.BitmapFactory;
8
+ import android.net.Uri;
9
+ import android.util.Base64;
10
+ import androidx.activity.result.ActivityResult;
11
+ import androidx.activity.result.ActivityResultLauncher;
12
+ import androidx.activity.result.IntentSenderRequest;
13
+ import androidx.activity.result.contract.ActivityResultContracts;
14
+ import com.getcapacitor.JSArray;
3
15
  import com.getcapacitor.JSObject;
16
+ import com.getcapacitor.Logger;
4
17
  import com.getcapacitor.Plugin;
5
18
  import com.getcapacitor.PluginCall;
6
19
  import com.getcapacitor.PluginMethod;
7
20
  import com.getcapacitor.annotation.CapacitorPlugin;
21
+ import com.google.mlkit.vision.documentscanner.GmsDocumentScanner;
22
+ import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions;
23
+ import com.google.mlkit.vision.documentscanner.GmsDocumentScanning;
24
+ import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult;
25
+ import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult.Page;
26
+ import java.io.ByteArrayOutputStream;
27
+ import java.io.File;
28
+ import java.io.FileOutputStream;
29
+ import java.io.IOException;
30
+ import java.io.InputStream;
31
+ import java.util.ArrayList;
32
+ import java.util.List;
33
+ import java.util.Locale;
8
34
 
35
+ /**
36
+ * Bridges Capacitor calls to the ML Kit document scanner.
37
+ */
9
38
  @CapacitorPlugin(name = "DocumentScanner")
10
39
  public class DocumentScannerPlugin extends Plugin {
11
40
 
12
- private DocumentScanner implementation = new DocumentScanner();
41
+ private static final String RESPONSE_TYPE_BASE64 = "base64";
42
+ private static final String RESPONSE_TYPE_FILE_PATH = "imageFilePath";
43
+
44
+ private ActivityResultLauncher<IntentSenderRequest> scannerLauncher;
45
+ private PendingScan pendingScan;
46
+
47
+ private static class PendingScan {
48
+
49
+ private final String callId;
50
+ private final String responseType;
51
+ private final int quality;
52
+
53
+ PendingScan(String callId, String responseType, int quality) {
54
+ this.callId = callId;
55
+ this.responseType = responseType;
56
+ this.quality = quality;
57
+ }
58
+ }
59
+
60
+ @Override
61
+ public void load() {
62
+ super.load();
63
+ scannerLauncher = bridge.registerForActivityResult(
64
+ new ActivityResultContracts.StartIntentSenderForResult(),
65
+ this::handleScanResult
66
+ );
67
+ }
13
68
 
14
69
  @PluginMethod
15
- public void echo(PluginCall call) {
16
- String value = call.getString("value");
70
+ public void scanDocument(PluginCall call) {
71
+ if (scannerLauncher == null) {
72
+ call.reject("Document scanner is not ready.");
73
+ return;
74
+ }
75
+
76
+ if (pendingScan != null) {
77
+ call.reject("Another scan is in progress.");
78
+ return;
79
+ }
80
+
81
+ Activity activity = getActivity();
82
+ if (activity == null) {
83
+ call.reject("Activity reference is unavailable.");
84
+ return;
85
+ }
86
+
87
+ int quality = clamp(call.getInt("croppedImageQuality", 100), 0, 100);
88
+ String responseType = normalizeResponseType(call.getString("responseType"));
89
+ int pageLimit = clamp(call.getInt("maxNumDocuments", 24), 1, 24);
90
+ boolean allowAdjustCrop = call.getBoolean("letUserAdjustCrop", true);
91
+
92
+ GmsDocumentScannerOptions.Builder optionsBuilder = new GmsDocumentScannerOptions.Builder()
93
+ .setGalleryImportAllowed(false)
94
+ .setResultFormats(GmsDocumentScannerOptions.RESULT_FORMAT_JPEG)
95
+ .setPageLimit(pageLimit);
96
+
97
+ optionsBuilder.setScannerMode(
98
+ allowAdjustCrop ? GmsDocumentScannerOptions.SCANNER_MODE_FULL : GmsDocumentScannerOptions.SCANNER_MODE_BASE
99
+ );
100
+
101
+ GmsDocumentScanner scanner = GmsDocumentScanning.getClient(optionsBuilder.build());
102
+
103
+ bridge.saveCall(call);
104
+ pendingScan = new PendingScan(call.getCallbackId(), responseType, quality);
105
+
106
+ scanner
107
+ .getStartScanIntent(activity)
108
+ .addOnSuccessListener((intentSender) -> {
109
+ IntentSenderRequest request = new IntentSenderRequest.Builder(intentSender).build();
110
+ scannerLauncher.launch(request);
111
+ })
112
+ .addOnFailureListener((e) -> {
113
+ Logger.error("DocumentScanner", "Failed to start scanner", e);
114
+ PluginCall savedCall = getPendingCall();
115
+ if (savedCall != null) {
116
+ savedCall.reject("Unable to start document scanner: " + e.getLocalizedMessage(), e);
117
+ releasePendingCall(savedCall);
118
+ } else {
119
+ bridge.releaseCall(call);
120
+ pendingScan = null;
121
+ call.reject("Unable to start document scanner: " + e.getLocalizedMessage(), e);
122
+ }
123
+ });
124
+ }
125
+
126
+ private void handleScanResult(ActivityResult result) {
127
+ PluginCall call = getPendingCall();
128
+ if (call == null) {
129
+ return;
130
+ }
131
+
132
+ if (result.getResultCode() != Activity.RESULT_OK) {
133
+ JSObject response = new JSObject();
134
+ response.put("status", "cancel");
135
+ call.resolve(response);
136
+ releasePendingCall(call);
137
+ return;
138
+ }
139
+
140
+ Intent data = result.getData();
141
+ if (data == null) {
142
+ call.reject("Document scanner returned no data.");
143
+ releasePendingCall(call);
144
+ return;
145
+ }
146
+
147
+ GmsDocumentScanningResult scanningResult = GmsDocumentScanningResult.fromActivityResultIntent(data);
148
+ if (scanningResult == null) {
149
+ call.reject("Unable to parse document scan result.");
150
+ releasePendingCall(call);
151
+ return;
152
+ }
153
+
154
+ try {
155
+ List<String> scannedImages = processScanResult(scanningResult);
156
+ JSObject response = new JSObject();
157
+ response.put("status", "success");
158
+ response.put("scannedImages", new JSArray(scannedImages));
159
+ call.resolve(response);
160
+ } catch (IOException ioException) {
161
+ call.reject("Failed to process scanned images: " + ioException.getLocalizedMessage(), ioException);
162
+ } finally {
163
+ releasePendingCall(call);
164
+ }
165
+ }
166
+
167
+ private List<String> processScanResult(GmsDocumentScanningResult scanningResult) throws IOException {
168
+ List<String> results = new ArrayList<>();
169
+ List<Page> pages = scanningResult.getPages();
170
+ if (pages == null || pages.isEmpty()) {
171
+ return results;
172
+ }
173
+
174
+ for (int index = 0; index < pages.size(); index++) {
175
+ String processed = handlePage(pages.get(index), index);
176
+ if (processed != null) {
177
+ results.add(processed);
178
+ }
179
+ }
180
+ return results;
181
+ }
182
+
183
+ private String handlePage(Page page, int pageIndex) throws IOException {
184
+ PendingScan scan = pendingScan;
185
+ if (scan == null) {
186
+ throw new IOException("No active scan.");
187
+ }
188
+
189
+ Uri imageUri = page.getImageUri();
190
+ if (imageUri == null) {
191
+ throw new IOException("Missing image URI for scanned page.");
192
+ }
193
+
194
+ byte[] imageBytes = readBytesFromUri(imageUri);
195
+ if (scan.quality < 100) {
196
+ imageBytes = reencodeImage(imageBytes, scan.quality);
197
+ }
198
+
199
+ if (RESPONSE_TYPE_BASE64.equals(scan.responseType)) {
200
+ return Base64.encodeToString(imageBytes, Base64.NO_WRAP);
201
+ }
202
+
203
+ return writeImageFile(imageBytes, pageIndex);
204
+ }
205
+
206
+ private byte[] readBytesFromUri(Uri uri) throws IOException {
207
+ Context context = getContext();
208
+ if (context == null) {
209
+ throw new IOException("Context unavailable for reading image.");
210
+ }
211
+
212
+ try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) {
213
+ if (inputStream == null) {
214
+ throw new IOException("Unable to open image stream.");
215
+ }
216
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
217
+ byte[] data = new byte[8192];
218
+ int nRead;
219
+ while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
220
+ buffer.write(data, 0, nRead);
221
+ }
222
+ return buffer.toByteArray();
223
+ }
224
+ }
225
+
226
+ private byte[] reencodeImage(byte[] source, int quality) throws IOException {
227
+ Bitmap bitmap = BitmapFactory.decodeByteArray(source, 0, source.length);
228
+ if (bitmap == null) {
229
+ throw new IOException("Unable to decode scanned image.");
230
+ }
231
+ try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
232
+ if (!bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)) {
233
+ throw new IOException("Unable to compress scanned image.");
234
+ }
235
+ return outputStream.toByteArray();
236
+ } finally {
237
+ bitmap.recycle();
238
+ }
239
+ }
240
+
241
+ private String writeImageFile(byte[] imageBytes, int pageIndex) throws IOException {
242
+ Context context = getContext();
243
+ if (context == null) {
244
+ throw new IOException("Context unavailable for writing image.");
245
+ }
246
+
247
+ File directory = new File(context.getCacheDir(), "document_scanner");
248
+ if (!directory.exists() && !directory.mkdirs()) {
249
+ throw new IOException("Unable to create cache directory.");
250
+ }
251
+
252
+ String fileName = String.format(Locale.US, "DOCUMENT_SCAN_%d_%d.jpg", pageIndex, System.currentTimeMillis());
253
+ File outputFile = new File(directory, fileName);
254
+ try (FileOutputStream outputStream = new FileOutputStream(outputFile)) {
255
+ outputStream.write(imageBytes);
256
+ }
257
+
258
+ return outputFile.getAbsolutePath();
259
+ }
260
+
261
+ private PluginCall getPendingCall() {
262
+ if (pendingScan == null) {
263
+ return null;
264
+ }
265
+ return bridge.getSavedCall(pendingScan.callId);
266
+ }
267
+
268
+ private void releasePendingCall(PluginCall call) {
269
+ if (pendingScan != null) {
270
+ bridge.releaseCall(call);
271
+ pendingScan = null;
272
+ }
273
+ }
274
+
275
+ private int clamp(Integer value, int min, int max) {
276
+ if (value == null) {
277
+ return max;
278
+ }
279
+ return Math.max(min, Math.min(max, value));
280
+ }
17
281
 
18
- JSObject ret = new JSObject();
19
- ret.put("value", implementation.echo(value));
20
- call.resolve(ret);
282
+ private String normalizeResponseType(String value) {
283
+ if (value == null) {
284
+ return RESPONSE_TYPE_FILE_PATH;
285
+ }
286
+ String normalized = value.toLowerCase(Locale.ROOT);
287
+ if (RESPONSE_TYPE_BASE64.equals(normalized) || RESPONSE_TYPE_FILE_PATH.equals(normalized)) {
288
+ return normalized;
289
+ }
290
+ return RESPONSE_TYPE_FILE_PATH;
21
291
  }
22
292
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-document-scanner",
3
- "version": "7.0.6",
3
+ "version": "7.0.7",
4
4
  "description": "Capacitor plugin to scan document iOS and Android",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -1,11 +0,0 @@
1
- package app.capgo.plugin.document_scanner;
2
-
3
- import com.getcapacitor.Logger;
4
-
5
- public class DocumentScanner {
6
-
7
- public String echo(String value) {
8
- Logger.info("Echo", value);
9
- return value;
10
- }
11
- }
@@ -1,292 +0,0 @@
1
- package app.capgo.plugin.documentscanner;
2
-
3
- import android.app.Activity;
4
- import android.content.Context;
5
- import android.content.Intent;
6
- import android.graphics.Bitmap;
7
- import android.graphics.BitmapFactory;
8
- import android.net.Uri;
9
- import android.util.Base64;
10
- import androidx.activity.result.ActivityResult;
11
- import androidx.activity.result.ActivityResultLauncher;
12
- import androidx.activity.result.IntentSenderRequest;
13
- import androidx.activity.result.contract.ActivityResultContracts;
14
- import com.getcapacitor.JSArray;
15
- import com.getcapacitor.JSObject;
16
- import com.getcapacitor.Logger;
17
- import com.getcapacitor.Plugin;
18
- import com.getcapacitor.PluginCall;
19
- import com.getcapacitor.PluginMethod;
20
- import com.getcapacitor.annotation.CapacitorPlugin;
21
- import com.google.mlkit.vision.documentscanner.GmsDocumentScanner;
22
- import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions;
23
- import com.google.mlkit.vision.documentscanner.GmsDocumentScanning;
24
- import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult;
25
- import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult.Page;
26
- import java.io.ByteArrayOutputStream;
27
- import java.io.File;
28
- import java.io.FileOutputStream;
29
- import java.io.IOException;
30
- import java.io.InputStream;
31
- import java.util.ArrayList;
32
- import java.util.List;
33
- import java.util.Locale;
34
-
35
- /**
36
- * Bridges Capacitor calls to the ML Kit document scanner.
37
- */
38
- @CapacitorPlugin(name = "DocumentScanner")
39
- public class DocumentScannerPlugin extends Plugin {
40
-
41
- private static final String RESPONSE_TYPE_BASE64 = "base64";
42
- private static final String RESPONSE_TYPE_FILE_PATH = "imageFilePath";
43
-
44
- private ActivityResultLauncher<IntentSenderRequest> scannerLauncher;
45
- private PendingScan pendingScan;
46
-
47
- private static class PendingScan {
48
-
49
- private final String callId;
50
- private final String responseType;
51
- private final int quality;
52
-
53
- PendingScan(String callId, String responseType, int quality) {
54
- this.callId = callId;
55
- this.responseType = responseType;
56
- this.quality = quality;
57
- }
58
- }
59
-
60
- @Override
61
- public void load() {
62
- super.load();
63
- scannerLauncher = bridge.registerForActivityResult(
64
- new ActivityResultContracts.StartIntentSenderForResult(),
65
- this::handleScanResult
66
- );
67
- }
68
-
69
- @PluginMethod
70
- public void scanDocument(PluginCall call) {
71
- if (scannerLauncher == null) {
72
- call.reject("Document scanner is not ready.");
73
- return;
74
- }
75
-
76
- if (pendingScan != null) {
77
- call.reject("Another scan is in progress.");
78
- return;
79
- }
80
-
81
- Activity activity = getActivity();
82
- if (activity == null) {
83
- call.reject("Activity reference is unavailable.");
84
- return;
85
- }
86
-
87
- int quality = clamp(call.getInt("croppedImageQuality", 100), 0, 100);
88
- String responseType = normalizeResponseType(call.getString("responseType"));
89
- int pageLimit = clamp(call.getInt("maxNumDocuments", 24), 1, 24);
90
- boolean allowAdjustCrop = call.getBoolean("letUserAdjustCrop", true);
91
-
92
- GmsDocumentScannerOptions.Builder optionsBuilder = new GmsDocumentScannerOptions.Builder()
93
- .setGalleryImportAllowed(false)
94
- .setResultFormats(GmsDocumentScannerOptions.RESULT_FORMAT_JPEG)
95
- .setPageLimit(pageLimit);
96
-
97
- optionsBuilder.setScannerMode(
98
- allowAdjustCrop ? GmsDocumentScannerOptions.SCANNER_MODE_FULL : GmsDocumentScannerOptions.SCANNER_MODE_BASE
99
- );
100
-
101
- GmsDocumentScanner scanner = GmsDocumentScanning.getClient(optionsBuilder.build());
102
-
103
- bridge.saveCall(call);
104
- pendingScan = new PendingScan(call.getCallbackId(), responseType, quality);
105
-
106
- scanner
107
- .getStartScanIntent(activity)
108
- .addOnSuccessListener((intentSender) -> {
109
- IntentSenderRequest request = new IntentSenderRequest.Builder(intentSender).build();
110
- scannerLauncher.launch(request);
111
- })
112
- .addOnFailureListener((e) -> {
113
- Logger.error("DocumentScanner", "Failed to start scanner", e);
114
- PluginCall savedCall = getPendingCall();
115
- if (savedCall != null) {
116
- savedCall.reject("Unable to start document scanner: " + e.getLocalizedMessage(), e);
117
- releasePendingCall(savedCall);
118
- } else {
119
- bridge.releaseCall(call);
120
- pendingScan = null;
121
- call.reject("Unable to start document scanner: " + e.getLocalizedMessage(), e);
122
- }
123
- });
124
- }
125
-
126
- private void handleScanResult(ActivityResult result) {
127
- PluginCall call = getPendingCall();
128
- if (call == null) {
129
- return;
130
- }
131
-
132
- if (result.getResultCode() != Activity.RESULT_OK) {
133
- JSObject response = new JSObject();
134
- response.put("status", "cancel");
135
- call.resolve(response);
136
- releasePendingCall(call);
137
- return;
138
- }
139
-
140
- Intent data = result.getData();
141
- if (data == null) {
142
- call.reject("Document scanner returned no data.");
143
- releasePendingCall(call);
144
- return;
145
- }
146
-
147
- GmsDocumentScanningResult scanningResult = GmsDocumentScanningResult.fromActivityResultIntent(data);
148
- if (scanningResult == null) {
149
- call.reject("Unable to parse document scan result.");
150
- releasePendingCall(call);
151
- return;
152
- }
153
-
154
- try {
155
- List<String> scannedImages = processScanResult(scanningResult);
156
- JSObject response = new JSObject();
157
- response.put("status", "success");
158
- response.put("scannedImages", new JSArray(scannedImages));
159
- call.resolve(response);
160
- } catch (IOException ioException) {
161
- call.reject("Failed to process scanned images: " + ioException.getLocalizedMessage(), ioException);
162
- } finally {
163
- releasePendingCall(call);
164
- }
165
- }
166
-
167
- private List<String> processScanResult(GmsDocumentScanningResult scanningResult) throws IOException {
168
- List<String> results = new ArrayList<>();
169
- List<Page> pages = scanningResult.getPages();
170
- if (pages == null || pages.isEmpty()) {
171
- return results;
172
- }
173
-
174
- for (int index = 0; index < pages.size(); index++) {
175
- String processed = handlePage(pages.get(index), index);
176
- if (processed != null) {
177
- results.add(processed);
178
- }
179
- }
180
- return results;
181
- }
182
-
183
- private String handlePage(Page page, int pageIndex) throws IOException {
184
- PendingScan scan = pendingScan;
185
- if (scan == null) {
186
- throw new IOException("No active scan.");
187
- }
188
-
189
- Uri imageUri = page.getImageUri();
190
- if (imageUri == null) {
191
- throw new IOException("Missing image URI for scanned page.");
192
- }
193
-
194
- byte[] imageBytes = readBytesFromUri(imageUri);
195
- if (scan.quality < 100) {
196
- imageBytes = reencodeImage(imageBytes, scan.quality);
197
- }
198
-
199
- if (RESPONSE_TYPE_BASE64.equals(scan.responseType)) {
200
- return Base64.encodeToString(imageBytes, Base64.NO_WRAP);
201
- }
202
-
203
- return writeImageFile(imageBytes, pageIndex);
204
- }
205
-
206
- private byte[] readBytesFromUri(Uri uri) throws IOException {
207
- Context context = getContext();
208
- if (context == null) {
209
- throw new IOException("Context unavailable for reading image.");
210
- }
211
-
212
- try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) {
213
- if (inputStream == null) {
214
- throw new IOException("Unable to open image stream.");
215
- }
216
- ByteArrayOutputStream buffer = new ByteArrayOutputStream();
217
- byte[] data = new byte[8192];
218
- int nRead;
219
- while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
220
- buffer.write(data, 0, nRead);
221
- }
222
- return buffer.toByteArray();
223
- }
224
- }
225
-
226
- private byte[] reencodeImage(byte[] source, int quality) throws IOException {
227
- Bitmap bitmap = BitmapFactory.decodeByteArray(source, 0, source.length);
228
- if (bitmap == null) {
229
- throw new IOException("Unable to decode scanned image.");
230
- }
231
- try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
232
- if (!bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)) {
233
- throw new IOException("Unable to compress scanned image.");
234
- }
235
- return outputStream.toByteArray();
236
- } finally {
237
- bitmap.recycle();
238
- }
239
- }
240
-
241
- private String writeImageFile(byte[] imageBytes, int pageIndex) throws IOException {
242
- Context context = getContext();
243
- if (context == null) {
244
- throw new IOException("Context unavailable for writing image.");
245
- }
246
-
247
- File directory = new File(context.getCacheDir(), "document_scanner");
248
- if (!directory.exists() && !directory.mkdirs()) {
249
- throw new IOException("Unable to create cache directory.");
250
- }
251
-
252
- String fileName = String.format(Locale.US, "DOCUMENT_SCAN_%d_%d.jpg", pageIndex, System.currentTimeMillis());
253
- File outputFile = new File(directory, fileName);
254
- try (FileOutputStream outputStream = new FileOutputStream(outputFile)) {
255
- outputStream.write(imageBytes);
256
- }
257
-
258
- return outputFile.getAbsolutePath();
259
- }
260
-
261
- private PluginCall getPendingCall() {
262
- if (pendingScan == null) {
263
- return null;
264
- }
265
- return bridge.getSavedCall(pendingScan.callId);
266
- }
267
-
268
- private void releasePendingCall(PluginCall call) {
269
- if (pendingScan != null) {
270
- bridge.releaseCall(call);
271
- pendingScan = null;
272
- }
273
- }
274
-
275
- private int clamp(Integer value, int min, int max) {
276
- if (value == null) {
277
- return max;
278
- }
279
- return Math.max(min, Math.min(max, value));
280
- }
281
-
282
- private String normalizeResponseType(String value) {
283
- if (value == null) {
284
- return RESPONSE_TYPE_FILE_PATH;
285
- }
286
- String normalized = value.toLowerCase(Locale.ROOT);
287
- if (RESPONSE_TYPE_BASE64.equals(normalized) || RESPONSE_TYPE_FILE_PATH.equals(normalized)) {
288
- return normalized;
289
- }
290
- return RESPONSE_TYPE_FILE_PATH;
291
- }
292
- }