react-native-rectangle-doc-scanner 3.130.0 → 3.132.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.
@@ -0,0 +1,16 @@
1
+ package com.reactnativerectangledocscanner
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class DocumentScannerPackage : ReactPackage {
9
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
10
+ return listOf(DocumentScannerModule(reactContext))
11
+ }
12
+
13
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
14
+ return listOf(DocumentScannerViewManager())
15
+ }
16
+ }
@@ -0,0 +1,388 @@
1
+ package com.reactnativerectangledocscanner
2
+
3
+ import android.content.Context
4
+ import android.graphics.BitmapFactory
5
+ import android.graphics.Canvas
6
+ import android.graphics.Color
7
+ import android.graphics.Paint
8
+ import android.graphics.PorterDuff
9
+ import android.graphics.PorterDuffXfermode
10
+ import android.os.Handler
11
+ import android.os.Looper
12
+ import android.util.Base64
13
+ import android.util.Log
14
+ import android.view.View
15
+ import android.widget.FrameLayout
16
+ import androidx.camera.view.PreviewView
17
+ import androidx.lifecycle.LifecycleOwner
18
+ import com.facebook.react.bridge.Arguments
19
+ import com.facebook.react.bridge.WritableMap
20
+ import com.facebook.react.uimanager.ThemedReactContext
21
+ import com.facebook.react.uimanager.events.RCTEventEmitter
22
+ import kotlinx.coroutines.*
23
+ import java.io.File
24
+ import kotlin.math.max
25
+
26
+ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context) {
27
+ private val themedContext = context
28
+ private val previewView: PreviewView
29
+ private val overlayView: OverlayView
30
+ private var cameraController: CameraController? = null
31
+
32
+ // Props (matching iOS)
33
+ var overlayColor: Int = Color.parseColor("#80FFFFFF")
34
+ var enableTorch: Boolean = false
35
+ var useFrontCam: Boolean = false
36
+ var useBase64: Boolean = false
37
+ var saveInAppDocument: Boolean = false
38
+ var captureMultiple: Boolean = false
39
+ var manualOnly: Boolean = false
40
+ var detectionCountBeforeCapture: Int = 15
41
+ var detectionRefreshRateInMS: Int = 100
42
+ var quality: Float = 0.95f
43
+ var brightness: Float = 0f
44
+ var contrast: Float = 1f
45
+ var saturation: Float = 1f
46
+
47
+ // State
48
+ 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
53
+ private var isCapturing = false
54
+
55
+ // Coroutine scope for async operations
56
+ private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
57
+
58
+ companion object {
59
+ private const val TAG = "DocumentScannerView"
60
+ }
61
+
62
+ init {
63
+ // Create preview view
64
+ previewView = PreviewView(context).apply {
65
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
66
+ scaleType = PreviewView.ScaleType.FILL_CENTER
67
+ }
68
+ addView(previewView)
69
+
70
+ // Create overlay view for drawing rectangle
71
+ overlayView = OverlayView(context)
72
+ addView(overlayView)
73
+
74
+ // Setup camera
75
+ post {
76
+ setupCamera()
77
+ }
78
+ }
79
+
80
+ private fun setupCamera() {
81
+ try {
82
+ val lifecycleOwner = context as? LifecycleOwner ?: run {
83
+ Log.e(TAG, "Context is not a LifecycleOwner")
84
+ return
85
+ }
86
+
87
+ cameraController = CameraController(context, lifecycleOwner, previewView)
88
+ cameraController?.startCamera(useFrontCam, !manualOnly)
89
+
90
+ // Start detection loop
91
+ startDetectionLoop()
92
+
93
+ Log.d(TAG, "Camera setup completed")
94
+ } catch (e: Exception) {
95
+ Log.e(TAG, "Failed to setup camera", e)
96
+ }
97
+ }
98
+
99
+ private fun startDetectionLoop() {
100
+ detectionRunnable?.let { detectionHandler.removeCallbacks(it) }
101
+
102
+ detectionRunnable = object : Runnable {
103
+ override fun run() {
104
+ // Perform detection
105
+ performDetection()
106
+
107
+ // Schedule next detection
108
+ detectionHandler.postDelayed(this, detectionRefreshRateInMS.toLong())
109
+ }
110
+ }
111
+
112
+ detectionHandler.post(detectionRunnable!!)
113
+ }
114
+
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
119
+ }
120
+
121
+ private fun onRectangleDetected(rectangle: Rectangle?, quality: RectangleQuality) {
122
+ lastDetectedRectangle = rectangle
123
+ lastDetectionQuality = quality
124
+
125
+ // Update overlay
126
+ overlayView.setRectangle(rectangle, overlayColor)
127
+
128
+ // Update stable counter based on quality
129
+ when (quality) {
130
+ RectangleQuality.GOOD -> {
131
+ if (rectangle != null) {
132
+ stableCounter++
133
+ Log.d(TAG, "Good rectangle detected, stableCounter: $stableCounter/$detectionCountBeforeCapture")
134
+ }
135
+ }
136
+ RectangleQuality.BAD_ANGLE, RectangleQuality.TOO_FAR -> {
137
+ if (stableCounter > 0) {
138
+ stableCounter--
139
+ }
140
+ Log.d(TAG, "Bad rectangle detected (type: $quality), stableCounter: $stableCounter")
141
+ }
142
+ }
143
+
144
+ // Send event to JavaScript
145
+ sendRectangleDetectEvent(rectangle, quality)
146
+
147
+ // Auto-capture if threshold reached
148
+ if (!manualOnly && stableCounter >= detectionCountBeforeCapture && rectangle != null) {
149
+ Log.d(TAG, "Auto-capture triggered! stableCounter: $stableCounter >= threshold: $detectionCountBeforeCapture")
150
+ stableCounter = 0
151
+ capture()
152
+ }
153
+ }
154
+
155
+ fun capture() {
156
+ captureWithPromise(null)
157
+ }
158
+
159
+ /**
160
+ * Capture image with promise support (matches iOS captureWithResolver:rejecter:)
161
+ * @param promise Optional promise to resolve with capture result
162
+ */
163
+ fun captureWithPromise(promise: com.facebook.react.bridge.Promise?) {
164
+ if (isCapturing) {
165
+ Log.d(TAG, "Already capturing, ignoring request")
166
+ promise?.reject("CAPTURE_IN_PROGRESS", "Capture already in progress")
167
+ return
168
+ }
169
+
170
+ isCapturing = true
171
+ Log.d(TAG, "Capture initiated with promise: ${promise != null}")
172
+
173
+ val outputDir = if (saveInAppDocument) {
174
+ context.filesDir
175
+ } else {
176
+ context.cacheDir
177
+ }
178
+
179
+ cameraController?.capturePhoto(
180
+ outputDirectory = outputDir,
181
+ onImageCaptured = { file ->
182
+ scope.launch {
183
+ processAndEmitImage(file, promise)
184
+ }
185
+ },
186
+ onError = { exception ->
187
+ Log.e(TAG, "Capture failed", exception)
188
+ isCapturing = false
189
+
190
+ // Reject promise if provided
191
+ promise?.reject("CAPTURE_FAILED", "Failed to capture image", exception)
192
+
193
+ // Also send event for backwards compatibility
194
+ sendErrorEvent("capture_failed")
195
+ }
196
+ )
197
+ }
198
+
199
+ private suspend fun processAndEmitImage(imageFile: File, promise: com.facebook.react.bridge.Promise? = null) = withContext(Dispatchers.IO) {
200
+ try {
201
+ // Detect rectangle in captured image
202
+ val bitmap = BitmapFactory.decodeFile(imageFile.absolutePath)
203
+ val detectedRectangle = DocumentDetector.detectRectangle(bitmap)
204
+
205
+ // Process image with detected rectangle
206
+ val shouldCrop = detectedRectangle != null && stableCounter > 0
207
+ val processed = ImageProcessor.processImage(
208
+ imagePath = imageFile.absolutePath,
209
+ rectangle = detectedRectangle,
210
+ brightness = brightness,
211
+ contrast = contrast,
212
+ saturation = saturation,
213
+ shouldCrop = shouldCrop
214
+ )
215
+
216
+ // Save or encode images
217
+ val result = if (useBase64) {
218
+ Arguments.createMap().apply {
219
+ putString("croppedImage", ImageProcessor.bitmapToBase64(processed.croppedImage, quality))
220
+ putString("initialImage", ImageProcessor.bitmapToBase64(processed.initialImage, quality))
221
+ putMap("rectangleCoordinates", detectedRectangle?.toMap()?.toWritableMap())
222
+ }
223
+ } else {
224
+ val timestamp = System.currentTimeMillis()
225
+ val croppedPath = ImageProcessor.saveBitmapToFile(
226
+ processed.croppedImage,
227
+ if (saveInAppDocument) context.filesDir else context.cacheDir,
228
+ "cropped_img_$timestamp.jpeg",
229
+ quality
230
+ )
231
+ val initialPath = ImageProcessor.saveBitmapToFile(
232
+ processed.initialImage,
233
+ if (saveInAppDocument) context.filesDir else context.cacheDir,
234
+ "initial_img_$timestamp.jpeg",
235
+ quality
236
+ )
237
+
238
+ Arguments.createMap().apply {
239
+ putString("croppedImage", croppedPath)
240
+ putString("initialImage", initialPath)
241
+ putMap("rectangleCoordinates", detectedRectangle?.toMap()?.toWritableMap())
242
+ }
243
+ }
244
+
245
+ withContext(Dispatchers.Main) {
246
+ Log.d(TAG, "Processing completed, resolving promise: ${promise != null}")
247
+
248
+ // Resolve promise first (if provided) - matches iOS behavior
249
+ promise?.resolve(result)
250
+
251
+ // Then send event for backwards compatibility
252
+ sendPictureTakenEvent(result)
253
+
254
+ isCapturing = false
255
+
256
+ if (!captureMultiple) {
257
+ stopCamera()
258
+ }
259
+ }
260
+ } catch (e: Exception) {
261
+ Log.e(TAG, "Failed to process image", e)
262
+ withContext(Dispatchers.Main) {
263
+ // Reject promise if provided
264
+ promise?.reject("PROCESSING_FAILED", "Failed to process image: ${e.message}", e)
265
+
266
+ // Also send error event for backwards compatibility
267
+ sendErrorEvent("processing_failed")
268
+ isCapturing = false
269
+ }
270
+ }
271
+ }
272
+
273
+ private fun sendPictureTakenEvent(data: WritableMap) {
274
+ val event = Arguments.createMap().apply {
275
+ merge(data)
276
+ }
277
+ themedContext.getJSModule(RCTEventEmitter::class.java)
278
+ .receiveEvent(id, "onPictureTaken", event)
279
+ }
280
+
281
+ private fun sendRectangleDetectEvent(rectangle: Rectangle?, quality: RectangleQuality) {
282
+ val event = Arguments.createMap().apply {
283
+ putInt("stableCounter", stableCounter)
284
+ putInt("lastDetectionType", quality.ordinal)
285
+ putMap("rectangleCoordinates", rectangle?.toMap()?.toWritableMap())
286
+ putMap("previewSize", Arguments.createMap().apply {
287
+ putInt("width", width)
288
+ putInt("height", height)
289
+ })
290
+ }
291
+ themedContext.getJSModule(RCTEventEmitter::class.java)
292
+ .receiveEvent(id, "onRectangleDetect", event)
293
+ }
294
+
295
+ private fun sendErrorEvent(error: String) {
296
+ val event = Arguments.createMap().apply {
297
+ putString("error", error)
298
+ }
299
+ themedContext.getJSModule(RCTEventEmitter::class.java)
300
+ .receiveEvent(id, "onPictureTaken", event)
301
+ }
302
+
303
+ fun setEnableTorch(enabled: Boolean) {
304
+ this.enableTorch = enabled
305
+ cameraController?.setTorchEnabled(enabled)
306
+ }
307
+
308
+ fun setUseFrontCam(enabled: Boolean) {
309
+ if (this.useFrontCam != enabled) {
310
+ this.useFrontCam = enabled
311
+ cameraController?.stopCamera()
312
+ setupCamera()
313
+ }
314
+ }
315
+
316
+ fun startCamera() {
317
+ cameraController?.startCamera(useFrontCam, !manualOnly)
318
+ startDetectionLoop()
319
+ }
320
+
321
+ fun stopCamera() {
322
+ detectionRunnable?.let { detectionHandler.removeCallbacks(it) }
323
+ cameraController?.stopCamera()
324
+ }
325
+
326
+ override fun onDetachedFromWindow() {
327
+ super.onDetachedFromWindow()
328
+ stopCamera()
329
+ cameraController?.shutdown()
330
+ scope.cancel()
331
+ }
332
+
333
+ /**
334
+ * Overlay view for drawing detected rectangle
335
+ */
336
+ private class OverlayView(context: Context) : View(context) {
337
+ private var rectangle: Rectangle? = null
338
+ private var overlayColor: Int = Color.parseColor("#80FFFFFF")
339
+ private val paint = Paint().apply {
340
+ style = Paint.Style.FILL
341
+ xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
342
+ }
343
+
344
+ fun setRectangle(rect: Rectangle?, color: Int) {
345
+ this.rectangle = rect
346
+ this.overlayColor = color
347
+ invalidate()
348
+ }
349
+
350
+ override fun onDraw(canvas: Canvas) {
351
+ super.onDraw(canvas)
352
+
353
+ rectangle?.let { rect ->
354
+ paint.color = overlayColor
355
+
356
+ // Draw the rectangle overlay (simplified - just a filled polygon)
357
+ val path = android.graphics.Path().apply {
358
+ moveTo(rect.topLeft.x.toFloat(), rect.topLeft.y.toFloat())
359
+ lineTo(rect.topRight.x.toFloat(), rect.topRight.y.toFloat())
360
+ lineTo(rect.bottomRight.x.toFloat(), rect.bottomRight.y.toFloat())
361
+ lineTo(rect.bottomLeft.x.toFloat(), rect.bottomLeft.y.toFloat())
362
+ close()
363
+ }
364
+
365
+ canvas.drawPath(path, paint)
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Extension function to convert Map to WritableMap
373
+ */
374
+ private fun Map<String, Any?>.toWritableMap(): WritableMap {
375
+ val map = Arguments.createMap()
376
+ forEach { (key, value) ->
377
+ when (value) {
378
+ null -> map.putNull(key)
379
+ is Boolean -> map.putBoolean(key, value)
380
+ is Double -> map.putDouble(key, value)
381
+ is Int -> map.putInt(key, value)
382
+ is String -> map.putString(key, value)
383
+ is Map<*, *> -> map.putMap(key, (value as Map<String, Any?>).toWritableMap())
384
+ else -> Log.w("DocumentScannerView", "Unknown type for key $key: ${value::class.java}")
385
+ }
386
+ }
387
+ return map
388
+ }
@@ -0,0 +1,102 @@
1
+ package com.reactnativerectangledocscanner
2
+
3
+ import android.graphics.Color
4
+ import com.facebook.react.bridge.ReadableArray
5
+ import com.facebook.react.common.MapBuilder
6
+ import com.facebook.react.uimanager.SimpleViewManager
7
+ import com.facebook.react.uimanager.ThemedReactContext
8
+ import com.facebook.react.uimanager.annotations.ReactProp
9
+
10
+ class DocumentScannerViewManager : SimpleViewManager<DocumentScannerView>() {
11
+
12
+ companion object {
13
+ const val REACT_CLASS = "RNPdfScannerManager"
14
+ }
15
+
16
+ override fun getName() = REACT_CLASS
17
+
18
+ override fun createViewInstance(reactContext: ThemedReactContext): DocumentScannerView {
19
+ return DocumentScannerView(reactContext)
20
+ }
21
+
22
+ @ReactProp(name = "overlayColor", customType = "Color")
23
+ fun setOverlayColor(view: DocumentScannerView, color: Int?) {
24
+ color?.let {
25
+ view.overlayColor = it
26
+ }
27
+ }
28
+
29
+ @ReactProp(name = "enableTorch")
30
+ fun setEnableTorch(view: DocumentScannerView, enabled: Boolean) {
31
+ view.setEnableTorch(enabled)
32
+ }
33
+
34
+ @ReactProp(name = "useFrontCam")
35
+ fun setUseFrontCam(view: DocumentScannerView, enabled: Boolean) {
36
+ view.setUseFrontCam(enabled)
37
+ }
38
+
39
+ @ReactProp(name = "useBase64")
40
+ fun setUseBase64(view: DocumentScannerView, enabled: Boolean) {
41
+ view.useBase64 = enabled
42
+ }
43
+
44
+ @ReactProp(name = "saveInAppDocument")
45
+ fun setSaveInAppDocument(view: DocumentScannerView, enabled: Boolean) {
46
+ view.saveInAppDocument = enabled
47
+ }
48
+
49
+ @ReactProp(name = "captureMultiple")
50
+ fun setCaptureMultiple(view: DocumentScannerView, enabled: Boolean) {
51
+ view.captureMultiple = enabled
52
+ }
53
+
54
+ @ReactProp(name = "manualOnly")
55
+ fun setManualOnly(view: DocumentScannerView, enabled: Boolean) {
56
+ view.manualOnly = enabled
57
+ }
58
+
59
+ @ReactProp(name = "detectionCountBeforeCapture")
60
+ fun setDetectionCountBeforeCapture(view: DocumentScannerView, count: Int) {
61
+ view.detectionCountBeforeCapture = count
62
+ }
63
+
64
+ @ReactProp(name = "detectionRefreshRateInMS")
65
+ fun setDetectionRefreshRateInMS(view: DocumentScannerView, rate: Int) {
66
+ view.detectionRefreshRateInMS = rate
67
+ }
68
+
69
+ @ReactProp(name = "quality")
70
+ fun setQuality(view: DocumentScannerView, quality: Float) {
71
+ view.quality = quality
72
+ }
73
+
74
+ @ReactProp(name = "brightness")
75
+ fun setBrightness(view: DocumentScannerView, brightness: Float) {
76
+ view.brightness = brightness
77
+ }
78
+
79
+ @ReactProp(name = "contrast")
80
+ fun setContrast(view: DocumentScannerView, contrast: Float) {
81
+ view.contrast = contrast
82
+ }
83
+
84
+ @ReactProp(name = "saturation")
85
+ fun setSaturation(view: DocumentScannerView, saturation: Float) {
86
+ view.saturation = saturation
87
+ }
88
+
89
+ override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
90
+ return MapBuilder.of(
91
+ "onPictureTaken",
92
+ MapBuilder.of("registrationName", "onPictureTaken"),
93
+ "onRectangleDetect",
94
+ MapBuilder.of("registrationName", "onRectangleDetect")
95
+ )
96
+ }
97
+
98
+ override fun onDropViewInstance(view: DocumentScannerView) {
99
+ super.onDropViewInstance(view)
100
+ view.stopCamera()
101
+ }
102
+ }