react-native-rectangle-doc-scanner 3.136.0 → 3.138.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?) -> Unit)? = null
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 buffer = imageProxy.planes[0].buffer
139
- val bytes = ByteArray(buffer.remaining())
140
- buffer.get(bytes)
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
- // Note: Simplified - in production you'd convert ImageProxy to proper format
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.max
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 lastDetectedRectangle: Rectangle? = null
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?.startCamera(useFrontCam, !manualOnly)
89
-
90
- // Start detection loop
91
- startDetectionLoop()
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 startDetectionLoop() {
100
- detectionRunnable?.let { detectionHandler.removeCallbacks(it) }
101
-
102
- detectionRunnable = object : Runnable {
103
- override fun run() {
104
- // Perform detection
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
- // Schedule next detection
108
- detectionHandler.postDelayed(this, detectionRefreshRateInMS.toLong())
109
- }
101
+ val quality = if (rectangle != null) {
102
+ DocumentDetector.evaluateRectangleQuality(rectangle, imageWidth, imageHeight)
103
+ } else {
104
+ RectangleQuality.TOO_FAR
110
105
  }
111
106
 
112
- detectionHandler.post(detectionRunnable!!)
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
- private fun performDetection() {
116
- // In a real implementation, we'd analyze the camera frames
117
- // For now, we'll simulate detection based on capture
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(rectangle: Rectangle?, quality: RectangleQuality) {
122
- lastDetectedRectangle = rectangle
123
- lastDetectionQuality = quality
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(rectangle, overlayColor)
126
+ overlayView.setRectangle(rectangleOnScreen, overlayColor)
127
127
 
128
128
  // Update stable counter based on quality
129
- when (quality) {
130
- RectangleQuality.GOOD -> {
131
- if (rectangle != null) {
132
- stableCounter++
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
- RectangleQuality.BAD_ANGLE, RectangleQuality.TOO_FAR -> {
137
- if (stableCounter > 0) {
138
- stableCounter--
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(rectangle, quality)
150
+ sendRectangleDetectEvent(rectangleOnScreen, rectangleCoordinates, quality, imageWidth, imageHeight)
146
151
 
147
152
  // Auto-capture if threshold reached
148
- if (!manualOnly && stableCounter >= detectionCountBeforeCapture && rectangle != null) {
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(rectangle: Rectangle?, quality: RectangleQuality) {
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", rectangle?.toMap()?.toWritableMap())
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
- cameraController?.startCamera(useFrontCam, !manualOnly)
318
- startDetectionLoop()
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() {
@@ -125,7 +125,8 @@ object ImageProcessor {
125
125
  val dstMat = Mat()
126
126
  srcMat.convertTo(dstMat, -1, contrast.toDouble(), brightness * 255.0)
127
127
 
128
- val resultBitmap = Bitmap.createBitmap(bitmap.width, bitmap.height, bitmap.config)
128
+ val safeConfig = bitmap.config ?: Bitmap.Config.ARGB_8888
129
+ val resultBitmap = Bitmap.createBitmap(bitmap.width, bitmap.height, safeConfig)
129
130
  Utils.matToBitmap(dstMat, resultBitmap)
130
131
 
131
132
  srcMat.release()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.136.0",
3
+ "version": "3.138.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",