react-native-rectangle-doc-scanner 3.135.0 → 3.137.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.
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.reactnativerectangledocscanner
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.graphics.ImageFormat
|
|
4
5
|
import android.util.Log
|
|
5
6
|
import android.util.Size
|
|
6
7
|
import androidx.camera.core.*
|
|
@@ -26,7 +27,7 @@ class CameraController(
|
|
|
26
27
|
private var useFrontCamera = false
|
|
27
28
|
private var torchEnabled = false
|
|
28
29
|
|
|
29
|
-
var onFrameAnalyzed: ((Rectangle
|
|
30
|
+
var onFrameAnalyzed: ((Rectangle?, Int, Int) -> Unit)? = null
|
|
30
31
|
|
|
31
32
|
companion object {
|
|
32
33
|
private const val TAG = "CameraController"
|
|
@@ -93,6 +94,7 @@ class CameraController(
|
|
|
93
94
|
ImageAnalysis.Builder()
|
|
94
95
|
.setTargetResolution(Size(720, 1280))
|
|
95
96
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
97
|
+
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
|
|
96
98
|
.build()
|
|
97
99
|
.also { analysis ->
|
|
98
100
|
analysis.setAnalyzer(cameraExecutor) { imageProxy ->
|
|
@@ -135,20 +137,98 @@ class CameraController(
|
|
|
135
137
|
*/
|
|
136
138
|
private fun analyzeFrame(imageProxy: ImageProxy) {
|
|
137
139
|
try {
|
|
138
|
-
val
|
|
139
|
-
val
|
|
140
|
-
|
|
140
|
+
val rotationDegrees = imageProxy.imageInfo.rotationDegrees
|
|
141
|
+
val frameWidth = if (rotationDegrees == 90 || rotationDegrees == 270) {
|
|
142
|
+
imageProxy.height
|
|
143
|
+
} else {
|
|
144
|
+
imageProxy.width
|
|
145
|
+
}
|
|
146
|
+
val frameHeight = if (rotationDegrees == 90 || rotationDegrees == 270) {
|
|
147
|
+
imageProxy.width
|
|
148
|
+
} else {
|
|
149
|
+
imageProxy.height
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (imageProxy.format != ImageFormat.YUV_420_888 || imageProxy.planes.size < 3) {
|
|
153
|
+
onFrameAnalyzed?.invoke(null, frameWidth, frameHeight)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
val nv21 = imageProxyToNV21(imageProxy)
|
|
158
|
+
val rectangle = DocumentDetector.detectRectangleInYUV(
|
|
159
|
+
nv21,
|
|
160
|
+
imageProxy.width,
|
|
161
|
+
imageProxy.height,
|
|
162
|
+
rotationDegrees
|
|
163
|
+
)
|
|
141
164
|
|
|
142
|
-
|
|
143
|
-
// For now, we'll skip real-time detection in the analyzer and do it on capture
|
|
144
|
-
onFrameAnalyzed?.invoke(null)
|
|
165
|
+
onFrameAnalyzed?.invoke(rectangle, frameWidth, frameHeight)
|
|
145
166
|
} catch (e: Exception) {
|
|
146
167
|
Log.e(TAG, "Error analyzing frame", e)
|
|
168
|
+
val rotationDegrees = imageProxy.imageInfo.rotationDegrees
|
|
169
|
+
val frameWidth = if (rotationDegrees == 90 || rotationDegrees == 270) {
|
|
170
|
+
imageProxy.height
|
|
171
|
+
} else {
|
|
172
|
+
imageProxy.width
|
|
173
|
+
}
|
|
174
|
+
val frameHeight = if (rotationDegrees == 90 || rotationDegrees == 270) {
|
|
175
|
+
imageProxy.width
|
|
176
|
+
} else {
|
|
177
|
+
imageProxy.height
|
|
178
|
+
}
|
|
179
|
+
onFrameAnalyzed?.invoke(null, frameWidth, frameHeight)
|
|
147
180
|
} finally {
|
|
148
181
|
imageProxy.close()
|
|
149
182
|
}
|
|
150
183
|
}
|
|
151
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Convert ImageProxy (YUV_420_888) to NV21 byte array
|
|
187
|
+
*/
|
|
188
|
+
private fun imageProxyToNV21(image: ImageProxy): ByteArray {
|
|
189
|
+
val width = image.width
|
|
190
|
+
val height = image.height
|
|
191
|
+
|
|
192
|
+
val ySize = width * height
|
|
193
|
+
val uvSize = width * height / 2
|
|
194
|
+
val nv21 = ByteArray(ySize + uvSize)
|
|
195
|
+
|
|
196
|
+
val yBuffer = image.planes[0].buffer
|
|
197
|
+
val uBuffer = image.planes[1].buffer
|
|
198
|
+
val vBuffer = image.planes[2].buffer
|
|
199
|
+
|
|
200
|
+
val yRowStride = image.planes[0].rowStride
|
|
201
|
+
val yPixelStride = image.planes[0].pixelStride
|
|
202
|
+
var outputOffset = 0
|
|
203
|
+
for (row in 0 until height) {
|
|
204
|
+
var inputOffset = row * yRowStride
|
|
205
|
+
for (col in 0 until width) {
|
|
206
|
+
nv21[outputOffset++] = yBuffer.get(inputOffset)
|
|
207
|
+
inputOffset += yPixelStride
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
val uvRowStride = image.planes[1].rowStride
|
|
212
|
+
val uvPixelStride = image.planes[1].pixelStride
|
|
213
|
+
val vRowStride = image.planes[2].rowStride
|
|
214
|
+
val vPixelStride = image.planes[2].pixelStride
|
|
215
|
+
|
|
216
|
+
val uvHeight = height / 2
|
|
217
|
+
val uvWidth = width / 2
|
|
218
|
+
for (row in 0 until uvHeight) {
|
|
219
|
+
var uInputOffset = row * uvRowStride
|
|
220
|
+
var vInputOffset = row * vRowStride
|
|
221
|
+
for (col in 0 until uvWidth) {
|
|
222
|
+
nv21[outputOffset++] = vBuffer.get(vInputOffset)
|
|
223
|
+
nv21[outputOffset++] = uBuffer.get(uInputOffset)
|
|
224
|
+
uInputOffset += uvPixelStride
|
|
225
|
+
vInputOffset += vPixelStride
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return nv21
|
|
230
|
+
}
|
|
231
|
+
|
|
152
232
|
/**
|
|
153
233
|
* Capture photo
|
|
154
234
|
*/
|
|
@@ -7,9 +7,6 @@ import android.graphics.Color
|
|
|
7
7
|
import android.graphics.Paint
|
|
8
8
|
import android.graphics.PorterDuff
|
|
9
9
|
import android.graphics.PorterDuffXfermode
|
|
10
|
-
import android.os.Handler
|
|
11
|
-
import android.os.Looper
|
|
12
|
-
import android.util.Base64
|
|
13
10
|
import android.util.Log
|
|
14
11
|
import android.view.View
|
|
15
12
|
import android.widget.FrameLayout
|
|
@@ -21,7 +18,7 @@ import com.facebook.react.uimanager.ThemedReactContext
|
|
|
21
18
|
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
22
19
|
import kotlinx.coroutines.*
|
|
23
20
|
import java.io.File
|
|
24
|
-
import kotlin.math.
|
|
21
|
+
import kotlin.math.min
|
|
25
22
|
|
|
26
23
|
class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context) {
|
|
27
24
|
private val themedContext = context
|
|
@@ -46,10 +43,7 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
46
43
|
|
|
47
44
|
// State
|
|
48
45
|
private var stableCounter = 0
|
|
49
|
-
private var
|
|
50
|
-
private var lastDetectionQuality: RectangleQuality = RectangleQuality.TOO_FAR
|
|
51
|
-
private val detectionHandler = Handler(Looper.getMainLooper())
|
|
52
|
-
private var detectionRunnable: Runnable? = null
|
|
46
|
+
private var lastDetectionTimestamp: Long = 0L
|
|
53
47
|
private var isCapturing = false
|
|
54
48
|
|
|
55
49
|
// Coroutine scope for async operations
|
|
@@ -85,10 +79,11 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
85
79
|
}
|
|
86
80
|
|
|
87
81
|
cameraController = CameraController(context, lifecycleOwner, previewView)
|
|
88
|
-
cameraController?.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
82
|
+
cameraController?.onFrameAnalyzed = { rectangle, imageWidth, imageHeight ->
|
|
83
|
+
handleDetectionResult(rectangle, imageWidth, imageHeight)
|
|
84
|
+
}
|
|
85
|
+
lastDetectionTimestamp = 0L
|
|
86
|
+
cameraController?.startCamera(useFrontCam, true)
|
|
92
87
|
|
|
93
88
|
Log.d(TAG, "Camera setup completed")
|
|
94
89
|
} catch (e: Exception) {
|
|
@@ -96,56 +91,66 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
96
91
|
}
|
|
97
92
|
}
|
|
98
93
|
|
|
99
|
-
private fun
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
performDetection()
|
|
94
|
+
private fun handleDetectionResult(rectangle: Rectangle?, imageWidth: Int, imageHeight: Int) {
|
|
95
|
+
val now = System.currentTimeMillis()
|
|
96
|
+
if (now - lastDetectionTimestamp < detectionRefreshRateInMS) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
lastDetectionTimestamp = now
|
|
106
100
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
val quality = if (rectangle != null) {
|
|
102
|
+
DocumentDetector.evaluateRectangleQuality(rectangle, imageWidth, imageHeight)
|
|
103
|
+
} else {
|
|
104
|
+
RectangleQuality.TOO_FAR
|
|
110
105
|
}
|
|
111
106
|
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
val rectangleOnScreen = if (rectangle != null && width > 0 && height > 0) {
|
|
108
|
+
DocumentDetector.transformRectangleToViewCoordinates(rectangle, imageWidth, imageHeight, width, height)
|
|
109
|
+
} else {
|
|
110
|
+
null
|
|
111
|
+
}
|
|
114
112
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// The actual detection happens during capture in this simplified version
|
|
113
|
+
post {
|
|
114
|
+
onRectangleDetected(rectangleOnScreen, rectangle, quality, imageWidth, imageHeight)
|
|
115
|
+
}
|
|
119
116
|
}
|
|
120
117
|
|
|
121
|
-
private fun onRectangleDetected(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
private fun onRectangleDetected(
|
|
119
|
+
rectangleOnScreen: Rectangle?,
|
|
120
|
+
rectangleCoordinates: Rectangle?,
|
|
121
|
+
quality: RectangleQuality,
|
|
122
|
+
imageWidth: Int,
|
|
123
|
+
imageHeight: Int
|
|
124
|
+
) {
|
|
125
125
|
// Update overlay
|
|
126
|
-
overlayView.setRectangle(
|
|
126
|
+
overlayView.setRectangle(rectangleOnScreen, overlayColor)
|
|
127
127
|
|
|
128
128
|
// Update stable counter based on quality
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
if (rectangleCoordinates == null) {
|
|
130
|
+
if (stableCounter != 0) {
|
|
131
|
+
Log.d(TAG, "Rectangle lost, resetting stableCounter")
|
|
132
|
+
}
|
|
133
|
+
stableCounter = 0
|
|
134
|
+
} else {
|
|
135
|
+
when (quality) {
|
|
136
|
+
RectangleQuality.GOOD -> {
|
|
137
|
+
stableCounter = min(stableCounter + 1, detectionCountBeforeCapture)
|
|
133
138
|
Log.d(TAG, "Good rectangle detected, stableCounter: $stableCounter/$detectionCountBeforeCapture")
|
|
134
139
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
RectangleQuality.BAD_ANGLE, RectangleQuality.TOO_FAR -> {
|
|
141
|
+
if (stableCounter > 0) {
|
|
142
|
+
stableCounter--
|
|
143
|
+
}
|
|
144
|
+
Log.d(TAG, "Bad rectangle detected (type: $quality), stableCounter: $stableCounter")
|
|
139
145
|
}
|
|
140
|
-
Log.d(TAG, "Bad rectangle detected (type: $quality), stableCounter: $stableCounter")
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
// Send event to JavaScript
|
|
145
|
-
sendRectangleDetectEvent(
|
|
150
|
+
sendRectangleDetectEvent(rectangleOnScreen, rectangleCoordinates, quality, imageWidth, imageHeight)
|
|
146
151
|
|
|
147
152
|
// Auto-capture if threshold reached
|
|
148
|
-
if (!manualOnly &&
|
|
153
|
+
if (!manualOnly && rectangleCoordinates != null && stableCounter >= detectionCountBeforeCapture) {
|
|
149
154
|
Log.d(TAG, "Auto-capture triggered! stableCounter: $stableCounter >= threshold: $detectionCountBeforeCapture")
|
|
150
155
|
stableCounter = 0
|
|
151
156
|
capture()
|
|
@@ -278,15 +283,26 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
278
283
|
.receiveEvent(id, "onPictureTaken", event)
|
|
279
284
|
}
|
|
280
285
|
|
|
281
|
-
private fun sendRectangleDetectEvent(
|
|
286
|
+
private fun sendRectangleDetectEvent(
|
|
287
|
+
rectangleOnScreen: Rectangle?,
|
|
288
|
+
rectangleCoordinates: Rectangle?,
|
|
289
|
+
quality: RectangleQuality,
|
|
290
|
+
imageWidth: Int,
|
|
291
|
+
imageHeight: Int
|
|
292
|
+
) {
|
|
282
293
|
val event = Arguments.createMap().apply {
|
|
283
294
|
putInt("stableCounter", stableCounter)
|
|
284
295
|
putInt("lastDetectionType", quality.ordinal)
|
|
285
|
-
putMap("rectangleCoordinates",
|
|
296
|
+
putMap("rectangleCoordinates", rectangleCoordinates?.toMap()?.toWritableMap())
|
|
297
|
+
putMap("rectangleOnScreen", rectangleOnScreen?.toMap()?.toWritableMap())
|
|
286
298
|
putMap("previewSize", Arguments.createMap().apply {
|
|
287
299
|
putInt("width", width)
|
|
288
300
|
putInt("height", height)
|
|
289
301
|
})
|
|
302
|
+
putMap("imageSize", Arguments.createMap().apply {
|
|
303
|
+
putInt("width", imageWidth)
|
|
304
|
+
putInt("height", imageHeight)
|
|
305
|
+
})
|
|
290
306
|
}
|
|
291
307
|
themedContext.getJSModule(RCTEventEmitter::class.java)
|
|
292
308
|
.receiveEvent(id, "onRectangleDetect", event)
|
|
@@ -314,13 +330,17 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
314
330
|
}
|
|
315
331
|
|
|
316
332
|
fun startCamera() {
|
|
317
|
-
|
|
318
|
-
|
|
333
|
+
lastDetectionTimestamp = 0L
|
|
334
|
+
cameraController?.onFrameAnalyzed = { rectangle, imageWidth, imageHeight ->
|
|
335
|
+
handleDetectionResult(rectangle, imageWidth, imageHeight)
|
|
336
|
+
}
|
|
337
|
+
cameraController?.startCamera(useFrontCam, true)
|
|
319
338
|
}
|
|
320
339
|
|
|
321
340
|
fun stopCamera() {
|
|
322
|
-
detectionRunnable?.let { detectionHandler.removeCallbacks(it) }
|
|
323
341
|
cameraController?.stopCamera()
|
|
342
|
+
overlayView.setRectangle(null, overlayColor)
|
|
343
|
+
stableCounter = 0
|
|
324
344
|
}
|
|
325
345
|
|
|
326
346
|
override fun onDetachedFromWindow() {
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -550,13 +550,6 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
550
550
|
}
|
|
551
551
|
}, []);
|
|
552
552
|
const activePreviewImage = croppedImageData ? getActivePreviewImage(croppedImageData) : null;
|
|
553
|
-
// Android: 카메라 컴포넌트가 없으므로 자동으로 갤러리 열기
|
|
554
|
-
(0, react_1.useEffect)(() => {
|
|
555
|
-
if (react_native_1.Platform.OS === 'android' && !croppedImageData && !isGalleryOpen) {
|
|
556
|
-
console.log('[FullDocScanner] Android detected - opening gallery automatically');
|
|
557
|
-
handleGalleryPick();
|
|
558
|
-
}
|
|
559
|
-
}, []);
|
|
560
553
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
561
554
|
croppedImageData ? (
|
|
562
555
|
// check_DP: Show confirmation screen
|
package/package.json
CHANGED
package/src/FullDocScanner.tsx
CHANGED
|
@@ -751,14 +751,6 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
751
751
|
|
|
752
752
|
const activePreviewImage = croppedImageData ? getActivePreviewImage(croppedImageData) : null;
|
|
753
753
|
|
|
754
|
-
// Android: 카메라 컴포넌트가 없으므로 자동으로 갤러리 열기
|
|
755
|
-
useEffect(() => {
|
|
756
|
-
if (Platform.OS === 'android' && !croppedImageData && !isGalleryOpen) {
|
|
757
|
-
console.log('[FullDocScanner] Android detected - opening gallery automatically');
|
|
758
|
-
handleGalleryPick();
|
|
759
|
-
}
|
|
760
|
-
}, []);
|
|
761
|
-
|
|
762
754
|
return (
|
|
763
755
|
<View style={styles.container}>
|
|
764
756
|
{croppedImageData ? (
|