react-native-document-processor 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,204 @@
1
- # Temporary Holding Version
1
+ # react-native-document-processor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Phiên bản **0.1.0** — thư viện React Native chụp tài liệu, tạo PDF và đọc QR/barcode
4
+ bằng native. Được phát triển cho luồng giấy tờ của 24hMoney.
5
+
6
+ Repository: [quocvu12345/react-native-document-processor](https://github.com/quocvu12345/react-native-document-processor).
7
+
8
+ ## Cài bằng npm hoặc Yarn
9
+
10
+ ```sh
11
+ yarn add react-native-document-processor@0.1.0 --exact
12
+ # Hoặc: npm install react-native-document-processor@0.1.0 --save-exact
13
+ pod install --project-directory=ios
14
+ ```
15
+
16
+ Package: [react-native-document-processor trên npm](https://www.npmjs.com/package/react-native-document-processor).
17
+ Source native của phiên bản này cũng có ở tag GitHub `v0.1.0`.
18
+
19
+ Yêu cầu Android API 28+, iOS 15.1+ và React Native có autolinking. Đã kiểm tra
20
+ trong ứng dụng React Native 0.83. Camera Android dùng Google Play services;
21
+ ML Kit barcode model được đóng gói sẵn trong app. iOS cần khai báo
22
+ `NSCameraUsageDescription` trong `Info.plist` của ứng dụng.
23
+
24
+ Metadata giấy phép hiện giữ nguyên `UNLICENSED`.
25
+
26
+ ## Luồng xử lý
27
+
28
+ `scanDocument` của chính module này mở camera native: VisionKit trên iOS và
29
+ ML Kit Document Scanner trên Android, có phát hiện biên, crop và xác nhận ảnh.
30
+ Đã thay thế và gỡ `react-native-document-scanner-plugin` cùng
31
+ `react-native-images-to-pdf`. Sau khi người dùng hoàn tất chụp:
32
+
33
+ ```text
34
+ Ảnh đã scan
35
+ → native decode + chuẩn hóa hướng EXIF
36
+ → đọc QR trên bitmap
37
+ → tạo preview nhỏ + ghi trang PDF từ cùng bitmap
38
+ → trả PDF URI + mã QR + số trang về JS
39
+ → đối chiếu mã giấy tờ và upload PDF bằng logic hiện có
40
+ ```
41
+
42
+ Mỗi trang cần một lời gọi `appendPage`; bắt đầu, hoàn tất và giải phóng session
43
+ có ba lời gọi riêng. Không truyền bitmap/base64 qua bridge, không ghi ảnh crop
44
+ hoặc ảnh resize trung gian, không render lại PDF để đọc QR. Các trang xử lý tuần
45
+ tự trên worker riêng. Android ghi JPEG trực tiếp vào PDF và chỉ giữ metadata của
46
+ các trang đã xong; iOS dùng Core Graphics PDF context và autorelease pool từng trang.
47
+
48
+ - iOS: Vision chỉ đọc QR, ImageIO decode ảnh, Core Graphics tạo PDF.
49
+ - Android: ML Kit barcode-scanning **bundled** 17.3.0 chỉ đọc QR, ImageDecoder
50
+ chuẩn hóa EXIF, `JpegPdfWriter` ghi PDF image-only; camera dùng
51
+ play-services-mlkit-document-scanner 16.0.0 và chỉ yêu cầu JPEG.
52
+ - Đọc ảnh tối đa 4096 px cạnh dài trước khi encode PDF; PDF tối đa 3000 px,
53
+ JPEG 92%; preview tối đa 1000 px, JPEG 85%. Không upscale ảnh nhỏ.
54
+ - Khi chưa thấy payload `doc|...|<số>`, thử bốn vùng chồng lấn 60% trong RAM như
55
+ chiến lược cũ. JS vẫn là nơi parse, giữ số 0 đầu, gộp mã trùng, đối chiếu mã
56
+ thiếu/thừa và giữ các trang xuất hiện của từng mã.
57
+ - Lỗi đọc QR được trả bằng trạng thái trang `error`; lỗi decode ảnh hoặc ghi PDF
58
+ làm hủy cả session để tránh upload một PDF thiếu trang.
59
+ - Preview là tùy chọn; lỗi tạo preview không làm mất mã QR đã đọc được.
60
+
61
+ ## Phát triển trong ứng dụng 24hMoney
62
+
63
+ Root `package.json` dùng phiên bản registry `0.1.0`. Expo/React Native autolinking
64
+ nhận cả podspec iOS và package Android.
65
+
66
+ ```sh
67
+ yarn install
68
+ pod install --project-directory=ios
69
+ ```
70
+
71
+ Sau đó rebuild native app bằng cấu hình development đang dùng. Reload JS/OTA
72
+ không bổ sung được native module vào binary cũ. `FileUploadHelper` luôn dùng module
73
+ mới cho camera và tạo PDF; không còn fallback sang hai thư viện đã gỡ. Cần phát
74
+ hành binary mới và quản lý runtime OTA phù hợp khi triển khai thay đổi này.
75
+
76
+ Source phát triển vẫn nằm trong `node_modules_dev/react-native-document-processor`.
77
+ Khi cần thử thay đổi local, có thể tạm dùng dependency `file:`; trước khi phát hành
78
+ app cần quay lại phiên bản registry đã publish và rebuild native.
79
+
80
+ ```ts
81
+ import { processDocumentImages, scanDocument } from 'react-native-document-processor';
82
+
83
+ const capture = await scanDocument({ croppedImageQuality: 100 });
84
+ if (capture.status === 'cancel') return;
85
+ const output = await processDocumentImages(capture.scannedImages, {
86
+ onProgress: (completed, total) => updateProgress(completed, total),
87
+ isCancelled: () => hasLeftScreen,
88
+ });
89
+ // output.pdfUri: PDF để upload
90
+ // output.pages: QR, trạng thái và preview của từng trang (đánh số từ 1)
91
+ // output.directoryUri: caller sở hữu sau khi thành công
92
+ // capture.scannedImages: caller cũng phải xóa sau khi dùng xong
93
+ ```
94
+
95
+ Trong app, gọi `DocumentQrService.processImagesToPdf` và giải phóng
96
+ `DocumentQrService.release(qrResult)` khi hết sử dụng preview. Helper tự xóa PDF
97
+ sau upload; thư mục native chứa mọi preview được xóa khi release kết quả, kể cả
98
+ preview của trang có mã trùng. Native không xóa ảnh đầu vào. Khi hủy/lỗi trước
99
+ khi chuyển quyền sở hữu, wrapper xóa toàn bộ output của session.
100
+
101
+ Android copy JPEG từ scanner sang cache do app quản lý, không re-encode; callback
102
+ được xử lý qua activity listener duy nhất, không đăng ký launcher lặp mỗi lần scan.
103
+ iOS lưu ảnh theo từng trang trên worker và chỉ trả kết quả khi camera đã đóng.
104
+ Hủy, lỗi camera, thiếu foreground activity và một lần mở camera đang chạy đều được xử lý.
105
+
106
+ Hủy được kiểm tra giữa các trang và trước/sau khi đóng PDF. Một trang đang được
107
+ Vision/ML Kit xử lý sẽ chạy xong rồi mới dọn tài nguyên; không giải phóng bitmap
108
+ trong lúc decoder còn dùng nó.
109
+
110
+ ## Phạm vi và kiểm tra
111
+
112
+ Mọi luồng `ScanFileDocument` đều dùng camera và PDF writer mới. Luồng không cần QR
113
+ truyền `scanQr: false`, bỏ qua decoder và preview nhưng vẫn trả ảnh gốc cho caller.
114
+ Luồng chọn PDF có sẵn tiếp tục dùng `react-native-pdf-thumbnail` để render trang.
115
+ `rn-qr-generator` và patch của nó đã gỡ; phần đọc mã từ ảnh trong màn QR,
116
+ `TextRecognizeService` và `DocumentQrService` đều dùng module này.
117
+ Camera quét trực tiếp vẫn dùng component VisionCamera hiện có (QR + EAN-13).
118
+ Thuật toán biên/crop tài liệu là API native của hệ điều hành.
119
+
120
+ ## Đọc QR/barcode từ ảnh
121
+
122
+ ```ts
123
+ import { scanBarcodes, scanQrImage } from 'react-native-document-processor';
124
+
125
+ const qr = await scanQrImage('/path/to/image.jpg'); // Chỉ QR
126
+ const all = await scanBarcodes('file:///path/to/image.jpg');
127
+ const selected = await scanBarcodes('content://provider/image', {
128
+ formats: ['qr', 'ean-13'],
129
+ });
130
+ // { values: ['0030358'], codes: [{ value: '0030358', format: 'qr' }] }
131
+ ```
132
+
133
+ Android dùng ML Kit bundled đã có; iOS dùng Vision, không thêm dependency.
134
+ API chạy trên worker, dùng chung decoder giới hạn 4096 px và chuẩn hóa EXIF với
135
+ luồng tài liệu. Không mở camera, tạo PDF, preview hoặc file tạm; không xóa ảnh đầu vào.
136
+ Hỗ trợ đường dẫn tuyệt đối/`file://`, thêm `content://` trên Android; không nhận
137
+ URL mạng/base64. Không có mã trả `values: [], codes: []`; ảnh hỏng/lỗi decoder reject.
138
+ Đọc nhiều mã, gộp mã trùng và giữ chuỗi gốc, kể cả số 0 đầu.
139
+
140
+ Các format: `qr`, `ean-13`, `ean-8`, `upc-a`, `upc-e`, `code-128`, `code-39`,
141
+ `code-93`, `codabar`, `itf`, `pdf-417`, `aztec`, `data-matrix`. iOS quy đổi UPC-A
142
+ từ EAN-13 có số 0 đầu khi yêu cầu UPC-A. Không cung cấp API tạo ảnh QR vì app
143
+ không dùng chức năng generate của thư viện cũ. Với giấy tờ, `scanQrImage` chỉ đọc QR
144
+ để tránh nhận nhầm chữ thành barcode 1D; fallback crop của luồng PDF có sẵn giữ nguyên.
145
+
146
+ Các kiểm tra tự động bao gồm matching mã, trang trùng, camera mới, xử lý tuần tự,
147
+ hủy, lỗi ghi file, cleanup và giữ ảnh của caller không cần QR. Chạy trong project
148
+ ứng dụng 24hMoney (các test service/helper nằm trong ứng dụng):
149
+
150
+ ```sh
151
+ yarn jest --runInBand src/services/__tests__/DocumentQrService.test.ts src/helpers/__tests__/FileUploadHelper.documentQr.test.tsx node_modules_dev/react-native-document-processor/src/__tests__/index.test.ts
152
+
153
+ # macOS + Xcode + JDK: kiểm tra PDF bằng parser/render độc lập
154
+ bash node_modules_dev/react-native-document-processor/tests/check-pdf-writer.sh
155
+
156
+ # Tại thư mục android, có thiết bị ADB: chạy ML Kit/PDF/JNI thực
157
+ ./gradlew :react-native-document-processor:connectedReleaseAndroidTest
158
+ ```
159
+
160
+ Nếu clone riêng repository thư viện, kiểm tra PDF writer độc lập bằng
161
+ `bash tests/check-pdf-writer.sh` trên macOS có Xcode và JDK. Test React Native và
162
+ Android instrumentation cần chạy trong ứng dụng host đã autolink module này.
163
+
164
+ Đã kiểm tra riêng Objective-C bằng Clang với iOS SDK/React headers thực, Java bằng
165
+ javac với Android SDK và dependency đã cache. PDF Android 30 trang đã được đọc
166
+ và render bằng PDFKit, kiểm tra số trang, thứ tự, kích thước và hướng ảnh.
167
+ App Android release đã build và cài trên MI 8 Lite; bảy instrumentation test
168
+ ML Kit/PDF/cleanup/camera thiếu activity, nhiều QR, EXIF, EAN-13, Code128,
169
+ lọc QR-only và khôi phục sau ảnh lỗi đã chạy thành công trên thiết bị.
170
+ Chưa build/link toàn bộ app iOS hoặc kiểm thử chụp giấy thật tự động.
171
+ Nếu JDK không nằm trong PATH, đặt `DOCUMENT_PROCESSOR_JAVAC` và
172
+ `DOCUMENT_PROCESSOR_JAVA` trỏ đến hai executable tương ứng khi chạy script.
173
+
174
+ Cần kiểm thử thực tế bộ 10/24/30 trang, QR nhỏ/mờ/xoay, nhiều QR trên một trang,
175
+ chụp thêm 24 + 6 trang, hủy/thoát màn hình và upload lỗi. Ảnh trên 4096 px được
176
+ downsample nên cần đo khả năng đọc QR rất nhỏ trên giấy thật. Chưa đo thời gian/RAM
177
+ hoặc đưa ra tỷ lệ tăng tốc trên thiết bị.
178
+
179
+ ## Log khi test Android
180
+
181
+ Log native và các sự kiện `DocumentQrService` dùng chung tag `DocumentScan`,
182
+ hoạt động cả trong release. Mặc định tắt; bật trên thiết bị test bằng:
183
+
184
+ ```sh
185
+ adb shell setprop log.tag.DocumentScan DEBUG
186
+ adb logcat -v threadtime 'DocumentScan:D' 'AndroidRuntime:E' '*:S'
187
+ ```
188
+
189
+ Android Studio: mở project `android`, chọn MI 8 Lite trong Logcat và lọc
190
+ `tag:DocumentScan`. Các sự kiện gồm `camera.*`, `session.*`, `page.*`,
191
+ `pdf.complete`, `barcode.*`, `js.native:complete`, `js.upload:*`. Log hiển thị thời gian
192
+ xử lý, số trang, kích thước PDF, QR từng trang, mã khớp/thiếu/thừa và lỗi.
193
+ Không ghi bitmap/base64. Log có mã giấy tờ khi bật nên chỉ lưu cục bộ để test.
194
+
195
+ Tắt khi hoàn tất (hoặc thiết bị khởi động lại):
196
+
197
+ ```sh
198
+ adb shell setprop log.tag.DocumentScan INFO
199
+ ```
200
+
201
+ Tài liệu API:
202
+ [Apple Vision](https://developer.apple.com/documentation/vision/vndetectbarcodesrequest),
203
+ [ML Kit barcode scanning](https://developers.google.com/ml-kit/vision/barcode-scanning/android),
204
+ [Android ImageDecoder](https://developer.android.com/reference/android/graphics/ImageDecoder).
@@ -0,0 +1,24 @@
1
+ apply plugin: 'com.android.library'
2
+
3
+ android {
4
+ namespace 'vn.money24h.documentprocessor'
5
+ testBuildType 'release'
6
+ compileSdkVersion rootProject.ext.has('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
7
+ defaultConfig {
8
+ minSdkVersion 28
9
+ targetSdkVersion rootProject.ext.has('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
10
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
11
+ }
12
+ compileOptions {
13
+ sourceCompatibility JavaVersion.VERSION_1_8
14
+ targetCompatibility JavaVersion.VERSION_1_8
15
+ }
16
+ }
17
+
18
+ dependencies {
19
+ implementation 'com.facebook.react:react-android'
20
+ implementation 'com.google.mlkit:barcode-scanning:17.3.0'
21
+ implementation 'com.google.android.gms:play-services-mlkit-document-scanner:16.0.0'
22
+ androidTestImplementation 'androidx.test:runner:1.6.1'
23
+ androidTestImplementation 'com.google.zxing:core:3.5.2'
24
+ }
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,81 @@
1
+ package vn.money24h.documentprocessor;
2
+
3
+ import android.graphics.Bitmap;
4
+ import com.facebook.react.bridge.Arguments;
5
+ import com.facebook.react.bridge.ReadableArray;
6
+ import com.facebook.react.bridge.WritableArray;
7
+ import com.facebook.react.bridge.WritableMap;
8
+ import com.google.android.gms.tasks.Tasks;
9
+ import com.google.mlkit.vision.barcode.BarcodeScanner;
10
+ import com.google.mlkit.vision.barcode.BarcodeScannerOptions;
11
+ import com.google.mlkit.vision.barcode.BarcodeScanning;
12
+ import com.google.mlkit.vision.barcode.common.Barcode;
13
+ import com.google.mlkit.vision.common.InputImage;
14
+ import java.util.LinkedHashMap;
15
+ import java.util.LinkedHashSet;
16
+ import java.util.Map;
17
+ import java.util.Set;
18
+
19
+ /** Stateless image decoding on the processor worker; no document session/files. */
20
+ final class BarcodeImageScanner {
21
+ private static final Map<String, Integer> FORMATS = new LinkedHashMap<>();
22
+ static {
23
+ FORMATS.put("qr", Barcode.FORMAT_QR_CODE);
24
+ FORMATS.put("ean-13", Barcode.FORMAT_EAN_13);
25
+ FORMATS.put("ean-8", Barcode.FORMAT_EAN_8);
26
+ FORMATS.put("upc-a", Barcode.FORMAT_UPC_A);
27
+ FORMATS.put("upc-e", Barcode.FORMAT_UPC_E);
28
+ FORMATS.put("code-128", Barcode.FORMAT_CODE_128);
29
+ FORMATS.put("code-39", Barcode.FORMAT_CODE_39);
30
+ FORMATS.put("code-93", Barcode.FORMAT_CODE_93);
31
+ FORMATS.put("codabar", Barcode.FORMAT_CODABAR);
32
+ FORMATS.put("itf", Barcode.FORMAT_ITF);
33
+ FORMATS.put("pdf-417", Barcode.FORMAT_PDF417);
34
+ FORMATS.put("aztec", Barcode.FORMAT_AZTEC);
35
+ FORMATS.put("data-matrix", Barcode.FORMAT_DATA_MATRIX);
36
+ }
37
+
38
+ private BarcodeImageScanner() { }
39
+
40
+ static WritableMap scan(Bitmap bitmap, ReadableArray formats) throws Exception {
41
+ if (formats == null || formats.size() == 0) {
42
+ throw new IllegalArgumentException("At least one barcode format is required.");
43
+ }
44
+ int mask = 0;
45
+ for (int i = 0; i < formats.size(); i++) {
46
+ Integer format = FORMATS.get(formats.getString(i));
47
+ if (format == null) { throw new IllegalArgumentException("Unsupported barcode format."); }
48
+ mask |= format;
49
+ }
50
+ BarcodeScanner scanner = BarcodeScanning.getClient(new BarcodeScannerOptions.Builder()
51
+ .setBarcodeFormats(mask).build());
52
+ try {
53
+ WritableArray codes = Arguments.createArray();
54
+ Set<String> values = new LinkedHashSet<>();
55
+ Set<String> uniqueCodes = new LinkedHashSet<>();
56
+ for (Barcode barcode : Tasks.await(scanner.process(InputImage.fromBitmap(bitmap, 0)))) {
57
+ String value = barcode.getRawValue();
58
+ if (value == null || value.isEmpty()) { continue; }
59
+ for (Map.Entry<String, Integer> format : FORMATS.entrySet()) {
60
+ if (format.getValue() != barcode.getFormat() || (mask & format.getValue()) == 0) { continue; }
61
+ if (uniqueCodes.add(format.getKey() + "\n" + value)) {
62
+ WritableMap code = Arguments.createMap();
63
+ code.putString("value", value);
64
+ code.putString("format", format.getKey());
65
+ codes.pushMap(code);
66
+ values.add(value);
67
+ }
68
+ break;
69
+ }
70
+ }
71
+ WritableArray rawValues = Arguments.createArray();
72
+ for (String value : values) { rawValues.pushString(value); }
73
+ WritableMap result = Arguments.createMap();
74
+ result.putArray("values", rawValues);
75
+ result.putArray("codes", codes);
76
+ return result;
77
+ } finally {
78
+ scanner.close();
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,163 @@
1
+ package vn.money24h.documentprocessor;
2
+
3
+ import android.app.Activity;
4
+ import android.content.Intent;
5
+ import android.net.Uri;
6
+ import android.os.SystemClock;
7
+ import com.facebook.react.bridge.Arguments;
8
+ import com.facebook.react.bridge.BaseActivityEventListener;
9
+ import com.facebook.react.bridge.Promise;
10
+ import com.facebook.react.bridge.ReactApplicationContext;
11
+ import com.facebook.react.bridge.UiThreadUtil;
12
+ import com.facebook.react.bridge.WritableArray;
13
+ import com.facebook.react.bridge.WritableMap;
14
+ import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions;
15
+ import com.google.mlkit.vision.documentscanner.GmsDocumentScanning;
16
+ import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult;
17
+ import java.io.File;
18
+ import java.io.FileOutputStream;
19
+ import java.io.IOException;
20
+ import java.io.InputStream;
21
+ import java.util.ArrayList;
22
+ import java.util.List;
23
+ import java.util.concurrent.ExecutorService;
24
+ import java.util.concurrent.Executors;
25
+
26
+ /** Owns camera lifecycle and copies scanner results into files owned by our caller. */
27
+ final class DocumentCapture extends BaseActivityEventListener {
28
+ private static final int REQUEST_SCAN = 24861;
29
+ private final ReactApplicationContext context;
30
+ private final ExecutorService worker = Executors.newSingleThreadExecutor();
31
+ private Promise pending;
32
+ private boolean closed;
33
+ private long startedAt;
34
+
35
+ DocumentCapture(ReactApplicationContext context) {
36
+ this.context = context;
37
+ context.addActivityEventListener(this);
38
+ }
39
+
40
+ synchronized void scan(Promise promise) {
41
+ if (closed || pending != null) {
42
+ DocumentScanLog.event("camera.busy", "closed=" + closed);
43
+ promise.reject("DOCUMENT_CAPTURE_BUSY", "Document camera is busy or closed.");
44
+ return;
45
+ }
46
+ pending = promise;
47
+ startedAt = SystemClock.elapsedRealtime();
48
+ DocumentScanLog.event("camera.start", "format=JPEG mode=FULL");
49
+ UiThreadUtil.runOnUiThread(() -> {
50
+ if (!isActive(promise)) { return; }
51
+ Activity activity = context.getCurrentActivity();
52
+ if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
53
+ fail(promise, new IOException("No foreground activity for document camera."));
54
+ return;
55
+ }
56
+ try {
57
+ GmsDocumentScannerOptions options = new GmsDocumentScannerOptions.Builder()
58
+ .setResultFormats(GmsDocumentScannerOptions.RESULT_FORMAT_JPEG)
59
+ .setScannerMode(GmsDocumentScannerOptions.SCANNER_MODE_FULL)
60
+ .build();
61
+ GmsDocumentScanning.getClient(options).getStartScanIntent(activity)
62
+ .addOnSuccessListener(sender -> {
63
+ if (!isActive(promise)) { return; }
64
+ try {
65
+ DocumentScanLog.event("camera.ready", "elapsedMs=" + (SystemClock.elapsedRealtime() - startedAt));
66
+ activity.startIntentSenderForResult(sender, REQUEST_SCAN, null, 0, 0, 0);
67
+ } catch (Exception error) {
68
+ fail(promise, error);
69
+ }
70
+ })
71
+ .addOnFailureListener(error -> fail(promise, error));
72
+ } catch (Exception error) {
73
+ fail(promise, error);
74
+ }
75
+ });
76
+ }
77
+
78
+ private synchronized boolean isActive(Promise promise) {
79
+ return !closed && pending == promise;
80
+ }
81
+
82
+ private synchronized void fail(Promise promise, Exception error) {
83
+ if (pending == promise) {
84
+ pending = null;
85
+ DocumentScanLog.error("camera.error", error);
86
+ promise.reject("DOCUMENT_CAPTURE_ERROR", error.getMessage(), error);
87
+ }
88
+ }
89
+
90
+ private synchronized boolean complete(Promise promise, String status, List<File> files) {
91
+ if (!isActive(promise)) { return false; }
92
+ WritableArray images = Arguments.createArray();
93
+ for (File file : files) { images.pushString(Uri.fromFile(file).toString()); }
94
+ WritableMap result = Arguments.createMap();
95
+ result.putString("status", status);
96
+ result.putArray("scannedImages", images);
97
+ pending = null;
98
+ DocumentScanLog.event("camera.complete", "status=" + status + " pages=" + files.size()
99
+ + " elapsedMs=" + (SystemClock.elapsedRealtime() - startedAt));
100
+ promise.resolve(result);
101
+ return true;
102
+ }
103
+
104
+ @Override
105
+ public synchronized void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
106
+ if (requestCode != REQUEST_SCAN || pending == null || closed) { return; }
107
+ Promise promise = pending;
108
+ DocumentScanLog.event("camera.result", "resultCode=" + resultCode);
109
+ if (resultCode == Activity.RESULT_CANCELED) {
110
+ complete(promise, "cancel", new ArrayList<>());
111
+ return;
112
+ }
113
+ GmsDocumentScanningResult result = GmsDocumentScanningResult.fromActivityResultIntent(data);
114
+ if (resultCode != Activity.RESULT_OK || result == null || result.getPages() == null
115
+ || result.getPages().isEmpty()) {
116
+ fail(promise, new IOException("Document camera returned no pages."));
117
+ return;
118
+ }
119
+ List<GmsDocumentScanningResult.Page> pages = new ArrayList<>(result.getPages());
120
+ DocumentScanLog.event("camera.copy.start", "pages=" + pages.size());
121
+ worker.execute(() -> {
122
+ long copyStartedAt = SystemClock.elapsedRealtime();
123
+ List<File> files = new ArrayList<>();
124
+ boolean transferred = false;
125
+ try {
126
+ byte[] buffer = new byte[64 * 1024];
127
+ for (GmsDocumentScanningResult.Page page : pages) {
128
+ if (!isActive(promise)) { return; }
129
+ File file = File.createTempFile("document-capture-", ".jpg", context.getCacheDir());
130
+ files.add(file);
131
+ try (InputStream input = context.getContentResolver().openInputStream(page.getImageUri());
132
+ FileOutputStream output = new FileOutputStream(file)) {
133
+ if (input == null) { throw new IOException("Cannot read scanned page."); }
134
+ int count;
135
+ while ((count = input.read(buffer)) != -1) { output.write(buffer, 0, count); }
136
+ }
137
+ if (file.length() == 0) { throw new IOException("Scanned page is empty."); }
138
+ DocumentScanLog.event("camera.copy.page", "page=" + files.size() + " bytes=" + file.length());
139
+ }
140
+ DocumentScanLog.event("camera.copy.complete", "pages=" + files.size()
141
+ + " elapsedMs=" + (SystemClock.elapsedRealtime() - copyStartedAt));
142
+ transferred = complete(promise, "success", files);
143
+ } catch (Exception error) {
144
+ fail(promise, error);
145
+ } finally {
146
+ if (!transferred) {
147
+ for (File file : files) { file.delete(); }
148
+ }
149
+ }
150
+ });
151
+ }
152
+
153
+ synchronized void invalidate() {
154
+ if (closed) { return; }
155
+ closed = true;
156
+ if (pending != null) {
157
+ pending.reject("DOCUMENT_CAPTURE_CANCELLED", "Document camera was closed.");
158
+ pending = null;
159
+ }
160
+ context.removeActivityEventListener(this);
161
+ worker.shutdown();
162
+ }
163
+ }