react-native-rectangle-doc-scanner 3.146.0 → 3.148.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/CameraView.kt +31 -4
- package/android/src/main/kotlin/com/reactnativerectangledocscanner/DocumentDetector.kt +1 -0
- package/android/src/main/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt +18 -7
- package/dist/FullDocScanner.js +13 -1
- package/package.json +1 -1
- package/src/FullDocScanner.tsx +14 -1
|
@@ -18,7 +18,10 @@ import androidx.camera.core.Preview
|
|
|
18
18
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
19
19
|
import androidx.camera.view.PreviewView
|
|
20
20
|
import androidx.core.content.ContextCompat
|
|
21
|
+
import androidx.lifecycle.Lifecycle
|
|
21
22
|
import androidx.lifecycle.LifecycleOwner
|
|
23
|
+
import androidx.lifecycle.LifecycleRegistry
|
|
24
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
22
25
|
import com.google.common.util.concurrent.ListenableFuture
|
|
23
26
|
import java.util.concurrent.ExecutorService
|
|
24
27
|
import java.util.concurrent.Executors
|
|
@@ -27,7 +30,7 @@ import java.util.concurrent.Executors
|
|
|
27
30
|
* CameraView with real-time document detection, grid overlay, and rectangle overlay
|
|
28
31
|
* Matches iOS implementation behavior
|
|
29
32
|
*/
|
|
30
|
-
class CameraView(context: Context) : FrameLayout(context) {
|
|
33
|
+
class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
|
|
31
34
|
private val TAG = "CameraView"
|
|
32
35
|
|
|
33
36
|
private val previewView: PreviewView
|
|
@@ -40,9 +43,12 @@ class CameraView(context: Context) : FrameLayout(context) {
|
|
|
40
43
|
private var camera: Camera? = null
|
|
41
44
|
|
|
42
45
|
private val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
46
|
+
private val lifecycleRegistry = LifecycleRegistry(this)
|
|
43
47
|
// Callback for detected rectangles
|
|
44
48
|
var onRectangleDetected: ((Rectangle?) -> Unit)? = null
|
|
45
49
|
|
|
50
|
+
override fun getLifecycle(): Lifecycle = lifecycleRegistry
|
|
51
|
+
|
|
46
52
|
init {
|
|
47
53
|
// Create preview view
|
|
48
54
|
previewView = PreviewView(context).apply {
|
|
@@ -56,6 +62,7 @@ class CameraView(context: Context) : FrameLayout(context) {
|
|
|
56
62
|
addView(overlayView)
|
|
57
63
|
|
|
58
64
|
Log.d(TAG, "CameraView initialized")
|
|
65
|
+
lifecycleRegistry.currentState = Lifecycle.State.CREATED
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
/**
|
|
@@ -83,16 +90,23 @@ class CameraView(context: Context) : FrameLayout(context) {
|
|
|
83
90
|
*/
|
|
84
91
|
fun stopCamera() {
|
|
85
92
|
cameraProvider?.unbindAll()
|
|
86
|
-
|
|
93
|
+
if (lifecycleRegistry.currentState != Lifecycle.State.DESTROYED) {
|
|
94
|
+
lifecycleRegistry.currentState = Lifecycle.State.CREATED
|
|
95
|
+
}
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
/**
|
|
90
99
|
* Bind camera use cases
|
|
91
100
|
*/
|
|
92
101
|
private fun bindCamera() {
|
|
93
|
-
val
|
|
102
|
+
val ctx = context
|
|
103
|
+
val lifecycleOwner = when {
|
|
104
|
+
ctx is LifecycleOwner -> ctx
|
|
105
|
+
ctx is ThemedReactContext -> ctx.currentActivity as? LifecycleOwner ?: ctx as? LifecycleOwner
|
|
106
|
+
else -> null
|
|
107
|
+
}
|
|
94
108
|
if (lifecycleOwner == null) {
|
|
95
|
-
Log.e(TAG, "
|
|
109
|
+
Log.e(TAG, "Unable to resolve LifecycleOwner for CameraView")
|
|
96
110
|
return
|
|
97
111
|
}
|
|
98
112
|
|
|
@@ -132,6 +146,10 @@ class CameraView(context: Context) : FrameLayout(context) {
|
|
|
132
146
|
} catch (e: Exception) {
|
|
133
147
|
Log.e(TAG, "Failed to bind camera", e)
|
|
134
148
|
}
|
|
149
|
+
|
|
150
|
+
if (lifecycleRegistry.currentState != Lifecycle.State.DESTROYED) {
|
|
151
|
+
lifecycleRegistry.currentState = Lifecycle.State.RESUMED
|
|
152
|
+
}
|
|
135
153
|
}
|
|
136
154
|
|
|
137
155
|
/**
|
|
@@ -201,6 +219,15 @@ class CameraView(context: Context) : FrameLayout(context) {
|
|
|
201
219
|
}
|
|
202
220
|
}
|
|
203
221
|
|
|
222
|
+
override fun onDetachedFromWindow() {
|
|
223
|
+
super.onDetachedFromWindow()
|
|
224
|
+
stopCamera()
|
|
225
|
+
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
|
|
226
|
+
if (!cameraExecutor.isShutdown) {
|
|
227
|
+
cameraExecutor.shutdown()
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
204
231
|
/**
|
|
205
232
|
* Overlay view for grid and rectangle
|
|
206
233
|
*/
|
|
@@ -11,7 +11,9 @@ import android.util.Log
|
|
|
11
11
|
import android.view.View
|
|
12
12
|
import android.widget.FrameLayout
|
|
13
13
|
import androidx.camera.view.PreviewView
|
|
14
|
+
import androidx.lifecycle.Lifecycle
|
|
14
15
|
import androidx.lifecycle.LifecycleOwner
|
|
16
|
+
import androidx.lifecycle.LifecycleRegistry
|
|
15
17
|
import com.facebook.react.bridge.Arguments
|
|
16
18
|
import com.facebook.react.bridge.WritableMap
|
|
17
19
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
@@ -20,11 +22,12 @@ import kotlinx.coroutines.*
|
|
|
20
22
|
import java.io.File
|
|
21
23
|
import kotlin.math.min
|
|
22
24
|
|
|
23
|
-
class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context) {
|
|
25
|
+
class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), LifecycleOwner {
|
|
24
26
|
private val themedContext = context
|
|
25
27
|
private val previewView: PreviewView
|
|
26
28
|
private val overlayView: OverlayView
|
|
27
29
|
private var cameraController: CameraController? = null
|
|
30
|
+
private val lifecycleRegistry = LifecycleRegistry(this)
|
|
28
31
|
|
|
29
32
|
// Props (matching iOS)
|
|
30
33
|
var overlayColor: Int = Color.parseColor("#80FFFFFF")
|
|
@@ -53,6 +56,8 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
53
56
|
private const val TAG = "DocumentScannerView"
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
override fun getLifecycle(): Lifecycle = lifecycleRegistry
|
|
60
|
+
|
|
56
61
|
init {
|
|
57
62
|
// Create preview view
|
|
58
63
|
previewView = PreviewView(context).apply {
|
|
@@ -69,20 +74,20 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
69
74
|
post {
|
|
70
75
|
setupCamera()
|
|
71
76
|
}
|
|
77
|
+
|
|
78
|
+
lifecycleRegistry.currentState = Lifecycle.State.CREATED
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
private fun setupCamera() {
|
|
75
82
|
try {
|
|
76
|
-
|
|
77
|
-
Log.e(TAG, "Context is not a LifecycleOwner")
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
cameraController = CameraController(context, lifecycleOwner, previewView)
|
|
83
|
+
cameraController = CameraController(context, this, previewView)
|
|
82
84
|
cameraController?.onFrameAnalyzed = { rectangle, imageWidth, imageHeight ->
|
|
83
85
|
handleDetectionResult(rectangle, imageWidth, imageHeight)
|
|
84
86
|
}
|
|
85
87
|
lastDetectionTimestamp = 0L
|
|
88
|
+
if (lifecycleRegistry.currentState != Lifecycle.State.DESTROYED) {
|
|
89
|
+
lifecycleRegistry.currentState = Lifecycle.State.RESUMED
|
|
90
|
+
}
|
|
86
91
|
cameraController?.startCamera(isUsingFrontCamera, true)
|
|
87
92
|
if (isTorchEnabled) {
|
|
88
93
|
cameraController?.setTorchEnabled(true)
|
|
@@ -91,6 +96,7 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
91
96
|
Log.d(TAG, "Camera setup completed")
|
|
92
97
|
} catch (e: Exception) {
|
|
93
98
|
Log.e(TAG, "Failed to setup camera", e)
|
|
99
|
+
lifecycleRegistry.currentState = Lifecycle.State.CREATED
|
|
94
100
|
}
|
|
95
101
|
}
|
|
96
102
|
|
|
@@ -341,17 +347,22 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
341
347
|
if (isTorchEnabled) {
|
|
342
348
|
cameraController?.setTorchEnabled(true)
|
|
343
349
|
}
|
|
350
|
+
lifecycleRegistry.currentState = Lifecycle.State.RESUMED
|
|
344
351
|
}
|
|
345
352
|
|
|
346
353
|
fun stopCamera() {
|
|
347
354
|
cameraController?.stopCamera()
|
|
348
355
|
overlayView.setRectangle(null, overlayColor)
|
|
349
356
|
stableCounter = 0
|
|
357
|
+
if (lifecycleRegistry.currentState != Lifecycle.State.DESTROYED) {
|
|
358
|
+
lifecycleRegistry.currentState = Lifecycle.State.CREATED
|
|
359
|
+
}
|
|
350
360
|
}
|
|
351
361
|
|
|
352
362
|
override fun onDetachedFromWindow() {
|
|
353
363
|
super.onDetachedFromWindow()
|
|
354
364
|
stopCamera()
|
|
365
|
+
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
|
|
355
366
|
cameraController?.shutdown()
|
|
356
367
|
scope.cancel()
|
|
357
368
|
}
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -46,6 +46,18 @@ const DocScanner_1 = require("./DocScanner");
|
|
|
46
46
|
// 회전은 항상 지원됨 (회전 각도를 반환하고 tdb 앱에서 처리)
|
|
47
47
|
const isImageRotationSupported = () => true;
|
|
48
48
|
const stripFileUri = (value) => value.replace(/^file:\/\//, '');
|
|
49
|
+
const ensureFileUri = (value) => {
|
|
50
|
+
if (!value) {
|
|
51
|
+
return value ?? '';
|
|
52
|
+
}
|
|
53
|
+
if (value.startsWith('file://') || value.startsWith('content://')) {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
if (value.startsWith('/')) {
|
|
57
|
+
return `file://${value}`;
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
};
|
|
49
61
|
const CROPPER_TIMEOUT_MS = 8000;
|
|
50
62
|
const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
|
|
51
63
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -580,7 +592,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
580
592
|
react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC6B0\uB85C 90\u00B0")))) : null,
|
|
581
593
|
isBusinessMode && capturedPhotos.length === 0 && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.captureBackButton, onPress: handleCaptureSecondPhoto, accessibilityLabel: mergedStrings.secondBtn, accessibilityRole: "button" },
|
|
582
594
|
react_1.default.createElement(react_native_1.Text, { style: styles.captureBackButtonText }, mergedStrings.secondBtn))),
|
|
583
|
-
activePreviewImage ? (react_1.default.createElement(react_native_1.Image, { source: { uri: activePreviewImage.path }, style: [
|
|
595
|
+
activePreviewImage ? (react_1.default.createElement(react_native_1.Image, { source: { uri: ensureFileUri(activePreviewImage.path) }, style: [
|
|
584
596
|
styles.previewImage,
|
|
585
597
|
{ transform: [{ rotate: `${rotationDegrees}deg` }] }
|
|
586
598
|
], resizeMode: "contain" })) : null,
|
package/package.json
CHANGED
package/src/FullDocScanner.tsx
CHANGED
|
@@ -27,6 +27,19 @@ const isImageRotationSupported = () => true;
|
|
|
27
27
|
|
|
28
28
|
const stripFileUri = (value: string) => value.replace(/^file:\/\//, '');
|
|
29
29
|
|
|
30
|
+
const ensureFileUri = (value?: string | null) => {
|
|
31
|
+
if (!value) {
|
|
32
|
+
return value ?? '';
|
|
33
|
+
}
|
|
34
|
+
if (value.startsWith('file://') || value.startsWith('content://')) {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
if (value.startsWith('/')) {
|
|
38
|
+
return `file://${value}`;
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
};
|
|
42
|
+
|
|
30
43
|
const CROPPER_TIMEOUT_MS = 8000;
|
|
31
44
|
const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
|
|
32
45
|
|
|
@@ -835,7 +848,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
835
848
|
|
|
836
849
|
{activePreviewImage ? (
|
|
837
850
|
<Image
|
|
838
|
-
source={{ uri: activePreviewImage.path }}
|
|
851
|
+
source={{ uri: ensureFileUri(activePreviewImage.path) }}
|
|
839
852
|
style={[
|
|
840
853
|
styles.previewImage,
|
|
841
854
|
{ transform: [{ rotate: `${rotationDegrees}deg` }] }
|