@capgo/capacitor-document-scanner 7.0.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,17 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'CapgoCapacitorDocumentScanner'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = package['license']
10
+ s.homepage = package['repository']['url']
11
+ s.author = package['author']
12
+ s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
13
+ s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
14
+ s.ios.deployment_target = '14.0'
15
+ s.dependency 'Capacitor'
16
+ s.swift_version = '5.1'
17
+ end
package/Package.swift ADDED
@@ -0,0 +1,28 @@
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "CapgoCapacitorDocumentScanner",
6
+ platforms: [.iOS(.v14)],
7
+ products: [
8
+ .library(
9
+ name: "CapgoCapacitorDocumentScanner",
10
+ targets: ["DocumentScannerPlugin"])
11
+ ],
12
+ dependencies: [
13
+ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.0.0")
14
+ ],
15
+ targets: [
16
+ .target(
17
+ name: "DocumentScannerPlugin",
18
+ dependencies: [
19
+ .product(name: "Capacitor", package: "capacitor-swift-pm"),
20
+ .product(name: "Cordova", package: "capacitor-swift-pm")
21
+ ],
22
+ path: "ios/Sources/DocumentScannerPlugin"),
23
+ .testTarget(
24
+ name: "DocumentScannerPluginTests",
25
+ dependencies: ["DocumentScannerPlugin"],
26
+ path: "ios/Tests/DocumentScannerPluginTests")
27
+ ]
28
+ )
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # @capgo/capacitor-document-scanner
2
+ <a href="https://capgo.app/"><img src='https://raw.githubusercontent.com/Cap-go/capgo/main/assets/capgo_banner.png' alt='Capgo - Instant updates for capacitor'/></a>
3
+
4
+ <div align="center">
5
+ <h2><a href="https://capgo.app/?ref=plugin"> ➡️ Get Instant updates for your App with Capgo</a></h2>
6
+ <h2><a href="https://capgo.app/consulting/?ref=plugin"> Missing a feature? We’ll build the plugin for you 💪</a></h2>
7
+ </div>
8
+ Capacitor plugin to scan document iOS and Android
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @capgo/capacitor-document-scanner
14
+ npx cap sync
15
+ ```
16
+
17
+ ## API
18
+
19
+ <docgen-index>
20
+
21
+ * [`scanDocument(...)`](#scandocument)
22
+ * [Interfaces](#interfaces)
23
+ * [Enums](#enums)
24
+
25
+ </docgen-index>
26
+
27
+ <docgen-api>
28
+ <!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
29
+
30
+ ### scanDocument(...)
31
+
32
+ ```typescript
33
+ scanDocument(options?: ScanDocumentOptions | undefined) => Promise<ScanDocumentResponse>
34
+ ```
35
+
36
+ Opens the device camera and starts the document scanning experience.
37
+
38
+ | Param | Type |
39
+ | ------------- | ------------------------------------------------------------------- |
40
+ | **`options`** | <code><a href="#scandocumentoptions">ScanDocumentOptions</a></code> |
41
+
42
+ **Returns:** <code>Promise&lt;<a href="#scandocumentresponse">ScanDocumentResponse</a>&gt;</code>
43
+
44
+ --------------------
45
+
46
+
47
+ ### Interfaces
48
+
49
+
50
+ #### ScanDocumentResponse
51
+
52
+ | Prop | Type | Description |
53
+ | ------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------ |
54
+ | **`scannedImages`** | <code>string[]</code> | Scanned images in the requested response format. |
55
+ | **`status`** | <code><a href="#scandocumentresponsestatus">ScanDocumentResponseStatus</a></code> | Indicates whether the scan completed or was cancelled. |
56
+
57
+
58
+ #### ScanDocumentOptions
59
+
60
+ | Prop | Type | Description | Default |
61
+ | ------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
62
+ | **`croppedImageQuality`** | <code>number</code> | Android only: quality of the cropped image from 0 - 100 (100 is best). | <code>100</code> |
63
+ | **`letUserAdjustCrop`** | <code>boolean</code> | Android only: allow the user to adjust the detected crop before saving. Disabling this forces single-document capture. | <code>true</code> |
64
+ | **`maxNumDocuments`** | <code>number</code> | Android only: maximum number of documents the user can scan. | <code>24</code> |
65
+ | **`responseType`** | <code><a href="#responsetype">ResponseType</a></code> | Format to return scanned images in (file paths or base64 strings). | <code>ResponseType.ImageFilePath</code> |
66
+
67
+
68
+ ### Enums
69
+
70
+
71
+ #### ScanDocumentResponseStatus
72
+
73
+ | Members | Value | Description |
74
+ | ------------- | ---------------------- | --------------------------------- |
75
+ | **`Success`** | <code>'success'</code> | The scan completed successfully. |
76
+ | **`Cancel`** | <code>'cancel'</code> | The user cancelled the scan flow. |
77
+
78
+
79
+ #### ResponseType
80
+
81
+ | Members | Value | Description |
82
+ | ------------------- | ---------------------------- | ------------------------------------------------ |
83
+ | **`Base64`** | <code>'base64'</code> | Return scanned images as base64-encoded strings. |
84
+ | **`ImageFilePath`** | <code>'imageFilePath'</code> | Return scanned images as file paths on disk. |
85
+
86
+ </docgen-api>
@@ -0,0 +1,59 @@
1
+ ext {
2
+ junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
3
+ androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
4
+ androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1'
5
+ androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
6
+ }
7
+
8
+ buildscript {
9
+ repositories {
10
+ google()
11
+ mavenCentral()
12
+ }
13
+ dependencies {
14
+ classpath 'com.android.tools.build:gradle:8.7.2'
15
+ }
16
+ }
17
+
18
+ apply plugin: 'com.android.library'
19
+
20
+ android {
21
+ namespace "app.capgo.plugin.documentscanner"
22
+ compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
23
+ defaultConfig {
24
+ minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
25
+ targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
26
+ versionCode 1
27
+ versionName "1.0"
28
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
29
+ }
30
+ buildTypes {
31
+ release {
32
+ minifyEnabled false
33
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
34
+ }
35
+ }
36
+ lintOptions {
37
+ abortOnError false
38
+ }
39
+ compileOptions {
40
+ sourceCompatibility JavaVersion.VERSION_21
41
+ targetCompatibility JavaVersion.VERSION_21
42
+ }
43
+ }
44
+
45
+ repositories {
46
+ google()
47
+ mavenCentral()
48
+ }
49
+
50
+
51
+ dependencies {
52
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
53
+ implementation project(':capacitor-android')
54
+ implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
55
+ implementation "com.google.android.gms:play-services-mlkit-document-scanner:16.0.0"
56
+ testImplementation "junit:junit:$junitVersion"
57
+ androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
58
+ androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
59
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,11 @@
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
+ }
@@ -0,0 +1,22 @@
1
+ package app.capgo.plugin.document_scanner;
2
+
3
+ import com.getcapacitor.JSObject;
4
+ import com.getcapacitor.Plugin;
5
+ import com.getcapacitor.PluginCall;
6
+ import com.getcapacitor.PluginMethod;
7
+ import com.getcapacitor.annotation.CapacitorPlugin;
8
+
9
+ @CapacitorPlugin(name = "DocumentScanner")
10
+ public class DocumentScannerPlugin extends Plugin {
11
+
12
+ private DocumentScanner implementation = new DocumentScanner();
13
+
14
+ @PluginMethod
15
+ public void echo(PluginCall call) {
16
+ String value = call.getString("value");
17
+
18
+ JSObject ret = new JSObject();
19
+ ret.put("value", implementation.echo(value));
20
+ call.resolve(ret);
21
+ }
22
+ }
@@ -0,0 +1,292 @@
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
+ }
File without changes
package/dist/docs.json ADDED
@@ -0,0 +1,156 @@
1
+ {
2
+ "api": {
3
+ "name": "DocumentScannerPlugin",
4
+ "slug": "documentscannerplugin",
5
+ "docs": "",
6
+ "tags": [],
7
+ "methods": [
8
+ {
9
+ "name": "scanDocument",
10
+ "signature": "(options?: ScanDocumentOptions | undefined) => Promise<ScanDocumentResponse>",
11
+ "parameters": [
12
+ {
13
+ "name": "options",
14
+ "docs": "",
15
+ "type": "ScanDocumentOptions | undefined"
16
+ }
17
+ ],
18
+ "returns": "Promise<ScanDocumentResponse>",
19
+ "tags": [],
20
+ "docs": "Opens the device camera and starts the document scanning experience.",
21
+ "complexTypes": [
22
+ "ScanDocumentResponse",
23
+ "ScanDocumentOptions"
24
+ ],
25
+ "slug": "scandocument"
26
+ }
27
+ ],
28
+ "properties": []
29
+ },
30
+ "interfaces": [
31
+ {
32
+ "name": "ScanDocumentResponse",
33
+ "slug": "scandocumentresponse",
34
+ "docs": "",
35
+ "tags": [],
36
+ "methods": [],
37
+ "properties": [
38
+ {
39
+ "name": "scannedImages",
40
+ "tags": [],
41
+ "docs": "Scanned images in the requested response format.",
42
+ "complexTypes": [],
43
+ "type": "string[] | undefined"
44
+ },
45
+ {
46
+ "name": "status",
47
+ "tags": [],
48
+ "docs": "Indicates whether the scan completed or was cancelled.",
49
+ "complexTypes": [
50
+ "ScanDocumentResponseStatus"
51
+ ],
52
+ "type": "ScanDocumentResponseStatus"
53
+ }
54
+ ]
55
+ },
56
+ {
57
+ "name": "ScanDocumentOptions",
58
+ "slug": "scandocumentoptions",
59
+ "docs": "",
60
+ "tags": [],
61
+ "methods": [],
62
+ "properties": [
63
+ {
64
+ "name": "croppedImageQuality",
65
+ "tags": [
66
+ {
67
+ "text": "100",
68
+ "name": "default"
69
+ }
70
+ ],
71
+ "docs": "Android only: quality of the cropped image from 0 - 100 (100 is best).",
72
+ "complexTypes": [],
73
+ "type": "number | undefined"
74
+ },
75
+ {
76
+ "name": "letUserAdjustCrop",
77
+ "tags": [
78
+ {
79
+ "text": "true",
80
+ "name": "default"
81
+ }
82
+ ],
83
+ "docs": "Android only: allow the user to adjust the detected crop before saving.\nDisabling this forces single-document capture.",
84
+ "complexTypes": [],
85
+ "type": "boolean | undefined"
86
+ },
87
+ {
88
+ "name": "maxNumDocuments",
89
+ "tags": [
90
+ {
91
+ "text": "24",
92
+ "name": "default"
93
+ }
94
+ ],
95
+ "docs": "Android only: maximum number of documents the user can scan.",
96
+ "complexTypes": [],
97
+ "type": "number | undefined"
98
+ },
99
+ {
100
+ "name": "responseType",
101
+ "tags": [
102
+ {
103
+ "text": "ResponseType.ImageFilePath",
104
+ "name": "default"
105
+ }
106
+ ],
107
+ "docs": "Format to return scanned images in (file paths or base64 strings).",
108
+ "complexTypes": [
109
+ "ResponseType"
110
+ ],
111
+ "type": "ResponseType"
112
+ }
113
+ ]
114
+ }
115
+ ],
116
+ "enums": [
117
+ {
118
+ "name": "ScanDocumentResponseStatus",
119
+ "slug": "scandocumentresponsestatus",
120
+ "members": [
121
+ {
122
+ "name": "Success",
123
+ "value": "'success'",
124
+ "tags": [],
125
+ "docs": "The scan completed successfully."
126
+ },
127
+ {
128
+ "name": "Cancel",
129
+ "value": "'cancel'",
130
+ "tags": [],
131
+ "docs": "The user cancelled the scan flow."
132
+ }
133
+ ]
134
+ },
135
+ {
136
+ "name": "ResponseType",
137
+ "slug": "responsetype",
138
+ "members": [
139
+ {
140
+ "name": "Base64",
141
+ "value": "'base64'",
142
+ "tags": [],
143
+ "docs": "Return scanned images as base64-encoded strings."
144
+ },
145
+ {
146
+ "name": "ImageFilePath",
147
+ "value": "'imageFilePath'",
148
+ "tags": [],
149
+ "docs": "Return scanned images as file paths on disk."
150
+ }
151
+ ]
152
+ }
153
+ ],
154
+ "typeAliases": [],
155
+ "pluginConfigs": []
156
+ }
@@ -0,0 +1,59 @@
1
+ export interface DocumentScannerPlugin {
2
+ /**
3
+ * Opens the device camera and starts the document scanning experience.
4
+ */
5
+ scanDocument(options?: ScanDocumentOptions): Promise<ScanDocumentResponse>;
6
+ }
7
+ export interface ScanDocumentOptions {
8
+ /**
9
+ * Android only: quality of the cropped image from 0 - 100 (100 is best).
10
+ * @default 100
11
+ */
12
+ croppedImageQuality?: number;
13
+ /**
14
+ * Android only: allow the user to adjust the detected crop before saving.
15
+ * Disabling this forces single-document capture.
16
+ * @default true
17
+ */
18
+ letUserAdjustCrop?: boolean;
19
+ /**
20
+ * Android only: maximum number of documents the user can scan.
21
+ * @default 24
22
+ */
23
+ maxNumDocuments?: number;
24
+ /**
25
+ * Format to return scanned images in (file paths or base64 strings).
26
+ * @default ResponseType.ImageFilePath
27
+ */
28
+ responseType?: ResponseType;
29
+ }
30
+ export declare enum ResponseType {
31
+ /**
32
+ * Return scanned images as base64-encoded strings.
33
+ */
34
+ Base64 = "base64",
35
+ /**
36
+ * Return scanned images as file paths on disk.
37
+ */
38
+ ImageFilePath = "imageFilePath"
39
+ }
40
+ export interface ScanDocumentResponse {
41
+ /**
42
+ * Scanned images in the requested response format.
43
+ */
44
+ scannedImages?: string[];
45
+ /**
46
+ * Indicates whether the scan completed or was cancelled.
47
+ */
48
+ status?: ScanDocumentResponseStatus;
49
+ }
50
+ export declare enum ScanDocumentResponseStatus {
51
+ /**
52
+ * The scan completed successfully.
53
+ */
54
+ Success = "success",
55
+ /**
56
+ * The user cancelled the scan flow.
57
+ */
58
+ Cancel = "cancel"
59
+ }
@@ -0,0 +1,23 @@
1
+ export var ResponseType;
2
+ (function (ResponseType) {
3
+ /**
4
+ * Return scanned images as base64-encoded strings.
5
+ */
6
+ ResponseType["Base64"] = "base64";
7
+ /**
8
+ * Return scanned images as file paths on disk.
9
+ */
10
+ ResponseType["ImageFilePath"] = "imageFilePath";
11
+ })(ResponseType || (ResponseType = {}));
12
+ export var ScanDocumentResponseStatus;
13
+ (function (ScanDocumentResponseStatus) {
14
+ /**
15
+ * The scan completed successfully.
16
+ */
17
+ ScanDocumentResponseStatus["Success"] = "success";
18
+ /**
19
+ * The user cancelled the scan flow.
20
+ */
21
+ ScanDocumentResponseStatus["Cancel"] = "cancel";
22
+ })(ScanDocumentResponseStatus || (ScanDocumentResponseStatus = {}));
23
+ //# sourceMappingURL=definitions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAkCA,MAAM,CAAN,IAAY,YAUX;AAVD,WAAY,YAAY;IACtB;;OAEG;IACH,iCAAiB,CAAA;IAEjB;;OAEG;IACH,+CAA+B,CAAA;AACjC,CAAC,EAVW,YAAY,KAAZ,YAAY,QAUvB;AAcD,MAAM,CAAN,IAAY,0BAUX;AAVD,WAAY,0BAA0B;IACpC;;OAEG;IACH,iDAAmB,CAAA;IAEnB;;OAEG;IACH,+CAAiB,CAAA;AACnB,CAAC,EAVW,0BAA0B,KAA1B,0BAA0B,QAUrC","sourcesContent":["export interface DocumentScannerPlugin {\n /**\n * Opens the device camera and starts the document scanning experience.\n */\n scanDocument(options?: ScanDocumentOptions): Promise<ScanDocumentResponse>;\n}\n\nexport interface ScanDocumentOptions {\n /**\n * Android only: quality of the cropped image from 0 - 100 (100 is best).\n * @default 100\n */\n croppedImageQuality?: number;\n\n /**\n * Android only: allow the user to adjust the detected crop before saving.\n * Disabling this forces single-document capture.\n * @default true\n */\n letUserAdjustCrop?: boolean;\n\n /**\n * Android only: maximum number of documents the user can scan.\n * @default 24\n */\n maxNumDocuments?: number;\n\n /**\n * Format to return scanned images in (file paths or base64 strings).\n * @default ResponseType.ImageFilePath\n */\n responseType?: ResponseType;\n}\n\nexport enum ResponseType {\n /**\n * Return scanned images as base64-encoded strings.\n */\n Base64 = 'base64',\n\n /**\n * Return scanned images as file paths on disk.\n */\n ImageFilePath = 'imageFilePath',\n}\n\nexport interface ScanDocumentResponse {\n /**\n * Scanned images in the requested response format.\n */\n scannedImages?: string[];\n\n /**\n * Indicates whether the scan completed or was cancelled.\n */\n status?: ScanDocumentResponseStatus;\n}\n\nexport enum ScanDocumentResponseStatus {\n /**\n * The scan completed successfully.\n */\n Success = 'success',\n\n /**\n * The user cancelled the scan flow.\n */\n Cancel = 'cancel',\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import type { DocumentScannerPlugin } from './definitions';
2
+ declare const DocumentScanner: DocumentScannerPlugin;
3
+ export * from './definitions';
4
+ export { DocumentScanner };
@@ -0,0 +1,7 @@
1
+ import { registerPlugin } from '@capacitor/core';
2
+ const DocumentScanner = registerPlugin('DocumentScanner', {
3
+ web: () => import('./web').then((m) => new m.DocumentScannerWeb()),
4
+ });
5
+ export * from './definitions';
6
+ export { DocumentScanner };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,eAAe,GAAG,cAAc,CAAwB,iBAAiB,EAAE;IAC/E,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,kBAAkB,EAAE,CAAC;CACnE,CAAC,CAAC;AAEH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,CAAC","sourcesContent":["import { registerPlugin } from '@capacitor/core';\n\nimport type { DocumentScannerPlugin } from './definitions';\n\nconst DocumentScanner = registerPlugin<DocumentScannerPlugin>('DocumentScanner', {\n web: () => import('./web').then((m) => new m.DocumentScannerWeb()),\n});\n\nexport * from './definitions';\nexport { DocumentScanner };\n"]}
@@ -0,0 +1,5 @@
1
+ import { WebPlugin } from '@capacitor/core';
2
+ import type { DocumentScannerPlugin, ScanDocumentOptions, ScanDocumentResponse } from './definitions';
3
+ export declare class DocumentScannerWeb extends WebPlugin implements DocumentScannerPlugin {
4
+ scanDocument(_options?: ScanDocumentOptions): Promise<ScanDocumentResponse>;
5
+ }
@@ -0,0 +1,7 @@
1
+ import { WebPlugin } from '@capacitor/core';
2
+ export class DocumentScannerWeb extends WebPlugin {
3
+ async scanDocument(_options) {
4
+ throw this.unimplemented('Document scanning is not supported on the web.');
5
+ }
6
+ }
7
+ //# sourceMappingURL=web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C,MAAM,OAAO,kBAAmB,SAAQ,SAAS;IAC/C,KAAK,CAAC,YAAY,CAAC,QAA8B;QAC/C,MAAM,IAAI,CAAC,aAAa,CAAC,gDAAgD,CAAC,CAAC;IAC7E,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type { DocumentScannerPlugin, ScanDocumentOptions, ScanDocumentResponse } from './definitions';\n\nexport class DocumentScannerWeb extends WebPlugin implements DocumentScannerPlugin {\n async scanDocument(_options?: ScanDocumentOptions): Promise<ScanDocumentResponse> {\n throw this.unimplemented('Document scanning is not supported on the web.');\n }\n}\n"]}
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ var core = require('@capacitor/core');
4
+
5
+ exports.ResponseType = void 0;
6
+ (function (ResponseType) {
7
+ /**
8
+ * Return scanned images as base64-encoded strings.
9
+ */
10
+ ResponseType["Base64"] = "base64";
11
+ /**
12
+ * Return scanned images as file paths on disk.
13
+ */
14
+ ResponseType["ImageFilePath"] = "imageFilePath";
15
+ })(exports.ResponseType || (exports.ResponseType = {}));
16
+ exports.ScanDocumentResponseStatus = void 0;
17
+ (function (ScanDocumentResponseStatus) {
18
+ /**
19
+ * The scan completed successfully.
20
+ */
21
+ ScanDocumentResponseStatus["Success"] = "success";
22
+ /**
23
+ * The user cancelled the scan flow.
24
+ */
25
+ ScanDocumentResponseStatus["Cancel"] = "cancel";
26
+ })(exports.ScanDocumentResponseStatus || (exports.ScanDocumentResponseStatus = {}));
27
+
28
+ const DocumentScanner = core.registerPlugin('DocumentScanner', {
29
+ web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.DocumentScannerWeb()),
30
+ });
31
+
32
+ class DocumentScannerWeb extends core.WebPlugin {
33
+ async scanDocument(_options) {
34
+ throw this.unimplemented('Document scanning is not supported on the web.');
35
+ }
36
+ }
37
+
38
+ var web = /*#__PURE__*/Object.freeze({
39
+ __proto__: null,
40
+ DocumentScannerWeb: DocumentScannerWeb
41
+ });
42
+
43
+ exports.DocumentScanner = DocumentScanner;
44
+ //# sourceMappingURL=plugin.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.cjs.js","sources":["esm/definitions.js","esm/index.js","esm/web.js"],"sourcesContent":["export var ResponseType;\n(function (ResponseType) {\n /**\n * Return scanned images as base64-encoded strings.\n */\n ResponseType[\"Base64\"] = \"base64\";\n /**\n * Return scanned images as file paths on disk.\n */\n ResponseType[\"ImageFilePath\"] = \"imageFilePath\";\n})(ResponseType || (ResponseType = {}));\nexport var ScanDocumentResponseStatus;\n(function (ScanDocumentResponseStatus) {\n /**\n * The scan completed successfully.\n */\n ScanDocumentResponseStatus[\"Success\"] = \"success\";\n /**\n * The user cancelled the scan flow.\n */\n ScanDocumentResponseStatus[\"Cancel\"] = \"cancel\";\n})(ScanDocumentResponseStatus || (ScanDocumentResponseStatus = {}));\n//# sourceMappingURL=definitions.js.map","import { registerPlugin } from '@capacitor/core';\nconst DocumentScanner = registerPlugin('DocumentScanner', {\n web: () => import('./web').then((m) => new m.DocumentScannerWeb()),\n});\nexport * from './definitions';\nexport { DocumentScanner };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class DocumentScannerWeb extends WebPlugin {\n async scanDocument(_options) {\n throw this.unimplemented('Document scanning is not supported on the web.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["ResponseType","ScanDocumentResponseStatus","registerPlugin","WebPlugin"],"mappings":";;;;AAAWA;AACX,CAAC,UAAU,YAAY,EAAE;AACzB;AACA;AACA;AACA,IAAI,YAAY,CAAC,QAAQ,CAAC,GAAG,QAAQ;AACrC;AACA;AACA;AACA,IAAI,YAAY,CAAC,eAAe,CAAC,GAAG,eAAe;AACnD,CAAC,EAAEA,oBAAY,KAAKA,oBAAY,GAAG,EAAE,CAAC,CAAC;AAC5BC;AACX,CAAC,UAAU,0BAA0B,EAAE;AACvC;AACA;AACA;AACA,IAAI,0BAA0B,CAAC,SAAS,CAAC,GAAG,SAAS;AACrD;AACA;AACA;AACA,IAAI,0BAA0B,CAAC,QAAQ,CAAC,GAAG,QAAQ;AACnD,CAAC,EAAEA,kCAA0B,KAAKA,kCAA0B,GAAG,EAAE,CAAC,CAAC;;ACpB9D,MAAC,eAAe,GAAGC,mBAAc,CAAC,iBAAiB,EAAE;AAC1D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,kBAAkB,EAAE,CAAC;AACtE,CAAC;;ACFM,MAAM,kBAAkB,SAASC,cAAS,CAAC;AAClD,IAAI,MAAM,YAAY,CAAC,QAAQ,EAAE;AACjC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,gDAAgD,CAAC;AAClF,IAAI;AACJ;;;;;;;;;"}
package/dist/plugin.js ADDED
@@ -0,0 +1,47 @@
1
+ var capacitorDocumentScanner = (function (exports, core) {
2
+ 'use strict';
3
+
4
+ exports.ResponseType = void 0;
5
+ (function (ResponseType) {
6
+ /**
7
+ * Return scanned images as base64-encoded strings.
8
+ */
9
+ ResponseType["Base64"] = "base64";
10
+ /**
11
+ * Return scanned images as file paths on disk.
12
+ */
13
+ ResponseType["ImageFilePath"] = "imageFilePath";
14
+ })(exports.ResponseType || (exports.ResponseType = {}));
15
+ exports.ScanDocumentResponseStatus = void 0;
16
+ (function (ScanDocumentResponseStatus) {
17
+ /**
18
+ * The scan completed successfully.
19
+ */
20
+ ScanDocumentResponseStatus["Success"] = "success";
21
+ /**
22
+ * The user cancelled the scan flow.
23
+ */
24
+ ScanDocumentResponseStatus["Cancel"] = "cancel";
25
+ })(exports.ScanDocumentResponseStatus || (exports.ScanDocumentResponseStatus = {}));
26
+
27
+ const DocumentScanner = core.registerPlugin('DocumentScanner', {
28
+ web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.DocumentScannerWeb()),
29
+ });
30
+
31
+ class DocumentScannerWeb extends core.WebPlugin {
32
+ async scanDocument(_options) {
33
+ throw this.unimplemented('Document scanning is not supported on the web.');
34
+ }
35
+ }
36
+
37
+ var web = /*#__PURE__*/Object.freeze({
38
+ __proto__: null,
39
+ DocumentScannerWeb: DocumentScannerWeb
40
+ });
41
+
42
+ exports.DocumentScanner = DocumentScanner;
43
+
44
+ return exports;
45
+
46
+ })({}, capacitorExports);
47
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sources":["esm/definitions.js","esm/index.js","esm/web.js"],"sourcesContent":["export var ResponseType;\n(function (ResponseType) {\n /**\n * Return scanned images as base64-encoded strings.\n */\n ResponseType[\"Base64\"] = \"base64\";\n /**\n * Return scanned images as file paths on disk.\n */\n ResponseType[\"ImageFilePath\"] = \"imageFilePath\";\n})(ResponseType || (ResponseType = {}));\nexport var ScanDocumentResponseStatus;\n(function (ScanDocumentResponseStatus) {\n /**\n * The scan completed successfully.\n */\n ScanDocumentResponseStatus[\"Success\"] = \"success\";\n /**\n * The user cancelled the scan flow.\n */\n ScanDocumentResponseStatus[\"Cancel\"] = \"cancel\";\n})(ScanDocumentResponseStatus || (ScanDocumentResponseStatus = {}));\n//# sourceMappingURL=definitions.js.map","import { registerPlugin } from '@capacitor/core';\nconst DocumentScanner = registerPlugin('DocumentScanner', {\n web: () => import('./web').then((m) => new m.DocumentScannerWeb()),\n});\nexport * from './definitions';\nexport { DocumentScanner };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class DocumentScannerWeb extends WebPlugin {\n async scanDocument(_options) {\n throw this.unimplemented('Document scanning is not supported on the web.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["ResponseType","ScanDocumentResponseStatus","registerPlugin","WebPlugin"],"mappings":";;;AAAWA;IACX,CAAC,UAAU,YAAY,EAAE;IACzB;IACA;IACA;IACA,IAAI,YAAY,CAAC,QAAQ,CAAC,GAAG,QAAQ;IACrC;IACA;IACA;IACA,IAAI,YAAY,CAAC,eAAe,CAAC,GAAG,eAAe;IACnD,CAAC,EAAEA,oBAAY,KAAKA,oBAAY,GAAG,EAAE,CAAC,CAAC;AAC5BC;IACX,CAAC,UAAU,0BAA0B,EAAE;IACvC;IACA;IACA;IACA,IAAI,0BAA0B,CAAC,SAAS,CAAC,GAAG,SAAS;IACrD;IACA;IACA;IACA,IAAI,0BAA0B,CAAC,QAAQ,CAAC,GAAG,QAAQ;IACnD,CAAC,EAAEA,kCAA0B,KAAKA,kCAA0B,GAAG,EAAE,CAAC,CAAC;;ACpB9D,UAAC,eAAe,GAAGC,mBAAc,CAAC,iBAAiB,EAAE;IAC1D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,kBAAkB,EAAE,CAAC;IACtE,CAAC;;ICFM,MAAM,kBAAkB,SAASC,cAAS,CAAC;IAClD,IAAI,MAAM,YAAY,CAAC,QAAQ,EAAE;IACjC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,gDAAgD,CAAC;IAClF,IAAI;IACJ;;;;;;;;;;;;;;;"}
@@ -0,0 +1,126 @@
1
+ import UIKit
2
+ import VisionKit
3
+
4
+ /**
5
+ Handles presenting the VisionKit document scanner and returning results.
6
+ */
7
+ @available(iOS 13.0, *)
8
+ class DocScanner: NSObject, VNDocumentCameraViewControllerDelegate {
9
+ private weak var viewController: UIViewController?
10
+ private var successHandler: ([String]) -> Void
11
+ private var errorHandler: (String) -> Void
12
+ private var cancelHandler: () -> Void
13
+ private var responseType: String
14
+ private var croppedImageQuality: Int
15
+
16
+ init(
17
+ _ viewController: UIViewController? = nil,
18
+ successHandler: @escaping ([String]) -> Void = { _ in },
19
+ errorHandler: @escaping (String) -> Void = { _ in },
20
+ cancelHandler: @escaping () -> Void = {},
21
+ responseType: String = ResponseType.imageFilePath,
22
+ croppedImageQuality: Int = 100
23
+ ) {
24
+ self.viewController = viewController
25
+ self.successHandler = successHandler
26
+ self.errorHandler = errorHandler
27
+ self.cancelHandler = cancelHandler
28
+ self.responseType = responseType
29
+ self.croppedImageQuality = croppedImageQuality
30
+ }
31
+
32
+ override convenience init() {
33
+ self.init(nil)
34
+ }
35
+
36
+ func startScan() {
37
+ guard VNDocumentCameraViewController.isSupported else {
38
+ errorHandler("Document scanning is not supported on this device.")
39
+ return
40
+ }
41
+
42
+ DispatchQueue.main.async {
43
+ let documentCameraViewController = VNDocumentCameraViewController()
44
+ documentCameraViewController.delegate = self
45
+ self.viewController?.present(documentCameraViewController, animated: true)
46
+ }
47
+ }
48
+
49
+ func startScan(
50
+ _ viewController: UIViewController? = nil,
51
+ successHandler: @escaping ([String]) -> Void = { _ in },
52
+ errorHandler: @escaping (String) -> Void = { _ in },
53
+ cancelHandler: @escaping () -> Void = {},
54
+ responseType: String? = ResponseType.imageFilePath,
55
+ croppedImageQuality: Int? = 100
56
+ ) {
57
+ self.viewController = viewController
58
+ self.successHandler = successHandler
59
+ self.errorHandler = errorHandler
60
+ self.cancelHandler = cancelHandler
61
+ self.responseType = responseType ?? ResponseType.imageFilePath
62
+ self.croppedImageQuality = croppedImageQuality ?? 100
63
+
64
+ startScan()
65
+ }
66
+
67
+ func documentCameraViewController(
68
+ _ controller: VNDocumentCameraViewController,
69
+ didFinishWith scan: VNDocumentCameraScan
70
+ ) {
71
+ var results: [String] = []
72
+
73
+ for pageNumber in 0 ..< scan.pageCount {
74
+ guard
75
+ let scannedImageData = scan.imageOfPage(at: pageNumber)
76
+ .jpegData(compressionQuality: CGFloat(croppedImageQuality) / CGFloat(100))
77
+ else {
78
+ goBackToPreviousView(controller)
79
+ errorHandler("Unable to get scanned document in jpeg format.")
80
+ return
81
+ }
82
+
83
+ switch responseType {
84
+ case ResponseType.base64:
85
+ results.append(scannedImageData.base64EncodedString())
86
+ case ResponseType.imageFilePath:
87
+ do {
88
+ let imagePath = FileUtil().createImageFile(pageNumber)
89
+ try scannedImageData.write(to: imagePath)
90
+ results.append(imagePath.absoluteString)
91
+ } catch {
92
+ goBackToPreviousView(controller)
93
+ errorHandler("Unable to save scanned image: \(error.localizedDescription)")
94
+ return
95
+ }
96
+ default:
97
+ errorHandler(
98
+ "responseType must be \(ResponseType.base64) or \(ResponseType.imageFilePath)"
99
+ )
100
+ return
101
+ }
102
+ }
103
+
104
+ goBackToPreviousView(controller)
105
+ successHandler(results)
106
+ }
107
+
108
+ func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
109
+ goBackToPreviousView(controller)
110
+ cancelHandler()
111
+ }
112
+
113
+ func documentCameraViewController(
114
+ _ controller: VNDocumentCameraViewController,
115
+ didFailWithError error: Error
116
+ ) {
117
+ goBackToPreviousView(controller)
118
+ errorHandler(error.localizedDescription)
119
+ }
120
+
121
+ private func goBackToPreviousView(_ controller: VNDocumentCameraViewController) {
122
+ DispatchQueue.main.async {
123
+ controller.dismiss(animated: true)
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,51 @@
1
+ import Capacitor
2
+ import Foundation
3
+
4
+ @available(iOS 13.0, *)
5
+ @objc(DocumentScannerPlugin)
6
+ public class DocumentScannerPlugin: CAPPlugin, CAPBridgedPlugin {
7
+ public let identifier = "DocumentScannerPlugin"
8
+ public let jsName = "DocumentScanner"
9
+ public let pluginMethods: [CAPPluginMethod] = [
10
+ CAPPluginMethod(name: "scanDocument", returnType: CAPPluginReturnPromise)
11
+ ]
12
+
13
+ private var documentScanner: DocScanner?
14
+
15
+ @objc func scanDocument(_ call: CAPPluginCall) {
16
+ guard let bridgeViewController = bridge?.viewController else {
17
+ call.reject("Bridge view controller unavailable.")
18
+ return
19
+ }
20
+
21
+ documentScanner = DocScanner(
22
+ bridgeViewController,
23
+ successHandler: { [weak self] scannedImages in
24
+ call.resolve([
25
+ "status": "success",
26
+ "scannedImages": scannedImages
27
+ ])
28
+ self?.documentScanner = nil
29
+ },
30
+ errorHandler: { [weak self] errorMessage in
31
+ call.reject(errorMessage)
32
+ self?.documentScanner = nil
33
+ },
34
+ cancelHandler: { [weak self] in
35
+ call.resolve([
36
+ "status": "cancel"
37
+ ])
38
+ self?.documentScanner = nil
39
+ },
40
+ responseType: call.getString("responseType") ?? ResponseType.imageFilePath,
41
+ croppedImageQuality: clampQuality(call.getInt("croppedImageQuality"))
42
+ )
43
+
44
+ documentScanner?.startScan()
45
+ }
46
+
47
+ private func clampQuality(_ value: Int?) -> Int {
48
+ let quality = value ?? 100
49
+ return max(0, min(100, quality))
50
+ }
51
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ Allows throwing runtime errors with a custom message.
3
+ */
4
+ enum RuntimeError: Error {
5
+ case message(String)
6
+ }
@@ -0,0 +1,42 @@
1
+ import Foundation
2
+
3
+ /**
4
+ Utilities for creating and managing scanned image files.
5
+ */
6
+ class FileUtil {
7
+ func createImageFile(_ pageNumber: Int) -> URL {
8
+ let documentsDirectory = FileManager.default.urls(
9
+ for: .documentDirectory,
10
+ in: .userDomainMask
11
+ )[0]
12
+
13
+ return documentsDirectory.appendingPathComponent(
14
+ "DOCUMENT_SCAN_\(pageNumber)_\(currentTimestamp()).jpg"
15
+ )
16
+ }
17
+
18
+ func getBase64Image(imageFilePath: String) throws -> String {
19
+ let imageUrl = try imageURL(imageFilePath)
20
+ guard let imageData = try? Data(contentsOf: imageUrl) else {
21
+ throw RuntimeError.message("Unable to get image from file")
22
+ }
23
+ return imageData.base64EncodedString()
24
+ }
25
+
26
+ func deleteImage(imageFilePath: String) throws {
27
+ try FileManager.default.removeItem(at: imageURL(imageFilePath))
28
+ }
29
+
30
+ private func imageURL(_ imageFilePath: String) throws -> URL {
31
+ guard let imageUrl = URL(string: imageFilePath) else {
32
+ throw RuntimeError.message("Unable to get image from file")
33
+ }
34
+ return imageUrl
35
+ }
36
+
37
+ private func currentTimestamp() -> String {
38
+ let dateFormatter = DateFormatter()
39
+ dateFormatter.dateFormat = "yyyyMMdd_HHmmss"
40
+ return dateFormatter.string(from: Date())
41
+ }
42
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ Enumerates the supported response types for scanned images.
3
+ */
4
+ enum ResponseType {
5
+ static let base64 = "base64"
6
+ static let imageFilePath = "imageFilePath"
7
+ }
@@ -0,0 +1,9 @@
1
+ import XCTest
2
+ @testable import DocumentScannerPlugin
3
+
4
+ final class DocumentScannerTests: XCTestCase {
5
+ func testResponseTypeValues() {
6
+ XCTAssertEqual(ResponseType.base64, "base64")
7
+ XCTAssertEqual(ResponseType.imageFilePath, "imageFilePath")
8
+ }
9
+ }
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@capgo/capacitor-document-scanner",
3
+ "version": "7.0.0",
4
+ "description": "Capacitor plugin to scan document iOS and Android",
5
+ "main": "dist/plugin.cjs.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "unpkg": "dist/plugin.js",
9
+ "files": [
10
+ "android/src/main/",
11
+ "android/build.gradle",
12
+ "dist/",
13
+ "ios/Sources",
14
+ "ios/Tests",
15
+ "Package.swift",
16
+ "CapgoCapacitorDocumentScanner.podspec"
17
+ ],
18
+ "author": "Martin Donadieu <martin@capgo.app>",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/Cap-go/capacitor-document-scanner.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/Cap-go/capacitor-document-scanner/issues"
26
+ },
27
+ "keywords": [
28
+ "capacitor",
29
+ "plugin",
30
+ "native"
31
+ ],
32
+ "scripts": {
33
+ "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
34
+ "verify:ios": "xcodebuild -scheme CapgoCapacitorDocumentScanner -destination generic/platform=iOS",
35
+ "verify:android": "cd android && ./gradlew clean build test && cd ..",
36
+ "verify:web": "npm run build",
37
+ "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
38
+ "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
39
+ "eslint": "eslint . --ext ts",
40
+ "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
41
+ "swiftlint": "node-swiftlint",
42
+ "docgen": "docgen --api DocumentScannerPlugin --output-readme README.md --output-json dist/docs.json",
43
+ "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
44
+ "clean": "rimraf ./dist",
45
+ "watch": "tsc --watch",
46
+ "prepublishOnly": "npm run build"
47
+ },
48
+ "devDependencies": {
49
+ "@capacitor/android": "^7.0.0",
50
+ "@capacitor/core": "^7.0.0",
51
+ "@capacitor/docgen": "^0.3.0",
52
+ "@capacitor/ios": "^7.0.0",
53
+ "@ionic/eslint-config": "^0.4.0",
54
+ "@ionic/prettier-config": "^4.0.0",
55
+ "@ionic/swiftlint-config": "^2.0.0",
56
+ "eslint": "^8.57.0",
57
+ "prettier": "^3.4.2",
58
+ "prettier-plugin-java": "^2.6.6",
59
+ "rimraf": "^6.0.1",
60
+ "rollup": "^4.30.1",
61
+ "swiftlint": "^2.0.0",
62
+ "typescript": "~4.1.5"
63
+ },
64
+ "peerDependencies": {
65
+ "@capacitor/core": ">=7.0.0"
66
+ },
67
+ "prettier": "@ionic/prettier-config",
68
+ "swiftlint": "@ionic/swiftlint-config",
69
+ "eslintConfig": {
70
+ "extends": "@ionic/eslint-config/recommended"
71
+ },
72
+ "capacitor": {
73
+ "ios": {
74
+ "src": "ios"
75
+ },
76
+ "android": {
77
+ "src": "android"
78
+ }
79
+ }
80
+ }