react-native-rectangle-doc-scanner 3.145.0 → 3.147.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 +41 -15
- 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,22 @@ 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 lifecycleOwner = context
|
|
102
|
+
val lifecycleOwner = when (context) {
|
|
103
|
+
is LifecycleOwner -> context as LifecycleOwner
|
|
104
|
+
is ThemedReactContext -> context.currentActivity as? LifecycleOwner ?: context as? LifecycleOwner
|
|
105
|
+
else -> null
|
|
106
|
+
}
|
|
94
107
|
if (lifecycleOwner == null) {
|
|
95
|
-
Log.e(TAG, "
|
|
108
|
+
Log.e(TAG, "Unable to resolve LifecycleOwner for CameraView")
|
|
96
109
|
return
|
|
97
110
|
}
|
|
98
111
|
|
|
@@ -132,6 +145,10 @@ class CameraView(context: Context) : FrameLayout(context) {
|
|
|
132
145
|
} catch (e: Exception) {
|
|
133
146
|
Log.e(TAG, "Failed to bind camera", e)
|
|
134
147
|
}
|
|
148
|
+
|
|
149
|
+
if (lifecycleRegistry.currentState != Lifecycle.State.DESTROYED) {
|
|
150
|
+
lifecycleRegistry.currentState = Lifecycle.State.RESUMED
|
|
151
|
+
}
|
|
135
152
|
}
|
|
136
153
|
|
|
137
154
|
/**
|
|
@@ -190,17 +207,26 @@ class CameraView(context: Context) : FrameLayout(context) {
|
|
|
190
207
|
onRectangleDetected?.invoke(transformedRectangle)
|
|
191
208
|
}
|
|
192
209
|
} catch (e: Exception) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
} finally {
|
|
199
|
-
imageProxy.close()
|
|
210
|
+
Log.e(TAG, "Failed to analyze frame", e)
|
|
211
|
+
post {
|
|
212
|
+
overlayView.setDetectedRectangle(null)
|
|
213
|
+
onRectangleDetected?.invoke(null)
|
|
200
214
|
}
|
|
215
|
+
} finally {
|
|
216
|
+
imageProxy.close()
|
|
201
217
|
}
|
|
202
218
|
}
|
|
203
219
|
|
|
220
|
+
override fun onDetachedFromWindow() {
|
|
221
|
+
super.onDetachedFromWindow()
|
|
222
|
+
stopCamera()
|
|
223
|
+
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
|
|
224
|
+
if (!cameraExecutor.isShutdown) {
|
|
225
|
+
cameraExecutor.shutdown()
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
204
230
|
/**
|
|
205
231
|
* Overlay view for grid and rectangle
|
|
206
232
|
*/
|
|
@@ -260,10 +286,10 @@ class CameraView(context: Context) : FrameLayout(context) {
|
|
|
260
286
|
|
|
261
287
|
private fun drawRectangle(canvas: Canvas, rect: Rectangle) {
|
|
262
288
|
val path = Path().apply {
|
|
263
|
-
moveTo(rect.topLeft.x, rect.topLeft.y)
|
|
264
|
-
lineTo(rect.topRight.x, rect.topRight.y)
|
|
265
|
-
lineTo(rect.bottomRight.x, rect.bottomRight.y)
|
|
266
|
-
lineTo(rect.bottomLeft.x, rect.bottomLeft.y)
|
|
289
|
+
moveTo(rect.topLeft.x.toFloat(), rect.topLeft.y.toFloat())
|
|
290
|
+
lineTo(rect.topRight.x.toFloat(), rect.topRight.y.toFloat())
|
|
291
|
+
lineTo(rect.bottomRight.x.toFloat(), rect.bottomRight.y.toFloat())
|
|
292
|
+
lineTo(rect.bottomLeft.x.toFloat(), rect.bottomLeft.y.toFloat())
|
|
267
293
|
close()
|
|
268
294
|
}
|
|
269
295
|
|
|
@@ -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` }] }
|