react-native-rectangle-doc-scanner 12.0.0 → 13.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 +0 -1
- package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/CameraController.kt +7 -30
- package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt +60 -22
- package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerViewManager.kt +0 -5
- package/android/src/main/kotlin/com/reactnativerectangledocscanner/DocumentScannerModule.kt +4 -187
- package/dist/DocScanner.js +4 -13
- package/dist/FullDocScanner.js +2 -11
- package/package.json +1 -1
- package/src/DocScanner.tsx +3 -13
- package/src/FullDocScanner.tsx +2 -12
- package/src/external.d.ts +0 -1
- package/vendor/react-native-document-scanner/index.d.ts +0 -1
package/android/build.gradle
CHANGED
|
@@ -86,7 +86,6 @@ 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'
|
|
90
89
|
|
|
91
90
|
if (hasVisionCamera) {
|
|
92
91
|
// VisionCamera mode - include VisionCamera dependency
|
|
@@ -486,32 +486,11 @@ class CameraController(
|
|
|
486
486
|
|
|
487
487
|
if (viewWidth <= 0 || viewHeight <= 0) return null
|
|
488
488
|
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
val
|
|
492
|
-
val displayRotationDegrees = when (textureView.display?.rotation ?: Surface.ROTATION_0) {
|
|
493
|
-
Surface.ROTATION_0 -> 0
|
|
494
|
-
Surface.ROTATION_90 -> 90
|
|
495
|
-
Surface.ROTATION_180 -> 180
|
|
496
|
-
Surface.ROTATION_270 -> 270
|
|
497
|
-
else -> 0
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
fun rotatePoint(point: org.opencv.core.Point): org.opencv.core.Point {
|
|
501
|
-
return if (sensorOrientation == 90) {
|
|
502
|
-
org.opencv.core.Point(
|
|
503
|
-
point.y,
|
|
504
|
-
imageWidth - point.x
|
|
505
|
-
)
|
|
506
|
-
} else {
|
|
507
|
-
point
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
val finalWidth = if (sensorOrientation == 90) imageHeight else imageWidth
|
|
512
|
-
val finalHeight = if (sensorOrientation == 90) imageWidth else imageHeight
|
|
489
|
+
// Rectangle coordinates are already in the rotated image space (effective rotation applied).
|
|
490
|
+
val finalWidth = imageWidth
|
|
491
|
+
val finalHeight = imageHeight
|
|
513
492
|
|
|
514
|
-
//
|
|
493
|
+
// Apply fit-center scaling to match TextureView display.
|
|
515
494
|
val scaleX = viewWidth / finalWidth.toFloat()
|
|
516
495
|
val scaleY = viewHeight / finalHeight.toFloat()
|
|
517
496
|
val scale = scaleX.coerceAtMost(scaleY)
|
|
@@ -522,10 +501,9 @@ class CameraController(
|
|
|
522
501
|
val offsetY = (viewHeight - scaledHeight) / 2f
|
|
523
502
|
|
|
524
503
|
fun transformPoint(point: org.opencv.core.Point): org.opencv.core.Point {
|
|
525
|
-
val rotated = rotatePoint(point)
|
|
526
504
|
return org.opencv.core.Point(
|
|
527
|
-
|
|
528
|
-
|
|
505
|
+
point.x * scale + offsetX,
|
|
506
|
+
point.y * scale + offsetY
|
|
529
507
|
)
|
|
530
508
|
}
|
|
531
509
|
|
|
@@ -536,10 +514,9 @@ class CameraController(
|
|
|
536
514
|
transformPoint(rectangle.bottomRight)
|
|
537
515
|
)
|
|
538
516
|
|
|
539
|
-
Log.d(TAG, "[MAPPING]
|
|
517
|
+
Log.d(TAG, "[MAPPING] Image: ${imageWidth}x${imageHeight} → Final: ${finalWidth}x${finalHeight}")
|
|
540
518
|
Log.d(TAG, "[MAPPING] View: ${viewWidth.toInt()}x${viewHeight.toInt()}, Scale: $scale, Offset: ($offsetX, $offsetY)")
|
|
541
519
|
Log.d(TAG, "[MAPPING] TL: (${rectangle.topLeft.x}, ${rectangle.topLeft.y}) → " +
|
|
542
|
-
"Rotated: (${rotatePoint(rectangle.topLeft).x}, ${rotatePoint(rectangle.topLeft).y}) → " +
|
|
543
520
|
"Final: (${result.topLeft.x}, ${result.topLeft.y})")
|
|
544
521
|
|
|
545
522
|
return result
|
package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt
CHANGED
|
@@ -49,7 +49,6 @@ 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
|
|
53
52
|
|
|
54
53
|
// State
|
|
55
54
|
private var stableCounter = 0
|
|
@@ -61,6 +60,7 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
61
60
|
private var lastDetectedImageHeight = 0
|
|
62
61
|
private var lastRectangleOnScreen: Rectangle? = null
|
|
63
62
|
private var lastSmoothedRectangleOnScreen: Rectangle? = null
|
|
63
|
+
private val iouHistory = ArrayDeque<Rectangle>()
|
|
64
64
|
|
|
65
65
|
// Coroutine scope for async operations
|
|
66
66
|
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
@@ -143,10 +143,6 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
private fun initializeCameraWhenReady() {
|
|
146
|
-
if (useExternalScanner) {
|
|
147
|
-
Log.d(TAG, "[INIT] External scanner enabled - skipping camera startup")
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
146
|
// If view is already laid out, start camera immediately
|
|
151
147
|
if (width > 0 && height > 0) {
|
|
152
148
|
Log.d(TAG, "[INIT] View already laid out, starting camera immediately")
|
|
@@ -180,20 +176,6 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
180
176
|
}
|
|
181
177
|
}
|
|
182
178
|
|
|
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
179
|
|
|
198
180
|
private fun setupCamera() {
|
|
199
181
|
try {
|
|
@@ -325,23 +307,33 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
325
307
|
overlayView.setRectangle(rectangleOnScreen, overlayColor)
|
|
326
308
|
}
|
|
327
309
|
|
|
328
|
-
// Update stable counter based on quality
|
|
310
|
+
// Update stable counter based on quality + IOU stability
|
|
329
311
|
if (rectangleCoordinates == null) {
|
|
330
312
|
if (stableCounter != 0) {
|
|
331
313
|
Log.d(TAG, "Rectangle lost, resetting stableCounter")
|
|
332
314
|
}
|
|
333
315
|
stableCounter = 0
|
|
316
|
+
clearIouHistory()
|
|
334
317
|
} else {
|
|
335
318
|
when (quality) {
|
|
336
319
|
RectangleQuality.GOOD -> {
|
|
337
|
-
|
|
338
|
-
|
|
320
|
+
val isStable = rectangleOnScreen?.let { updateIouHistory(it) } ?: false
|
|
321
|
+
if (isStable) {
|
|
322
|
+
stableCounter = min(stableCounter + 1, detectionCountBeforeCapture)
|
|
323
|
+
Log.d(TAG, "Good rectangle detected, stableCounter: $stableCounter/$detectionCountBeforeCapture")
|
|
324
|
+
} else {
|
|
325
|
+
if (stableCounter > 0) {
|
|
326
|
+
stableCounter--
|
|
327
|
+
}
|
|
328
|
+
Log.d(TAG, "Rectangle unstable (IOU), stableCounter: $stableCounter")
|
|
329
|
+
}
|
|
339
330
|
}
|
|
340
331
|
RectangleQuality.BAD_ANGLE, RectangleQuality.TOO_FAR -> {
|
|
341
332
|
if (stableCounter > 0) {
|
|
342
333
|
stableCounter--
|
|
343
334
|
}
|
|
344
335
|
Log.d(TAG, "Bad rectangle detected (type: $quality), stableCounter: $stableCounter")
|
|
336
|
+
clearIouHistory()
|
|
345
337
|
}
|
|
346
338
|
}
|
|
347
339
|
}
|
|
@@ -357,6 +349,52 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
357
349
|
}
|
|
358
350
|
}
|
|
359
351
|
|
|
352
|
+
private fun updateIouHistory(rectangle: Rectangle): Boolean {
|
|
353
|
+
if (iouHistory.size >= 3) {
|
|
354
|
+
iouHistory.removeFirst()
|
|
355
|
+
}
|
|
356
|
+
iouHistory.addLast(rectangle)
|
|
357
|
+
if (iouHistory.size < 3) {
|
|
358
|
+
return false
|
|
359
|
+
}
|
|
360
|
+
val r0 = iouHistory.elementAt(0)
|
|
361
|
+
val r1 = iouHistory.elementAt(1)
|
|
362
|
+
val r2 = iouHistory.elementAt(2)
|
|
363
|
+
val iou01 = rectangleIou(r0, r1)
|
|
364
|
+
val iou12 = rectangleIou(r1, r2)
|
|
365
|
+
val iou02 = rectangleIou(r0, r2)
|
|
366
|
+
return iou01 >= 0.85 && iou12 >= 0.85 && iou02 >= 0.85
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private fun clearIouHistory() {
|
|
370
|
+
iouHistory.clear()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private fun rectangleIou(a: Rectangle, b: Rectangle): Double {
|
|
374
|
+
fun bounds(r: Rectangle): DoubleArray {
|
|
375
|
+
val minX = min(min(r.topLeft.x, r.topRight.x), min(r.bottomLeft.x, r.bottomRight.x))
|
|
376
|
+
val maxX = max(max(r.topLeft.x, r.topRight.x), max(r.bottomLeft.x, r.bottomRight.x))
|
|
377
|
+
val minY = min(min(r.topLeft.y, r.topRight.y), min(r.bottomLeft.y, r.bottomRight.y))
|
|
378
|
+
val maxY = max(max(r.topLeft.y, r.topRight.y), max(r.bottomLeft.y, r.bottomRight.y))
|
|
379
|
+
return doubleArrayOf(minX, minY, maxX, maxY)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
val ab = bounds(a)
|
|
383
|
+
val bb = bounds(b)
|
|
384
|
+
val interLeft = max(ab[0], bb[0])
|
|
385
|
+
val interTop = max(ab[1], bb[1])
|
|
386
|
+
val interRight = min(ab[2], bb[2])
|
|
387
|
+
val interBottom = min(ab[3], bb[3])
|
|
388
|
+
val interW = max(0.0, interRight - interLeft)
|
|
389
|
+
val interH = max(0.0, interBottom - interTop)
|
|
390
|
+
val interArea = interW * interH
|
|
391
|
+
val areaA = max(0.0, (ab[2] - ab[0])) * max(0.0, (ab[3] - ab[1]))
|
|
392
|
+
val areaB = max(0.0, (bb[2] - bb[0])) * max(0.0, (bb[3] - bb[1]))
|
|
393
|
+
val union = areaA + areaB - interArea
|
|
394
|
+
if (union <= 0.0) return 0.0
|
|
395
|
+
return interArea / union
|
|
396
|
+
}
|
|
397
|
+
|
|
360
398
|
fun capture() {
|
|
361
399
|
captureWithPromise(null)
|
|
362
400
|
}
|
package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerViewManager.kt
CHANGED
|
@@ -94,11 +94,6 @@ 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
|
-
|
|
102
97
|
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
|
|
103
98
|
return MapBuilder.of(
|
|
104
99
|
"onPictureTaken",
|
|
@@ -1,49 +1,24 @@
|
|
|
1
1
|
package com.reactnativerectangledocscanner
|
|
2
2
|
|
|
3
|
-
import android.app.Activity
|
|
4
|
-
import android.content.Intent
|
|
5
3
|
import android.graphics.BitmapFactory
|
|
6
|
-
import android.net.Uri
|
|
7
4
|
import android.util.Log
|
|
8
5
|
import com.facebook.react.bridge.*
|
|
9
6
|
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
|
|
13
7
|
import kotlinx.coroutines.*
|
|
14
8
|
import org.opencv.core.Point
|
|
15
|
-
import java.io.File
|
|
16
|
-
import java.io.FileOutputStream
|
|
17
9
|
|
|
18
10
|
class DocumentScannerModule(reactContext: ReactApplicationContext) :
|
|
19
|
-
ReactContextBaseJavaModule(reactContext)
|
|
11
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
20
12
|
|
|
21
13
|
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
22
14
|
|
|
23
15
|
companion object {
|
|
24
16
|
const val NAME = "RNPdfScannerManager"
|
|
25
17
|
private const val TAG = "DocumentScannerModule"
|
|
26
|
-
private const val EXTERNAL_SCAN_REQUEST = 9401
|
|
27
18
|
}
|
|
28
19
|
|
|
29
20
|
override fun getName() = NAME
|
|
30
21
|
|
|
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
|
-
|
|
47
22
|
/**
|
|
48
23
|
* Capture image from the document scanner view
|
|
49
24
|
* Matches iOS signature: capture(reactTag, resolver, rejecter)
|
|
@@ -71,13 +46,9 @@ class DocumentScannerModule(reactContext: ReactApplicationContext) :
|
|
|
71
46
|
if (view is DocumentScannerView) {
|
|
72
47
|
Log.d(TAG, "Found DocumentScannerView, triggering capture with promise")
|
|
73
48
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
}
|
|
49
|
+
// Pass promise to view so it can be resolved when capture completes
|
|
50
|
+
// This matches iOS behavior where promise is resolved with actual image data
|
|
51
|
+
view.captureWithPromise(promise)
|
|
81
52
|
} else {
|
|
82
53
|
Log.e(TAG, "View with tag $tag is not DocumentScannerView: ${view?.javaClass?.simpleName}")
|
|
83
54
|
promise.reject("INVALID_VIEW", "View is not a DocumentScannerView")
|
|
@@ -93,160 +64,6 @@ class DocumentScannerModule(reactContext: ReactApplicationContext) :
|
|
|
93
64
|
}
|
|
94
65
|
}
|
|
95
66
|
|
|
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
|
-
|
|
250
67
|
/**
|
|
251
68
|
* Apply color controls to an image
|
|
252
69
|
* Matches iOS: applyColorControls(imagePath, brightness, contrast, saturation, resolver, rejecter)
|
package/dist/DocScanner.js
CHANGED
|
@@ -715,28 +715,19 @@ 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: react_native_1.Platform.OS === 'android', detectionConfig: detectionConfig,
|
|
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, 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';
|
|
725
724
|
(0, react_1.useEffect)(() => {
|
|
726
|
-
if (react_native_1.Platform.OS !== 'android'
|
|
725
|
+
if (react_native_1.Platform.OS !== 'android') {
|
|
727
726
|
return;
|
|
728
727
|
}
|
|
729
|
-
|
|
730
|
-
console.log('[DocScanner] Using VisionCamera pipeline');
|
|
731
|
-
}
|
|
732
|
-
else {
|
|
733
|
-
console.warn('[DocScanner] VisionCamera pipeline unavailable, falling back to native view.', {
|
|
734
|
-
hasVisionCameraModule: Boolean(visionCameraModule),
|
|
735
|
-
hasReanimated: Boolean(reanimatedModule),
|
|
736
|
-
});
|
|
737
|
-
}
|
|
728
|
+
console.log('[DocScanner] Using native CameraX pipeline on Android');
|
|
738
729
|
}, []);
|
|
739
|
-
if (
|
|
730
|
+
if (react_native_1.Platform.OS === 'android') {
|
|
740
731
|
return react_1.default.createElement(NativeScanner, { ref: ref, ...props });
|
|
741
732
|
}
|
|
742
733
|
if (hasVisionCamera) {
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -124,7 +124,6 @@ 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';
|
|
128
127
|
const [processing, setProcessing] = (0, react_1.useState)(false);
|
|
129
128
|
const [croppedImageData, setCroppedImageData] = (0, react_1.useState)(null);
|
|
130
129
|
const [isGalleryOpen, setIsGalleryOpen] = (0, react_1.useState)(false);
|
|
@@ -144,11 +143,6 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
144
143
|
const rectangleHintTimeoutRef = (0, react_1.useRef)(null);
|
|
145
144
|
const captureReadyTimeoutRef = (0, react_1.useRef)(null);
|
|
146
145
|
const isBusinessMode = type === 'business';
|
|
147
|
-
(0, react_1.useEffect)(() => {
|
|
148
|
-
if (useExternalScanner) {
|
|
149
|
-
setCaptureReady(true);
|
|
150
|
-
}
|
|
151
|
-
}, [useExternalScanner]);
|
|
152
146
|
const resetScannerView = (0, react_1.useCallback)((options) => {
|
|
153
147
|
setProcessing(false);
|
|
154
148
|
setCroppedImageData(null);
|
|
@@ -394,7 +388,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
394
388
|
return;
|
|
395
389
|
}
|
|
396
390
|
console.log('[FullDocScanner] Starting manual capture, grid detected:', rectangleDetected);
|
|
397
|
-
const captureMode =
|
|
391
|
+
const captureMode = rectangleDetected ? 'grid' : 'no-grid';
|
|
398
392
|
captureModeRef.current = captureMode;
|
|
399
393
|
captureInProgressRef.current = true;
|
|
400
394
|
// Add timeout to reset state if capture hangs
|
|
@@ -542,9 +536,6 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
542
536
|
resetScannerView({ remount: true });
|
|
543
537
|
}, [capturedPhotos.length, isBusinessMode, resetScannerView]);
|
|
544
538
|
const handleRectangleDetect = (0, react_1.useCallback)((event) => {
|
|
545
|
-
if (useExternalScanner) {
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
539
|
const stableCounter = event.stableCounter ?? 0;
|
|
549
540
|
const rectangleCoordinates = event.rectangleOnScreen ?? event.rectangleCoordinates;
|
|
550
541
|
const hasRectangle = Boolean(rectangleCoordinates);
|
|
@@ -598,7 +589,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
598
589
|
}
|
|
599
590
|
setRectangleDetected(false);
|
|
600
591
|
}
|
|
601
|
-
}, [rectangleDetected
|
|
592
|
+
}, [rectangleDetected]);
|
|
602
593
|
(0, react_1.useEffect)(() => () => {
|
|
603
594
|
if (rectangleCaptureTimeoutRef.current) {
|
|
604
595
|
clearTimeout(rectangleCaptureTimeoutRef.current);
|
package/package.json
CHANGED
package/src/DocScanner.tsx
CHANGED
|
@@ -1008,7 +1008,6 @@ const NativeScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
1008
1008
|
useBase64={useBase64}
|
|
1009
1009
|
manualOnly={Platform.OS === 'android'}
|
|
1010
1010
|
detectionConfig={detectionConfig}
|
|
1011
|
-
useExternalScanner={Platform.OS === 'android'}
|
|
1012
1011
|
onPictureTaken={handlePictureTaken}
|
|
1013
1012
|
onError={handleError}
|
|
1014
1013
|
onRectangleDetect={handleRectangleDetect}
|
|
@@ -1032,23 +1031,14 @@ const NativeScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
1032
1031
|
);
|
|
1033
1032
|
|
|
1034
1033
|
export const DocScanner = forwardRef<DocScannerHandle, Props>((props, ref) => {
|
|
1035
|
-
const useExternalScanner = Platform.OS === 'android';
|
|
1036
|
-
|
|
1037
1034
|
useEffect(() => {
|
|
1038
|
-
if (Platform.OS !== 'android'
|
|
1035
|
+
if (Platform.OS !== 'android') {
|
|
1039
1036
|
return;
|
|
1040
1037
|
}
|
|
1041
|
-
|
|
1042
|
-
console.log('[DocScanner] Using VisionCamera pipeline');
|
|
1043
|
-
} else {
|
|
1044
|
-
console.warn('[DocScanner] VisionCamera pipeline unavailable, falling back to native view.', {
|
|
1045
|
-
hasVisionCameraModule: Boolean(visionCameraModule),
|
|
1046
|
-
hasReanimated: Boolean(reanimatedModule),
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1038
|
+
console.log('[DocScanner] Using native CameraX pipeline on Android');
|
|
1049
1039
|
}, []);
|
|
1050
1040
|
|
|
1051
|
-
if (
|
|
1041
|
+
if (Platform.OS === 'android') {
|
|
1052
1042
|
return <NativeScanner ref={ref} {...props} />;
|
|
1053
1043
|
}
|
|
1054
1044
|
|
package/src/FullDocScanner.tsx
CHANGED
|
@@ -187,7 +187,6 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
187
187
|
cropHeight = 1600,
|
|
188
188
|
type,
|
|
189
189
|
}) => {
|
|
190
|
-
const useExternalScanner = Platform.OS === 'android';
|
|
191
190
|
const [processing, setProcessing] = useState(false);
|
|
192
191
|
const [croppedImageData, setCroppedImageData] = useState<PreviewImageData | null>(null);
|
|
193
192
|
const [isGalleryOpen, setIsGalleryOpen] = useState(false);
|
|
@@ -209,12 +208,6 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
209
208
|
|
|
210
209
|
const isBusinessMode = type === 'business';
|
|
211
210
|
|
|
212
|
-
useEffect(() => {
|
|
213
|
-
if (useExternalScanner) {
|
|
214
|
-
setCaptureReady(true);
|
|
215
|
-
}
|
|
216
|
-
}, [useExternalScanner]);
|
|
217
|
-
|
|
218
211
|
const resetScannerView = useCallback(
|
|
219
212
|
(options?: { remount?: boolean }) => {
|
|
220
213
|
setProcessing(false);
|
|
@@ -546,7 +539,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
546
539
|
|
|
547
540
|
console.log('[FullDocScanner] Starting manual capture, grid detected:', rectangleDetected);
|
|
548
541
|
|
|
549
|
-
const captureMode =
|
|
542
|
+
const captureMode = rectangleDetected ? 'grid' : 'no-grid';
|
|
550
543
|
captureModeRef.current = captureMode;
|
|
551
544
|
captureInProgressRef.current = true;
|
|
552
545
|
|
|
@@ -736,9 +729,6 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
736
729
|
}, [capturedPhotos.length, isBusinessMode, resetScannerView]);
|
|
737
730
|
|
|
738
731
|
const handleRectangleDetect = useCallback((event: RectangleDetectEvent) => {
|
|
739
|
-
if (useExternalScanner) {
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
732
|
const stableCounter = event.stableCounter ?? 0;
|
|
743
733
|
const rectangleCoordinates = event.rectangleOnScreen ?? event.rectangleCoordinates;
|
|
744
734
|
const hasRectangle = Boolean(rectangleCoordinates);
|
|
@@ -797,7 +787,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
797
787
|
}
|
|
798
788
|
setRectangleDetected(false);
|
|
799
789
|
}
|
|
800
|
-
}, [rectangleDetected
|
|
790
|
+
}, [rectangleDetected]);
|
|
801
791
|
|
|
802
792
|
useEffect(
|
|
803
793
|
() => () => {
|
package/src/external.d.ts
CHANGED
|
@@ -75,7 +75,6 @@ declare module 'react-native-document-scanner' {
|
|
|
75
75
|
maxAnchorMisses?: number;
|
|
76
76
|
maxCenterDelta?: number;
|
|
77
77
|
};
|
|
78
|
-
useExternalScanner?: boolean;
|
|
79
78
|
onPictureTaken?: (event: DocumentScannerResult) => void;
|
|
80
79
|
onError?: (error: Error) => void;
|
|
81
80
|
onRectangleDetect?: (event: RectangleEventPayload) => void;
|
|
@@ -46,7 +46,6 @@ export interface DocumentScannerProps {
|
|
|
46
46
|
maxAnchorMisses?: number;
|
|
47
47
|
maxCenterDelta?: number;
|
|
48
48
|
};
|
|
49
|
-
useExternalScanner?: boolean;
|
|
50
49
|
onPictureTaken?: (event: DocumentScannerResult) => void;
|
|
51
50
|
onError?: (error: Error) => void;
|
|
52
51
|
onRectangleDetect?: (event: RectangleEventPayload) => void;
|