react-native-rectangle-doc-scanner 11.2.0 → 12.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.
- package/android/build.gradle +1 -0
- package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/CameraController.kt +3 -2
- package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt +20 -0
- package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerViewManager.kt +5 -0
- package/android/src/common/kotlin/com/reactnativerectangledocscanner/DocumentDetector.kt +7 -7
- package/android/src/main/kotlin/com/reactnativerectangledocscanner/DocumentScannerModule.kt +187 -4
- package/dist/DocScanner.js +6 -2
- package/dist/FullDocScanner.js +11 -2
- package/package.json +1 -1
- package/src/DocScanner.tsx +10 -3
- package/src/FullDocScanner.tsx +12 -2
- package/src/external.d.ts +1 -0
- package/vendor/react-native-document-scanner/index.d.ts +1 -0
package/android/build.gradle
CHANGED
|
@@ -86,6 +86,7 @@ dependencies {
|
|
|
86
86
|
|
|
87
87
|
// ML Kit object detection for live rectangle hints (Camera2 mode)
|
|
88
88
|
implementation 'com.google.mlkit:object-detection:17.0.1'
|
|
89
|
+
implementation 'com.google.android.gms:play-services-mlkit-document-scanner:16.0.0-beta1'
|
|
89
90
|
|
|
90
91
|
if (hasVisionCamera) {
|
|
91
92
|
// VisionCamera mode - include VisionCamera dependency
|
|
@@ -280,10 +280,11 @@ class CameraController(
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
// Use the same rotation logic as updateTextureViewTransform
|
|
283
|
+
val tabletUpsideDownFix = if (sensorOrientation == 0 && displayRotationDegrees == 90) 180 else 0
|
|
283
284
|
val effectiveRotation = if (sensorOrientation == 0) {
|
|
284
|
-
displayRotationDegrees
|
|
285
|
+
(displayRotationDegrees + tabletUpsideDownFix) % 360
|
|
285
286
|
} else {
|
|
286
|
-
sensorOrientation
|
|
287
|
+
sensorOrientation
|
|
287
288
|
}
|
|
288
289
|
|
|
289
290
|
Log.d(TAG, "[ANALYZE] Sensor: $sensorOrientation°, Display: $displayRotationDegrees°, Effective: $effectiveRotation°")
|
package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt
CHANGED
|
@@ -49,6 +49,7 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
49
49
|
var brightness: Float = 0f
|
|
50
50
|
var contrast: Float = 1f
|
|
51
51
|
var saturation: Float = 1f
|
|
52
|
+
var useExternalScanner: Boolean = false
|
|
52
53
|
|
|
53
54
|
// State
|
|
54
55
|
private var stableCounter = 0
|
|
@@ -142,6 +143,10 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
private fun initializeCameraWhenReady() {
|
|
146
|
+
if (useExternalScanner) {
|
|
147
|
+
Log.d(TAG, "[INIT] External scanner enabled - skipping camera startup")
|
|
148
|
+
return
|
|
149
|
+
}
|
|
145
150
|
// If view is already laid out, start camera immediately
|
|
146
151
|
if (width > 0 && height > 0) {
|
|
147
152
|
Log.d(TAG, "[INIT] View already laid out, starting camera immediately")
|
|
@@ -175,6 +180,21 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
175
180
|
}
|
|
176
181
|
}
|
|
177
182
|
|
|
183
|
+
fun setUseExternalScanner(enabled: Boolean) {
|
|
184
|
+
if (useExternalScanner == enabled) {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
useExternalScanner = enabled
|
|
188
|
+
Log.d(TAG, "[SET] useExternalScanner: $enabled")
|
|
189
|
+
if (enabled) {
|
|
190
|
+
stopCamera()
|
|
191
|
+
overlayView.setRectangle(null, overlayColor)
|
|
192
|
+
} else if (width > 0 && height > 0 && cameraController == null) {
|
|
193
|
+
setupCamera()
|
|
194
|
+
startCamera()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
178
198
|
private fun setupCamera() {
|
|
179
199
|
try {
|
|
180
200
|
Log.d(TAG, "[SETUP] Creating CameraController...")
|
package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerViewManager.kt
CHANGED
|
@@ -94,6 +94,11 @@ class DocumentScannerViewManager : SimpleViewManager<DocumentScannerView>() {
|
|
|
94
94
|
view.saturation = saturation
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
@ReactProp(name = "useExternalScanner")
|
|
98
|
+
fun setUseExternalScanner(view: DocumentScannerView, enabled: Boolean) {
|
|
99
|
+
view.setUseExternalScanner(enabled)
|
|
100
|
+
}
|
|
101
|
+
|
|
97
102
|
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
|
|
98
103
|
return MapBuilder.of(
|
|
99
104
|
"onPictureTaken",
|
|
@@ -259,7 +259,7 @@ class DocumentDetector {
|
|
|
259
259
|
|
|
260
260
|
var largestRectangle: Rectangle? = null
|
|
261
261
|
var bestScore = 0.0
|
|
262
|
-
val minArea = max(
|
|
262
|
+
val minArea = max(350.0, (srcMat.rows() * srcMat.cols()) * 0.0005)
|
|
263
263
|
|
|
264
264
|
debugStats.contours = contours.size
|
|
265
265
|
|
|
@@ -289,7 +289,7 @@ class DocumentDetector {
|
|
|
289
289
|
val rect = Imgproc.minAreaRect(MatOfPoint2f(*points))
|
|
290
290
|
val rectArea = rect.size.area()
|
|
291
291
|
val rectangularity = if (rectArea > 1.0) contourArea / rectArea else 0.0
|
|
292
|
-
if (rectangularity >= 0.
|
|
292
|
+
if (rectangularity >= 0.5 && isCandidateValid(ordered, srcMat)) {
|
|
293
293
|
debugStats.candidates += 1
|
|
294
294
|
val score = contourArea * rectangularity
|
|
295
295
|
if (score > bestScore) {
|
|
@@ -313,7 +313,7 @@ class DocumentDetector {
|
|
|
313
313
|
val rectArea = rotated.size.area()
|
|
314
314
|
if (rectArea > 1.0) {
|
|
315
315
|
val rectangularity = contourArea / rectArea
|
|
316
|
-
if (rectangularity >= 0.
|
|
316
|
+
if (rectangularity >= 0.5) {
|
|
317
317
|
debugStats.candidates += 1
|
|
318
318
|
val boxPoints = Array(4) { Point() }
|
|
319
319
|
rotated.points(boxPoints)
|
|
@@ -497,16 +497,16 @@ class DocumentDetector {
|
|
|
497
497
|
val rectHeight = max(leftEdgeLength, rightEdgeLength)
|
|
498
498
|
val rectArea = rectWidth * rectHeight
|
|
499
499
|
|
|
500
|
-
// Check if rectangle is too small (less than
|
|
501
|
-
// or too large (more than
|
|
500
|
+
// Check if rectangle is too small (less than 6% of view area)
|
|
501
|
+
// or too large (more than 95% - likely detecting screen instead of document)
|
|
502
502
|
val areaRatio = rectArea / viewArea
|
|
503
|
-
if (areaRatio < 0.
|
|
503
|
+
if (areaRatio < 0.06) {
|
|
504
504
|
if (BuildConfig.DEBUG) {
|
|
505
505
|
Log.d(TAG, "[QUALITY] TOO_FAR (small): area=${String.format("%.1f", rectArea)}, ratio=${String.format("%.2f", areaRatio)}")
|
|
506
506
|
}
|
|
507
507
|
return RectangleQuality.TOO_FAR
|
|
508
508
|
}
|
|
509
|
-
if (areaRatio > 0.
|
|
509
|
+
if (areaRatio > 0.95) {
|
|
510
510
|
if (BuildConfig.DEBUG) {
|
|
511
511
|
Log.d(TAG, "[QUALITY] TOO_FAR (large): area=${String.format("%.1f", rectArea)}, ratio=${String.format("%.2f", areaRatio)} - likely detecting screen")
|
|
512
512
|
}
|
|
@@ -1,24 +1,49 @@
|
|
|
1
1
|
package com.reactnativerectangledocscanner
|
|
2
2
|
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Intent
|
|
3
5
|
import android.graphics.BitmapFactory
|
|
6
|
+
import android.net.Uri
|
|
4
7
|
import android.util.Log
|
|
5
8
|
import com.facebook.react.bridge.*
|
|
6
9
|
import com.facebook.react.uimanager.UIManagerModule
|
|
10
|
+
import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions
|
|
11
|
+
import com.google.mlkit.vision.documentscanner.GmsDocumentScanning
|
|
12
|
+
import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult
|
|
7
13
|
import kotlinx.coroutines.*
|
|
8
14
|
import org.opencv.core.Point
|
|
15
|
+
import java.io.File
|
|
16
|
+
import java.io.FileOutputStream
|
|
9
17
|
|
|
10
18
|
class DocumentScannerModule(reactContext: ReactApplicationContext) :
|
|
11
|
-
ReactContextBaseJavaModule(reactContext) {
|
|
19
|
+
ReactContextBaseJavaModule(reactContext), ActivityEventListener {
|
|
12
20
|
|
|
13
21
|
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
14
22
|
|
|
15
23
|
companion object {
|
|
16
24
|
const val NAME = "RNPdfScannerManager"
|
|
17
25
|
private const val TAG = "DocumentScannerModule"
|
|
26
|
+
private const val EXTERNAL_SCAN_REQUEST = 9401
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
override fun getName() = NAME
|
|
21
30
|
|
|
31
|
+
private data class PendingScanConfig(
|
|
32
|
+
val useBase64: Boolean,
|
|
33
|
+
val saveInAppDocument: Boolean,
|
|
34
|
+
val quality: Float,
|
|
35
|
+
val brightness: Float,
|
|
36
|
+
val contrast: Float,
|
|
37
|
+
val saturation: Float
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
private var pendingScanPromise: Promise? = null
|
|
41
|
+
private var pendingScanConfig: PendingScanConfig? = null
|
|
42
|
+
|
|
43
|
+
init {
|
|
44
|
+
reactContext.addActivityEventListener(this)
|
|
45
|
+
}
|
|
46
|
+
|
|
22
47
|
/**
|
|
23
48
|
* Capture image from the document scanner view
|
|
24
49
|
* Matches iOS signature: capture(reactTag, resolver, rejecter)
|
|
@@ -46,9 +71,13 @@ class DocumentScannerModule(reactContext: ReactApplicationContext) :
|
|
|
46
71
|
if (view is DocumentScannerView) {
|
|
47
72
|
Log.d(TAG, "Found DocumentScannerView, triggering capture with promise")
|
|
48
73
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
74
|
+
if (view.useExternalScanner) {
|
|
75
|
+
startExternalScan(view, promise)
|
|
76
|
+
} else {
|
|
77
|
+
// Pass promise to view so it can be resolved when capture completes
|
|
78
|
+
// This matches iOS behavior where promise is resolved with actual image data
|
|
79
|
+
view.captureWithPromise(promise)
|
|
80
|
+
}
|
|
52
81
|
} else {
|
|
53
82
|
Log.e(TAG, "View with tag $tag is not DocumentScannerView: ${view?.javaClass?.simpleName}")
|
|
54
83
|
promise.reject("INVALID_VIEW", "View is not a DocumentScannerView")
|
|
@@ -64,6 +93,160 @@ class DocumentScannerModule(reactContext: ReactApplicationContext) :
|
|
|
64
93
|
}
|
|
65
94
|
}
|
|
66
95
|
|
|
96
|
+
private fun startExternalScan(view: DocumentScannerView, promise: Promise) {
|
|
97
|
+
if (pendingScanPromise != null) {
|
|
98
|
+
promise.reject("SCAN_IN_PROGRESS", "Another scan is already in progress")
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
val activity = currentActivity ?: run {
|
|
103
|
+
promise.reject("NO_ACTIVITY", "Activity not available")
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
val options = GmsDocumentScannerOptions.Builder()
|
|
108
|
+
.setScannerMode(GmsDocumentScannerOptions.SCANNER_MODE_FULL)
|
|
109
|
+
.setGalleryImportAllowed(true)
|
|
110
|
+
.setPageLimit(1)
|
|
111
|
+
.setResultFormats(GmsDocumentScannerOptions.RESULT_FORMAT_JPEG)
|
|
112
|
+
.build()
|
|
113
|
+
|
|
114
|
+
val scanner = GmsDocumentScanning.getClient(options)
|
|
115
|
+
|
|
116
|
+
pendingScanPromise = promise
|
|
117
|
+
pendingScanConfig = PendingScanConfig(
|
|
118
|
+
useBase64 = view.useBase64,
|
|
119
|
+
saveInAppDocument = view.saveInAppDocument,
|
|
120
|
+
quality = view.quality,
|
|
121
|
+
brightness = view.brightness,
|
|
122
|
+
contrast = view.contrast,
|
|
123
|
+
saturation = view.saturation
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
scanner.getStartScanIntent(activity)
|
|
127
|
+
.addOnSuccessListener { intentSender ->
|
|
128
|
+
try {
|
|
129
|
+
activity.startIntentSenderForResult(
|
|
130
|
+
intentSender,
|
|
131
|
+
EXTERNAL_SCAN_REQUEST,
|
|
132
|
+
null,
|
|
133
|
+
0,
|
|
134
|
+
0,
|
|
135
|
+
0
|
|
136
|
+
)
|
|
137
|
+
} catch (e: Exception) {
|
|
138
|
+
Log.e(TAG, "Failed to launch ML Kit scanner", e)
|
|
139
|
+
cleanupPendingScan()
|
|
140
|
+
promise.reject("SCAN_LAUNCH_FAILED", "Failed to launch scanner: ${e.message}", e)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
.addOnFailureListener { e ->
|
|
144
|
+
Log.e(TAG, "Failed to get ML Kit scan intent", e)
|
|
145
|
+
cleanupPendingScan()
|
|
146
|
+
promise.reject("SCAN_INTENT_FAILED", "Failed to start scanner: ${e.message}", e)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
override fun onActivityResult(activity: Activity?, requestCode: Int, resultCode: Int, data: Intent?) {
|
|
151
|
+
if (requestCode != EXTERNAL_SCAN_REQUEST) {
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
val promise = pendingScanPromise ?: return
|
|
156
|
+
val config = pendingScanConfig
|
|
157
|
+
cleanupPendingScan()
|
|
158
|
+
|
|
159
|
+
if (resultCode != Activity.RESULT_OK || data == null) {
|
|
160
|
+
promise.reject("SCAN_CANCELLED", "Scan cancelled or failed")
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
val result = GmsDocumentScanningResult.fromActivityResultIntent(data)
|
|
165
|
+
val page = result?.pages?.firstOrNull()
|
|
166
|
+
val imageUri = page?.imageUri
|
|
167
|
+
|
|
168
|
+
if (imageUri == null || config == null) {
|
|
169
|
+
promise.reject("SCAN_NO_RESULT", "No scanned image returned")
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
scope.launch {
|
|
174
|
+
try {
|
|
175
|
+
val outputDir = if (config.saveInAppDocument) {
|
|
176
|
+
reactApplicationContext.filesDir
|
|
177
|
+
} else {
|
|
178
|
+
reactApplicationContext.cacheDir
|
|
179
|
+
}
|
|
180
|
+
val timestamp = System.currentTimeMillis()
|
|
181
|
+
val initialPath = copyUriToFile(imageUri, outputDir, "doc_scan_initial_$timestamp.jpg")
|
|
182
|
+
|
|
183
|
+
val processed = withContext(Dispatchers.IO) {
|
|
184
|
+
ImageProcessor.processImage(
|
|
185
|
+
imagePath = initialPath,
|
|
186
|
+
rectangle = null,
|
|
187
|
+
brightness = config.brightness,
|
|
188
|
+
contrast = config.contrast,
|
|
189
|
+
saturation = config.saturation,
|
|
190
|
+
shouldCrop = false
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
val resultMap = Arguments.createMap()
|
|
195
|
+
if (config.useBase64) {
|
|
196
|
+
val croppedBase64 = ImageProcessor.bitmapToBase64(processed.croppedImage, config.quality)
|
|
197
|
+
val initialBase64 = ImageProcessor.bitmapToBase64(processed.initialImage, config.quality)
|
|
198
|
+
resultMap.putString("croppedImage", croppedBase64)
|
|
199
|
+
resultMap.putString("initialImage", initialBase64)
|
|
200
|
+
} else {
|
|
201
|
+
val croppedPath = ImageProcessor.saveBitmapToFile(
|
|
202
|
+
processed.croppedImage,
|
|
203
|
+
outputDir,
|
|
204
|
+
"doc_scan_cropped_$timestamp.jpg",
|
|
205
|
+
config.quality
|
|
206
|
+
)
|
|
207
|
+
resultMap.putString("croppedImage", croppedPath)
|
|
208
|
+
resultMap.putString("initialImage", initialPath)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
resultMap.putMap("rectangleCoordinates", null)
|
|
212
|
+
resultMap.putInt("width", processed.croppedImage.width)
|
|
213
|
+
resultMap.putInt("height", processed.croppedImage.height)
|
|
214
|
+
|
|
215
|
+
// Cleanup bitmaps to avoid leaks.
|
|
216
|
+
if (processed.croppedImage !== processed.initialImage) {
|
|
217
|
+
processed.croppedImage.recycle()
|
|
218
|
+
processed.initialImage.recycle()
|
|
219
|
+
} else {
|
|
220
|
+
processed.croppedImage.recycle()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
promise.resolve(resultMap)
|
|
224
|
+
} catch (e: Exception) {
|
|
225
|
+
Log.e(TAG, "Failed to process scan result", e)
|
|
226
|
+
promise.reject("SCAN_PROCESS_FAILED", "Failed to process scan result: ${e.message}", e)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
override fun onNewIntent(intent: Intent?) {
|
|
232
|
+
// No-op
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private fun copyUriToFile(uri: Uri, outputDir: File, fileName: String): String {
|
|
236
|
+
val outputFile = File(outputDir, fileName)
|
|
237
|
+
reactApplicationContext.contentResolver.openInputStream(uri)?.use { input ->
|
|
238
|
+
FileOutputStream(outputFile).use { output ->
|
|
239
|
+
input.copyTo(output)
|
|
240
|
+
}
|
|
241
|
+
} ?: throw IllegalStateException("Failed to open input stream for URI: $uri")
|
|
242
|
+
return outputFile.absolutePath
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private fun cleanupPendingScan() {
|
|
246
|
+
pendingScanPromise = null
|
|
247
|
+
pendingScanConfig = null
|
|
248
|
+
}
|
|
249
|
+
|
|
67
250
|
/**
|
|
68
251
|
* Apply color controls to an image
|
|
69
252
|
* Matches iOS: applyColorControls(imagePath, brightness, contrast, saturation, resolver, rejecter)
|
package/dist/DocScanner.js
CHANGED
|
@@ -715,14 +715,15 @@ const NativeScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAU
|
|
|
715
715
|
const overlayIsActive = autoCapture ? isAutoCapturing : (detectedRectangle?.stableCounter ?? 0) > 0;
|
|
716
716
|
const detectionThreshold = autoCapture ? minStableFrames : 99999;
|
|
717
717
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
718
|
-
react_1.default.createElement(react_native_document_scanner_1.default, { ref: scannerRef, style: styles.scanner, detectionCountBeforeCapture: detectionThreshold, overlayColor: overlayColor, enableTorch: enableTorch, quality: normalizedQuality, useBase64: useBase64, manualOnly:
|
|
718
|
+
react_1.default.createElement(react_native_document_scanner_1.default, { ref: scannerRef, style: styles.scanner, detectionCountBeforeCapture: detectionThreshold, overlayColor: overlayColor, enableTorch: enableTorch, quality: normalizedQuality, useBase64: useBase64, manualOnly: react_native_1.Platform.OS === 'android', detectionConfig: detectionConfig, useExternalScanner: react_native_1.Platform.OS === 'android', onPictureTaken: handlePictureTaken, onError: handleError, onRectangleDetect: handleRectangleDetect }),
|
|
719
719
|
showGrid && overlayPolygon && (react_1.default.createElement(overlay_1.ScannerOverlay, { active: overlayIsActive, color: gridColor ?? overlayColor, lineWidth: gridLineWidth, polygon: overlayPolygon, clipRect: react_native_1.Platform.OS === 'android' ? null : (detectedRectangle?.previewViewport ?? null) })),
|
|
720
720
|
showManualCaptureButton && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: handleManualCapture })),
|
|
721
721
|
children));
|
|
722
722
|
});
|
|
723
723
|
exports.DocScanner = (0, react_1.forwardRef)((props, ref) => {
|
|
724
|
+
const useExternalScanner = react_native_1.Platform.OS === 'android';
|
|
724
725
|
(0, react_1.useEffect)(() => {
|
|
725
|
-
if (react_native_1.Platform.OS !== 'android') {
|
|
726
|
+
if (react_native_1.Platform.OS !== 'android' || useExternalScanner) {
|
|
726
727
|
return;
|
|
727
728
|
}
|
|
728
729
|
if (hasVisionCamera) {
|
|
@@ -735,6 +736,9 @@ exports.DocScanner = (0, react_1.forwardRef)((props, ref) => {
|
|
|
735
736
|
});
|
|
736
737
|
}
|
|
737
738
|
}, []);
|
|
739
|
+
if (useExternalScanner) {
|
|
740
|
+
return react_1.default.createElement(NativeScanner, { ref: ref, ...props });
|
|
741
|
+
}
|
|
738
742
|
if (hasVisionCamera) {
|
|
739
743
|
return react_1.default.createElement(VisionCameraScanner, { ref: ref, ...props });
|
|
740
744
|
}
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -124,6 +124,7 @@ const normalizeCapturedDocument = (document) => {
|
|
|
124
124
|
};
|
|
125
125
|
};
|
|
126
126
|
const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3170f3', gridColor, gridLineWidth, showGrid, strings, minStableFrames, onError, enableGallery = true, cropWidth = 1200, cropHeight = 1600, type, }) => {
|
|
127
|
+
const useExternalScanner = react_native_1.Platform.OS === 'android';
|
|
127
128
|
const [processing, setProcessing] = (0, react_1.useState)(false);
|
|
128
129
|
const [croppedImageData, setCroppedImageData] = (0, react_1.useState)(null);
|
|
129
130
|
const [isGalleryOpen, setIsGalleryOpen] = (0, react_1.useState)(false);
|
|
@@ -143,6 +144,11 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
143
144
|
const rectangleHintTimeoutRef = (0, react_1.useRef)(null);
|
|
144
145
|
const captureReadyTimeoutRef = (0, react_1.useRef)(null);
|
|
145
146
|
const isBusinessMode = type === 'business';
|
|
147
|
+
(0, react_1.useEffect)(() => {
|
|
148
|
+
if (useExternalScanner) {
|
|
149
|
+
setCaptureReady(true);
|
|
150
|
+
}
|
|
151
|
+
}, [useExternalScanner]);
|
|
146
152
|
const resetScannerView = (0, react_1.useCallback)((options) => {
|
|
147
153
|
setProcessing(false);
|
|
148
154
|
setCroppedImageData(null);
|
|
@@ -388,7 +394,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
388
394
|
return;
|
|
389
395
|
}
|
|
390
396
|
console.log('[FullDocScanner] Starting manual capture, grid detected:', rectangleDetected);
|
|
391
|
-
const captureMode = rectangleDetected ? 'grid' : 'no-grid';
|
|
397
|
+
const captureMode = useExternalScanner ? 'grid' : (rectangleDetected ? 'grid' : 'no-grid');
|
|
392
398
|
captureModeRef.current = captureMode;
|
|
393
399
|
captureInProgressRef.current = true;
|
|
394
400
|
// Add timeout to reset state if capture hangs
|
|
@@ -536,6 +542,9 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
536
542
|
resetScannerView({ remount: true });
|
|
537
543
|
}, [capturedPhotos.length, isBusinessMode, resetScannerView]);
|
|
538
544
|
const handleRectangleDetect = (0, react_1.useCallback)((event) => {
|
|
545
|
+
if (useExternalScanner) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
539
548
|
const stableCounter = event.stableCounter ?? 0;
|
|
540
549
|
const rectangleCoordinates = event.rectangleOnScreen ?? event.rectangleCoordinates;
|
|
541
550
|
const hasRectangle = Boolean(rectangleCoordinates);
|
|
@@ -589,7 +598,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
589
598
|
}
|
|
590
599
|
setRectangleDetected(false);
|
|
591
600
|
}
|
|
592
|
-
}, [rectangleDetected]);
|
|
601
|
+
}, [rectangleDetected, useExternalScanner]);
|
|
593
602
|
(0, react_1.useEffect)(() => () => {
|
|
594
603
|
if (rectangleCaptureTimeoutRef.current) {
|
|
595
604
|
clearTimeout(rectangleCaptureTimeoutRef.current);
|
package/package.json
CHANGED
package/src/DocScanner.tsx
CHANGED
|
@@ -994,7 +994,7 @@ const NativeScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
994
994
|
: detectedRectangle?.rectangleOnScreen ?? detectedRectangle?.rectangleCoordinates ?? null;
|
|
995
995
|
const overlayIsActive = autoCapture ? isAutoCapturing : (detectedRectangle?.stableCounter ?? 0) > 0;
|
|
996
996
|
|
|
997
|
-
|
|
997
|
+
const detectionThreshold = autoCapture ? minStableFrames : 99999;
|
|
998
998
|
|
|
999
999
|
return (
|
|
1000
1000
|
<View style={styles.container}>
|
|
@@ -1006,8 +1006,9 @@ const NativeScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
1006
1006
|
enableTorch={enableTorch}
|
|
1007
1007
|
quality={normalizedQuality}
|
|
1008
1008
|
useBase64={useBase64}
|
|
1009
|
-
manualOnly={
|
|
1009
|
+
manualOnly={Platform.OS === 'android'}
|
|
1010
1010
|
detectionConfig={detectionConfig}
|
|
1011
|
+
useExternalScanner={Platform.OS === 'android'}
|
|
1011
1012
|
onPictureTaken={handlePictureTaken}
|
|
1012
1013
|
onError={handleError}
|
|
1013
1014
|
onRectangleDetect={handleRectangleDetect}
|
|
@@ -1031,8 +1032,10 @@ const NativeScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
1031
1032
|
);
|
|
1032
1033
|
|
|
1033
1034
|
export const DocScanner = forwardRef<DocScannerHandle, Props>((props, ref) => {
|
|
1035
|
+
const useExternalScanner = Platform.OS === 'android';
|
|
1036
|
+
|
|
1034
1037
|
useEffect(() => {
|
|
1035
|
-
if (Platform.OS !== 'android') {
|
|
1038
|
+
if (Platform.OS !== 'android' || useExternalScanner) {
|
|
1036
1039
|
return;
|
|
1037
1040
|
}
|
|
1038
1041
|
if (hasVisionCamera) {
|
|
@@ -1045,6 +1048,10 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>((props, ref) => {
|
|
|
1045
1048
|
}
|
|
1046
1049
|
}, []);
|
|
1047
1050
|
|
|
1051
|
+
if (useExternalScanner) {
|
|
1052
|
+
return <NativeScanner ref={ref} {...props} />;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1048
1055
|
if (hasVisionCamera) {
|
|
1049
1056
|
return <VisionCameraScanner ref={ref} {...props} />;
|
|
1050
1057
|
}
|
package/src/FullDocScanner.tsx
CHANGED
|
@@ -187,6 +187,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
187
187
|
cropHeight = 1600,
|
|
188
188
|
type,
|
|
189
189
|
}) => {
|
|
190
|
+
const useExternalScanner = Platform.OS === 'android';
|
|
190
191
|
const [processing, setProcessing] = useState(false);
|
|
191
192
|
const [croppedImageData, setCroppedImageData] = useState<PreviewImageData | null>(null);
|
|
192
193
|
const [isGalleryOpen, setIsGalleryOpen] = useState(false);
|
|
@@ -208,6 +209,12 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
208
209
|
|
|
209
210
|
const isBusinessMode = type === 'business';
|
|
210
211
|
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
if (useExternalScanner) {
|
|
214
|
+
setCaptureReady(true);
|
|
215
|
+
}
|
|
216
|
+
}, [useExternalScanner]);
|
|
217
|
+
|
|
211
218
|
const resetScannerView = useCallback(
|
|
212
219
|
(options?: { remount?: boolean }) => {
|
|
213
220
|
setProcessing(false);
|
|
@@ -539,7 +546,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
539
546
|
|
|
540
547
|
console.log('[FullDocScanner] Starting manual capture, grid detected:', rectangleDetected);
|
|
541
548
|
|
|
542
|
-
const captureMode = rectangleDetected ? 'grid' : 'no-grid';
|
|
549
|
+
const captureMode = useExternalScanner ? 'grid' : (rectangleDetected ? 'grid' : 'no-grid');
|
|
543
550
|
captureModeRef.current = captureMode;
|
|
544
551
|
captureInProgressRef.current = true;
|
|
545
552
|
|
|
@@ -729,6 +736,9 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
729
736
|
}, [capturedPhotos.length, isBusinessMode, resetScannerView]);
|
|
730
737
|
|
|
731
738
|
const handleRectangleDetect = useCallback((event: RectangleDetectEvent) => {
|
|
739
|
+
if (useExternalScanner) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
732
742
|
const stableCounter = event.stableCounter ?? 0;
|
|
733
743
|
const rectangleCoordinates = event.rectangleOnScreen ?? event.rectangleCoordinates;
|
|
734
744
|
const hasRectangle = Boolean(rectangleCoordinates);
|
|
@@ -787,7 +797,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
787
797
|
}
|
|
788
798
|
setRectangleDetected(false);
|
|
789
799
|
}
|
|
790
|
-
}, [rectangleDetected]);
|
|
800
|
+
}, [rectangleDetected, useExternalScanner]);
|
|
791
801
|
|
|
792
802
|
useEffect(
|
|
793
803
|
() => () => {
|
package/src/external.d.ts
CHANGED
|
@@ -75,6 +75,7 @@ declare module 'react-native-document-scanner' {
|
|
|
75
75
|
maxAnchorMisses?: number;
|
|
76
76
|
maxCenterDelta?: number;
|
|
77
77
|
};
|
|
78
|
+
useExternalScanner?: boolean;
|
|
78
79
|
onPictureTaken?: (event: DocumentScannerResult) => void;
|
|
79
80
|
onError?: (error: Error) => void;
|
|
80
81
|
onRectangleDetect?: (event: RectangleEventPayload) => void;
|
|
@@ -46,6 +46,7 @@ export interface DocumentScannerProps {
|
|
|
46
46
|
maxAnchorMisses?: number;
|
|
47
47
|
maxCenterDelta?: number;
|
|
48
48
|
};
|
|
49
|
+
useExternalScanner?: boolean;
|
|
49
50
|
onPictureTaken?: (event: DocumentScannerResult) => void;
|
|
50
51
|
onError?: (error: Error) => void;
|
|
51
52
|
onRectangleDetect?: (event: RectangleEventPayload) => void;
|