Skip to content

Commit 12ce511

Browse files
authored
switch to native Google document scanner (#113)
1 parent b065fc6 commit 12ce511

6 files changed

Lines changed: 97 additions & 131 deletions

File tree

README.md

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ cd ios && pod install && cd ..
4040

4141
* [Basic Example](#basic-example)
4242
* [Limit Number of Scans](#limit-number-of-scans)
43-
* [Remove Cropper](#remove-cropper)
4443

4544
### Basic Example
4645

@@ -144,50 +143,6 @@ export default () => {
144143

145144
<video src="https://user-images.githubusercontent.com/26162804/161643345-6fe15f33-9414-46f5-b5d5-24d88948e801.mp4" data-canonical-src="https://user-images.githubusercontent.com/26162804/161643345-6fe15f33-9414-46f5-b5d5-24d88948e801.mp4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px;"></video>
146145

147-
### Remove Cropper
148-
149-
You can automatically accept the detected document corners, and prevent the user from
150-
making adjustments. Set letUserAdjustCrop to false to skip the crop screen. This limits
151-
the max number of scans to 1. This only works on Android.
152-
153-
```javascript
154-
import React, { useState, useEffect } from 'react'
155-
import { Image } from 'react-native'
156-
import DocumentScanner from 'react-native-document-scanner-plugin'
157-
158-
export default () => {
159-
const [scannedImage, setScannedImage] = useState();
160-
161-
const scanDocument = async () => {
162-
// start the document scanner
163-
const { scannedImages } = await DocumentScanner.scanDocument({
164-
letUserAdjustCrop: false
165-
})
166-
167-
// get back an array with scanned image file paths
168-
if (scannedImages.length > 0) {
169-
// set the img src, so we can view the first scanned image
170-
setScannedImage(scannedImages[0])
171-
}
172-
}
173-
174-
useEffect(() => {
175-
// call scanDocument on load
176-
scanDocument()
177-
}, []);
178-
179-
return (
180-
<Image
181-
resizeMode="contain"
182-
style={{ width: '100%', height: '100%' }}
183-
source={{ uri: scannedImage }}
184-
/>
185-
)
186-
}
187-
```
188-
189-
<video src="https://user-images.githubusercontent.com/26162804/161643377-cabd7f51-a16f-4f5e-938a-afb6f3b1c8cb.mp4" data-canonical-src="https://user-images.githubusercontent.com/26162804/161643377-cabd7f51-a16f-4f5e-938a-afb6f3b1c8cb.mp4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px;"></video>
190-
191146
## Documentation
192147

193148
* [`scanDocument(...)`](#scandocument)
@@ -227,8 +182,7 @@ Opens the camera, and starts the document scan
227182
| Prop | Type | Description | Default |
228183
| ----------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
229184
| **`croppedImageQuality`** | <code>number</code> | The quality of the cropped image from 0 - 100. 100 is the best quality. | <code>: 100</code> |
230-
| **`letUserAdjustCrop`** | <code>boolean</code> | Android only: If true then once the user takes a photo, they get to preview the automatically detected document corners. They can then move the corners in case there needs to be an adjustment. If false then the user can't adjust the corners, and the user can only take 1 photo (maxNumDocuments can't be more than 1 in this case). | <code>: true</code> |
231-
| **`maxNumDocuments`** | <code>number</code> | Android only: The maximum number of photos an user can take (not counting photo retakes) | <code>: 24</code> |
185+
| **`maxNumDocuments`** | <code>number</code> | Android only: The maximum number of photos an user can take (not counting photo retakes) | <code>: undefined</code> |
232186
| **`responseType`** | <code><a href="#responsetype">ResponseType</a></code> | The response comes back in this format on success. It can be the document scan image file paths or base64 images. | <code>: ResponseType.ImageFilePath</code> |
233187

234188

android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,5 @@ repositories {
5656
dependencies {
5757
//noinspection GradleDynamicVersion
5858
implementation "com.facebook.react:react-native:+" // From node_modules
59-
implementation "com.websitebeaver:documentscanner:1.3.5"
59+
implementation "com.google.android.gms:play-services-mlkit-document-scanner:16.0.0-beta1"
6060
}
Lines changed: 91 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package com.reactnativedocumentscanner;
22

33
import android.app.Activity;
4-
import android.content.Intent;
4+
import android.graphics.Bitmap;
5+
import android.graphics.BitmapFactory;
6+
import android.net.Uri;
7+
import android.util.Base64;
58
import androidx.activity.ComponentActivity;
6-
import androidx.activity.result.ActivityResult;
9+
import androidx.activity.result.ActivityResultLauncher;
10+
import androidx.activity.result.IntentSenderRequest;
11+
import androidx.activity.result.contract.ActivityResultContracts;
712
import androidx.annotation.NonNull;
8-
import com.facebook.react.bridge.ActivityEventListener;
9-
import com.facebook.react.bridge.BaseActivityEventListener;
1013
import com.facebook.react.bridge.Promise;
1114
import com.facebook.react.bridge.ReactApplicationContext;
1215
import com.facebook.react.bridge.ReactContextBaseJavaModule;
@@ -17,37 +20,22 @@
1720
import com.facebook.react.bridge.WritableNativeArray;
1821
import com.facebook.react.bridge.WritableNativeMap;
1922
import com.facebook.react.module.annotations.ReactModule;
20-
import com.websitebeaver.documentscanner.DocumentScanner;
21-
import com.websitebeaver.documentscanner.constants.DocumentScannerExtra;
22-
import java.util.ArrayList;
23+
import com.google.mlkit.vision.documentscanner.GmsDocumentScanner;
24+
import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions;
25+
import com.google.mlkit.vision.documentscanner.GmsDocumentScanning;
26+
import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult;
27+
import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult.Page;
28+
import java.io.ByteArrayOutputStream;
29+
import java.io.FileNotFoundException;
30+
import java.util.List;
31+
import java.util.Objects;
2332

2433
@ReactModule(name = DocumentScannerModule.NAME)
2534
public class DocumentScannerModule extends ReactContextBaseJavaModule {
2635
public static final String NAME = "DocumentScanner";
2736

28-
private static final int DOCUMENT_SCAN_REQUEST = 938;
29-
30-
DocumentScanner documentScanner;
31-
32-
private ActivityEventListener activityEventListener = new BaseActivityEventListener() {
33-
@Override
34-
public void onActivityResult(
35-
final Activity activity,
36-
final int requestCode,
37-
final int resultCode,
38-
final Intent intent) {
39-
// trigger callbacks (success, cancel, error)
40-
if (requestCode == DOCUMENT_SCAN_REQUEST && documentScanner != null) {
41-
documentScanner.handleDocumentScanIntentResult(
42-
new ActivityResult(resultCode, intent)
43-
);
44-
}
45-
}
46-
};
47-
4837
public DocumentScannerModule(ReactApplicationContext reactContext) {
4938
super(reactContext);
50-
reactContext.addActivityEventListener(activityEventListener);
5139
}
5240

5341
@Override
@@ -56,56 +44,89 @@ public String getName() {
5644
return NAME;
5745
}
5846

47+
public String getImageInBase64(Activity currentActivity, Uri croppedImageUri, int quality) throws FileNotFoundException {
48+
Bitmap bitmap = BitmapFactory.decodeStream(
49+
currentActivity.getContentResolver().openInputStream(croppedImageUri)
50+
);
51+
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
52+
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, byteArrayOutputStream);
53+
byte[] byteArray = byteArrayOutputStream.toByteArray();
54+
return Base64.encodeToString(byteArray, Base64.DEFAULT);
55+
}
56+
5957
@ReactMethod
6058
public void scanDocument(ReadableMap options, Promise promise) {
6159
Activity currentActivity = getCurrentActivity();
6260
WritableMap response = new WritableNativeMap();
6361

64-
// create a document scanner
65-
documentScanner = new DocumentScanner(
66-
(ComponentActivity) currentActivity,
67-
(ArrayList<String> documentScanResults) -> {
68-
// document scan success
69-
WritableArray docScanResults = new WritableNativeArray();
70-
documentScanResults.forEach(
71-
documentScanResult -> docScanResults.pushString(documentScanResult)
72-
);
73-
response.putArray(
62+
GmsDocumentScannerOptions.Builder documentScannerOptionsBuilder = new GmsDocumentScannerOptions.Builder()
63+
.setResultFormats(GmsDocumentScannerOptions.RESULT_FORMAT_JPEG)
64+
.setScannerMode(GmsDocumentScannerOptions.SCANNER_MODE_FULL);
65+
66+
if (options.hasKey("maxNumDocuments")) {
67+
documentScannerOptionsBuilder.setPageLimit(
68+
options.getInt("maxNumDocuments")
69+
);
70+
}
71+
72+
int croppedImageQuality;
73+
if (options.hasKey("croppedImageQuality")) {
74+
croppedImageQuality = options.getInt("croppedImageQuality");
75+
} else {
76+
croppedImageQuality = 100;
77+
}
78+
79+
GmsDocumentScanner scanner = GmsDocumentScanning.getClient(documentScannerOptionsBuilder.build());
80+
ActivityResultLauncher<IntentSenderRequest> scannerLauncher = ((ComponentActivity) currentActivity).getActivityResultRegistry().register(
81+
"document-scanner",
82+
new ActivityResultContracts.StartIntentSenderForResult(),
83+
result -> {
84+
if (result.getResultCode() == Activity.RESULT_OK) {
85+
GmsDocumentScanningResult documentScanningResult = GmsDocumentScanningResult.fromActivityResultIntent(
86+
result.getData()
87+
);
88+
WritableArray docScanResults = new WritableNativeArray();
89+
90+
if (documentScanningResult != null) {
91+
List<Page> pages = documentScanningResult.getPages();
92+
if (pages != null) {
93+
for (Page page : pages) {
94+
Uri croppedImageUri = page.getImageUri();
95+
String croppedImageResults = croppedImageUri.toString();
96+
97+
if (options.hasKey("responseType") && Objects.equals(options.getString("responseType"), "base64")) {
98+
try {
99+
croppedImageResults = this.getImageInBase64(currentActivity, croppedImageUri, croppedImageQuality);
100+
} catch (FileNotFoundException error) {
101+
promise.reject("document scan error", error.getMessage());
102+
}
103+
}
104+
105+
docScanResults.pushString(croppedImageResults);
106+
}
107+
}
108+
}
109+
110+
response.putArray(
74111
"scannedImages",
75112
docScanResults
76-
);
77-
response.putString("status", "success");
78-
promise.resolve(response);
79-
return null;
80-
},
81-
(String errorMessage) -> {
82-
// document scan error
83-
promise.reject("document scan error", errorMessage);
84-
return null;
85-
},
86-
() -> {
87-
// when user cancels document scan
88-
response.putString("status", "cancel");
89-
promise.resolve(response);
90-
return null;
91-
},
92-
options.hasKey("responseType")
93-
? options.getString("responseType") : null,
94-
options.hasKey(DocumentScannerExtra.EXTRA_LET_USER_ADJUST_CROP)
95-
? options.getBoolean(DocumentScannerExtra.EXTRA_LET_USER_ADJUST_CROP)
96-
: null,
97-
options.hasKey(DocumentScannerExtra.EXTRA_MAX_NUM_DOCUMENTS)
98-
? options.getInt(DocumentScannerExtra.EXTRA_MAX_NUM_DOCUMENTS)
99-
: null,
100-
options.hasKey(DocumentScannerExtra.EXTRA_CROPPED_IMAGE_QUALITY)
101-
? options.getInt(DocumentScannerExtra.EXTRA_CROPPED_IMAGE_QUALITY)
102-
: null
113+
);
114+
response.putString("status", "success");
115+
promise.resolve(response);
116+
} else if (result.getResultCode() == Activity.RESULT_CANCELED) {
117+
// when user cancels document scan
118+
response.putString("status", "cancel");
119+
promise.resolve(response);
120+
}
121+
}
103122
);
104123

105-
// launch the document scanner
106-
currentActivity.startActivityForResult(
107-
documentScanner.createDocumentScanIntent(),
108-
DOCUMENT_SCAN_REQUEST
109-
);
124+
scanner.getStartScanIntent(currentActivity)
125+
.addOnSuccessListener(intentSender ->
126+
scannerLauncher.launch(new IntentSenderRequest.Builder(intentSender).build()))
127+
.addOnFailureListener(error -> {
128+
// document scan error
129+
promise.reject("document scan error", error.getMessage());
130+
});
110131
}
111132
}

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-document-scanner-plugin",
3-
"version": "0.9.1",
3+
"version": "1.0.0",
44
"description": "A React Native plugin that lets you scan documents using Android and iOS",
55
"main": "lib/commonjs/index",
66
"module": "lib/module/index",

src/index.tsx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,10 @@ export interface ScanDocumentOptions {
3434
* @default: 100
3535
*/
3636
croppedImageQuality?: number;
37-
38-
/**
39-
* Android only: If true then once the user takes a photo, they get to preview the automatically
40-
* detected document corners. They can then move the corners in case there needs to
41-
* be an adjustment. If false then the user can't adjust the corners, and the user
42-
* can only take 1 photo (maxNumDocuments can't be more than 1 in this case).
43-
* @default: true
44-
*/
45-
letUserAdjustCrop?: boolean;
4637

4738
/**
4839
* Android only: The maximum number of photos an user can take (not counting photo retakes)
49-
* @default: 24
40+
* @default: undefined
5041
*/
5142
maxNumDocuments?: number;
5243

0 commit comments

Comments
 (0)