react-native-rectangle-doc-scanner 3.148.0 → 3.150.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/src/main/kotlin/com/reactnativerectangledocscanner/CameraController.kt +13 -4
- package/android/src/main/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt +50 -8
- package/android/src/main/kotlin/com/reactnativerectangledocscanner/DocumentScannerViewManager.kt +1 -1
- package/package.json +1 -1
- package/vendor/react-native-document-scanner/index.js +31 -10
|
@@ -8,6 +8,7 @@ import androidx.camera.core.*
|
|
|
8
8
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
9
9
|
import androidx.camera.view.PreviewView
|
|
10
10
|
import androidx.core.content.ContextCompat
|
|
11
|
+
import androidx.lifecycle.Lifecycle
|
|
11
12
|
import androidx.lifecycle.LifecycleOwner
|
|
12
13
|
import java.io.File
|
|
13
14
|
import java.util.concurrent.ExecutorService
|
|
@@ -68,6 +69,13 @@ class CameraController(
|
|
|
68
69
|
private fun bindCameraUseCases(enableDetection: Boolean) {
|
|
69
70
|
val cameraProvider = cameraProvider ?: return
|
|
70
71
|
|
|
72
|
+
// Check lifecycle state
|
|
73
|
+
val lifecycle = lifecycleOwner.lifecycle
|
|
74
|
+
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
|
|
75
|
+
Log.e(TAG, "Cannot bind camera - lifecycle is destroyed")
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
// Select camera
|
|
72
80
|
val cameraSelector = if (useFrontCamera) {
|
|
73
81
|
CameraSelector.DEFAULT_FRONT_CAMERA
|
|
@@ -79,14 +87,12 @@ class CameraController(
|
|
|
79
87
|
val preview = Preview.Builder()
|
|
80
88
|
.setTargetResolution(Size(1080, 1920))
|
|
81
89
|
.build()
|
|
82
|
-
.also {
|
|
83
|
-
it.setSurfaceProvider(previewView.surfaceProvider)
|
|
84
|
-
}
|
|
85
90
|
|
|
86
91
|
// Image capture use case (high resolution for document scanning)
|
|
87
92
|
imageCapture = ImageCapture.Builder()
|
|
88
93
|
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
|
|
89
94
|
.setTargetResolution(Size(1920, 2560))
|
|
95
|
+
.setFlashMode(ImageCapture.FLASH_MODE_AUTO)
|
|
90
96
|
.build()
|
|
91
97
|
|
|
92
98
|
// Image analysis use case for rectangle detection
|
|
@@ -121,12 +127,15 @@ class CameraController(
|
|
|
121
127
|
*useCases.toTypedArray()
|
|
122
128
|
)
|
|
123
129
|
|
|
130
|
+
// Set surface provider AFTER binding to lifecycle
|
|
131
|
+
preview.setSurfaceProvider(previewView.surfaceProvider)
|
|
132
|
+
|
|
124
133
|
// Restore torch state if it was enabled
|
|
125
134
|
if (torchEnabled) {
|
|
126
135
|
setTorchEnabled(true)
|
|
127
136
|
}
|
|
128
137
|
|
|
129
|
-
Log.d(TAG, "Camera started successfully")
|
|
138
|
+
Log.d(TAG, "Camera started successfully, hasFlashUnit: ${camera?.cameraInfo?.hasFlashUnit()}")
|
|
130
139
|
} catch (e: Exception) {
|
|
131
140
|
Log.e(TAG, "Failed to bind camera use cases", e)
|
|
132
141
|
}
|
|
@@ -19,6 +19,7 @@ import com.facebook.react.bridge.WritableMap
|
|
|
19
19
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
20
20
|
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
21
21
|
import kotlinx.coroutines.*
|
|
22
|
+
import kotlinx.coroutines.delay
|
|
22
23
|
import java.io.File
|
|
23
24
|
import kotlin.math.min
|
|
24
25
|
|
|
@@ -48,6 +49,7 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
48
49
|
private var stableCounter = 0
|
|
49
50
|
private var lastDetectionTimestamp: Long = 0L
|
|
50
51
|
private var isCapturing = false
|
|
52
|
+
private var isDetaching = false
|
|
51
53
|
|
|
52
54
|
// Coroutine scope for async operations
|
|
53
55
|
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
@@ -181,6 +183,12 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
181
183
|
return
|
|
182
184
|
}
|
|
183
185
|
|
|
186
|
+
if (isDetaching) {
|
|
187
|
+
Log.d(TAG, "View is detaching, cannot capture")
|
|
188
|
+
promise?.reject("VIEW_DETACHING", "View is being removed")
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
184
192
|
isCapturing = true
|
|
185
193
|
Log.d(TAG, "Capture initiated with promise: ${promise != null}")
|
|
186
194
|
|
|
@@ -193,8 +201,14 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
193
201
|
cameraController?.capturePhoto(
|
|
194
202
|
outputDirectory = outputDir,
|
|
195
203
|
onImageCaptured = { file ->
|
|
196
|
-
|
|
197
|
-
|
|
204
|
+
if (!isDetaching) {
|
|
205
|
+
scope.launch {
|
|
206
|
+
processAndEmitImage(file, promise)
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
Log.d(TAG, "View detaching, skipping image processing")
|
|
210
|
+
isCapturing = false
|
|
211
|
+
promise?.reject("VIEW_DETACHING", "View was removed during capture")
|
|
198
212
|
}
|
|
199
213
|
},
|
|
200
214
|
onError = { exception ->
|
|
@@ -351,17 +365,45 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
351
365
|
}
|
|
352
366
|
|
|
353
367
|
fun stopCamera() {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
lifecycleRegistry.currentState
|
|
368
|
+
if (!isCapturing) {
|
|
369
|
+
cameraController?.stopCamera()
|
|
370
|
+
overlayView.setRectangle(null, overlayColor)
|
|
371
|
+
stableCounter = 0
|
|
372
|
+
if (lifecycleRegistry.currentState != Lifecycle.State.DESTROYED) {
|
|
373
|
+
lifecycleRegistry.currentState = Lifecycle.State.CREATED
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
Log.d(TAG, "Cannot stop camera while capturing")
|
|
359
377
|
}
|
|
360
378
|
}
|
|
361
379
|
|
|
362
380
|
override fun onDetachedFromWindow() {
|
|
363
381
|
super.onDetachedFromWindow()
|
|
364
|
-
|
|
382
|
+
Log.d(TAG, "onDetachedFromWindow called, isCapturing: $isCapturing")
|
|
383
|
+
|
|
384
|
+
// Mark as detaching to prevent new captures
|
|
385
|
+
isDetaching = true
|
|
386
|
+
|
|
387
|
+
// Wait for any ongoing capture to complete before cleaning up
|
|
388
|
+
if (isCapturing) {
|
|
389
|
+
Log.d(TAG, "Waiting for capture to complete before cleanup...")
|
|
390
|
+
// Use a coroutine to wait briefly for capture to complete
|
|
391
|
+
scope.launch {
|
|
392
|
+
var waitCount = 0
|
|
393
|
+
while (isCapturing && waitCount < 20) { // Wait up to 2 seconds
|
|
394
|
+
delay(100)
|
|
395
|
+
waitCount++
|
|
396
|
+
}
|
|
397
|
+
performCleanup()
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
performCleanup()
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private fun performCleanup() {
|
|
405
|
+
Log.d(TAG, "Performing cleanup")
|
|
406
|
+
cameraController?.stopCamera()
|
|
365
407
|
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
|
|
366
408
|
cameraController?.shutdown()
|
|
367
409
|
scope.cancel()
|
package/android/src/main/kotlin/com/reactnativerectangledocscanner/DocumentScannerViewManager.kt
CHANGED
|
@@ -10,7 +10,7 @@ import com.facebook.react.uimanager.annotations.ReactProp
|
|
|
10
10
|
class DocumentScannerViewManager : SimpleViewManager<DocumentScannerView>() {
|
|
11
11
|
|
|
12
12
|
companion object {
|
|
13
|
-
const val REACT_CLASS = "
|
|
13
|
+
const val REACT_CLASS = "RNPdfScanner"
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
override fun getName() = REACT_CLASS
|
package/package.json
CHANGED
|
@@ -79,21 +79,42 @@ class PdfScanner extends React.Component {
|
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
81
|
try {
|
|
82
|
-
const
|
|
82
|
+
const requestedPermissions = [
|
|
83
83
|
PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const isTiramisuOrNewer = Platform.Version >= 33;
|
|
87
|
+
|
|
88
|
+
if (isTiramisuOrNewer) {
|
|
89
|
+
// Android 13+: storage permissions split by media type. Request images access.
|
|
90
|
+
if (PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES) {
|
|
91
|
+
requestedPermissions.push(PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES);
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
if (PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE) {
|
|
95
|
+
requestedPermissions.push(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE);
|
|
96
|
+
}
|
|
97
|
+
if (PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE) {
|
|
98
|
+
requestedPermissions.push(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const granted = await PermissionsAndroid.requestMultiple(requestedPermissions);
|
|
87
103
|
|
|
88
104
|
const cameraGranted =
|
|
89
105
|
granted['android.permission.CAMERA'] ===
|
|
90
106
|
PermissionsAndroid.RESULTS.GRANTED;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
PermissionsAndroid.RESULTS.GRANTED
|
|
94
|
-
|
|
95
|
-
granted['android.permission.
|
|
96
|
-
|
|
107
|
+
|
|
108
|
+
const readGranted = isTiramisuOrNewer
|
|
109
|
+
? granted['android.permission.READ_MEDIA_IMAGES'] === PermissionsAndroid.RESULTS.GRANTED ||
|
|
110
|
+
granted['android.permission.READ_MEDIA_IMAGES'] === undefined
|
|
111
|
+
: granted['android.permission.READ_EXTERNAL_STORAGE'] === PermissionsAndroid.RESULTS.GRANTED ||
|
|
112
|
+
granted['android.permission.READ_EXTERNAL_STORAGE'] === undefined;
|
|
113
|
+
|
|
114
|
+
const writeGranted = isTiramisuOrNewer
|
|
115
|
+
? true // WRITE_EXTERNAL_STORAGE is deprecated on API 33+
|
|
116
|
+
: granted['android.permission.WRITE_EXTERNAL_STORAGE'] === PermissionsAndroid.RESULTS.GRANTED ||
|
|
117
|
+
granted['android.permission.WRITE_EXTERNAL_STORAGE'] === undefined;
|
|
97
118
|
|
|
98
119
|
if (cameraGranted && readGranted && writeGranted) {
|
|
99
120
|
this.setState({ permissionsAuthorized: true });
|