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.
@@ -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
- cameraExecutor.shutdown()
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 as? LifecycleOwner
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, "Context is not a LifecycleOwner")
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
  */
@@ -243,6 +243,7 @@ class DocumentDetector {
243
243
  /**
244
244
  * Transform rectangle coordinates from image space to view space
245
245
  */
246
+ @Suppress("UNUSED_PARAMETER")
246
247
  fun transformRectangleToViewCoordinates(
247
248
  rectangle: Rectangle,
248
249
  imageWidth: Int,
@@ -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
- val lifecycleOwner = context as? LifecycleOwner ?: run {
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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.146.0",
3
+ "version": "3.148.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -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` }] }