react-native-rectangle-doc-scanner 3.130.0 → 3.131.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/build.gradle +78 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/kotlin/com/reactnativerectangledocscanner/CameraController.kt +229 -0
- package/android/src/main/kotlin/com/reactnativerectangledocscanner/DocumentDetector.kt +263 -0
- package/android/src/main/kotlin/com/reactnativerectangledocscanner/DocumentScannerModule.kt +128 -0
- package/android/src/main/kotlin/com/reactnativerectangledocscanner/DocumentScannerPackage.kt +16 -0
- package/android/src/main/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt +363 -0
- package/android/src/main/kotlin/com/reactnativerectangledocscanner/DocumentScannerViewManager.kt +102 -0
- package/android/src/main/kotlin/com/reactnativerectangledocscanner/ImageProcessor.kt +220 -0
- package/package.json +5 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
package com.reactnativerectangledocscanner
|
|
2
|
+
|
|
3
|
+
import android.graphics.BitmapFactory
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import com.facebook.react.bridge.*
|
|
6
|
+
import com.facebook.react.uimanager.UIManagerModule
|
|
7
|
+
import kotlinx.coroutines.*
|
|
8
|
+
|
|
9
|
+
class DocumentScannerModule(reactContext: ReactApplicationContext) :
|
|
10
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
11
|
+
|
|
12
|
+
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
13
|
+
|
|
14
|
+
companion object {
|
|
15
|
+
const val NAME = "RNPdfScannerManager"
|
|
16
|
+
private const val TAG = "DocumentScannerModule"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun getName() = NAME
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Capture image from the document scanner view
|
|
23
|
+
* Matches iOS signature: capture(reactTag, resolver, rejecter)
|
|
24
|
+
*/
|
|
25
|
+
@ReactMethod
|
|
26
|
+
fun capture(reactTag: Double?, promise: Promise) {
|
|
27
|
+
Log.d(TAG, "capture called with reactTag: $reactTag")
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
val tag = reactTag?.toInt() ?: run {
|
|
31
|
+
promise.reject("NO_TAG", "React tag is required")
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
val uiManager = reactApplicationContext.getNativeModule(UIManagerModule::class.java)
|
|
36
|
+
?: run {
|
|
37
|
+
promise.reject("NO_UI_MANAGER", "UIManager not available")
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
UiThreadUtil.runOnUiThread {
|
|
42
|
+
try {
|
|
43
|
+
val view = uiManager.resolveView(tag)
|
|
44
|
+
|
|
45
|
+
if (view is DocumentScannerView) {
|
|
46
|
+
Log.d(TAG, "Found DocumentScannerView, triggering capture")
|
|
47
|
+
|
|
48
|
+
// Store promise to be resolved when capture completes
|
|
49
|
+
// For simplicity, we'll trigger the capture which will emit the event
|
|
50
|
+
view.capture()
|
|
51
|
+
|
|
52
|
+
// Note: In the current implementation, we use events (onPictureTaken)
|
|
53
|
+
// iOS also uses events for the main flow, but has a promise-based method too
|
|
54
|
+
// For consistency with the event-based approach, resolve immediately
|
|
55
|
+
promise.resolve(Arguments.createMap().apply {
|
|
56
|
+
putString("status", "capturing")
|
|
57
|
+
})
|
|
58
|
+
} else {
|
|
59
|
+
Log.e(TAG, "View with tag $tag is not DocumentScannerView: ${view?.javaClass?.simpleName}")
|
|
60
|
+
promise.reject("INVALID_VIEW", "View is not a DocumentScannerView")
|
|
61
|
+
}
|
|
62
|
+
} catch (e: Exception) {
|
|
63
|
+
Log.e(TAG, "Error resolving view", e)
|
|
64
|
+
promise.reject("VIEW_ERROR", "Failed to resolve view: ${e.message}", e)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (e: Exception) {
|
|
68
|
+
Log.e(TAG, "Error in capture method", e)
|
|
69
|
+
promise.reject("CAPTURE_ERROR", "Failed to capture: ${e.message}", e)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Apply color controls to an image
|
|
75
|
+
* Matches iOS: applyColorControls(imagePath, brightness, contrast, saturation, resolver, rejecter)
|
|
76
|
+
*/
|
|
77
|
+
@ReactMethod
|
|
78
|
+
fun applyColorControls(
|
|
79
|
+
imagePath: String,
|
|
80
|
+
brightness: Double,
|
|
81
|
+
contrast: Double,
|
|
82
|
+
saturation: Double,
|
|
83
|
+
promise: Promise
|
|
84
|
+
) {
|
|
85
|
+
scope.launch {
|
|
86
|
+
try {
|
|
87
|
+
withContext(Dispatchers.IO) {
|
|
88
|
+
val bitmap = BitmapFactory.decodeFile(imagePath)
|
|
89
|
+
?: throw Exception("Failed to load image from path: $imagePath")
|
|
90
|
+
|
|
91
|
+
val processedBitmap = ImageProcessor.applyColorControls(
|
|
92
|
+
bitmap = bitmap,
|
|
93
|
+
brightness = brightness.toFloat(),
|
|
94
|
+
contrast = contrast.toFloat(),
|
|
95
|
+
saturation = saturation.toFloat()
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
val outputDir = reactApplicationContext.cacheDir
|
|
99
|
+
val timestamp = System.currentTimeMillis()
|
|
100
|
+
val outputPath = ImageProcessor.saveBitmapToFile(
|
|
101
|
+
bitmap = processedBitmap,
|
|
102
|
+
directory = outputDir,
|
|
103
|
+
filename = "docscanner_enhanced_$timestamp.jpg",
|
|
104
|
+
quality = 0.98f
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
// Cleanup
|
|
108
|
+
bitmap.recycle()
|
|
109
|
+
if (processedBitmap != bitmap) {
|
|
110
|
+
processedBitmap.recycle()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
withContext(Dispatchers.Main) {
|
|
114
|
+
promise.resolve(outputPath)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (e: Exception) {
|
|
118
|
+
Log.e(TAG, "Failed to apply color controls", e)
|
|
119
|
+
promise.reject("COLOR_CONTROLS_ERROR", "Failed to apply color controls: ${e.message}", e)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
override fun onCatalystInstanceDestroy() {
|
|
125
|
+
super.onCatalystInstanceDestroy()
|
|
126
|
+
scope.cancel()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -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,363 @@
|
|
|
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
|
+
if (isCapturing) {
|
|
157
|
+
Log.d(TAG, "Already capturing, ignoring request")
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
isCapturing = true
|
|
162
|
+
Log.d(TAG, "Capture initiated")
|
|
163
|
+
|
|
164
|
+
val outputDir = if (saveInAppDocument) {
|
|
165
|
+
context.filesDir
|
|
166
|
+
} else {
|
|
167
|
+
context.cacheDir
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
cameraController?.capturePhoto(
|
|
171
|
+
outputDirectory = outputDir,
|
|
172
|
+
onImageCaptured = { file ->
|
|
173
|
+
scope.launch {
|
|
174
|
+
processAndEmitImage(file)
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
onError = { exception ->
|
|
178
|
+
Log.e(TAG, "Capture failed", exception)
|
|
179
|
+
isCapturing = false
|
|
180
|
+
sendErrorEvent("capture_failed")
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private suspend fun processAndEmitImage(imageFile: File) = withContext(Dispatchers.IO) {
|
|
186
|
+
try {
|
|
187
|
+
// Detect rectangle in captured image
|
|
188
|
+
val bitmap = BitmapFactory.decodeFile(imageFile.absolutePath)
|
|
189
|
+
val detectedRectangle = DocumentDetector.detectRectangle(bitmap)
|
|
190
|
+
|
|
191
|
+
// Process image with detected rectangle
|
|
192
|
+
val shouldCrop = detectedRectangle != null && stableCounter > 0
|
|
193
|
+
val processed = ImageProcessor.processImage(
|
|
194
|
+
imagePath = imageFile.absolutePath,
|
|
195
|
+
rectangle = detectedRectangle,
|
|
196
|
+
brightness = brightness,
|
|
197
|
+
contrast = contrast,
|
|
198
|
+
saturation = saturation,
|
|
199
|
+
shouldCrop = shouldCrop
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// Save or encode images
|
|
203
|
+
val result = if (useBase64) {
|
|
204
|
+
Arguments.createMap().apply {
|
|
205
|
+
putString("croppedImage", ImageProcessor.bitmapToBase64(processed.croppedImage, quality))
|
|
206
|
+
putString("initialImage", ImageProcessor.bitmapToBase64(processed.initialImage, quality))
|
|
207
|
+
putMap("rectangleCoordinates", detectedRectangle?.toMap()?.toWritableMap())
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
val timestamp = System.currentTimeMillis()
|
|
211
|
+
val croppedPath = ImageProcessor.saveBitmapToFile(
|
|
212
|
+
processed.croppedImage,
|
|
213
|
+
if (saveInAppDocument) context.filesDir else context.cacheDir,
|
|
214
|
+
"cropped_img_$timestamp.jpeg",
|
|
215
|
+
quality
|
|
216
|
+
)
|
|
217
|
+
val initialPath = ImageProcessor.saveBitmapToFile(
|
|
218
|
+
processed.initialImage,
|
|
219
|
+
if (saveInAppDocument) context.filesDir else context.cacheDir,
|
|
220
|
+
"initial_img_$timestamp.jpeg",
|
|
221
|
+
quality
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
Arguments.createMap().apply {
|
|
225
|
+
putString("croppedImage", croppedPath)
|
|
226
|
+
putString("initialImage", initialPath)
|
|
227
|
+
putMap("rectangleCoordinates", detectedRectangle?.toMap()?.toWritableMap())
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
withContext(Dispatchers.Main) {
|
|
232
|
+
sendPictureTakenEvent(result)
|
|
233
|
+
isCapturing = false
|
|
234
|
+
|
|
235
|
+
if (!captureMultiple) {
|
|
236
|
+
stopCamera()
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch (e: Exception) {
|
|
240
|
+
Log.e(TAG, "Failed to process image", e)
|
|
241
|
+
withContext(Dispatchers.Main) {
|
|
242
|
+
sendErrorEvent("processing_failed")
|
|
243
|
+
isCapturing = false
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private fun sendPictureTakenEvent(data: WritableMap) {
|
|
249
|
+
val event = Arguments.createMap().apply {
|
|
250
|
+
merge(data)
|
|
251
|
+
}
|
|
252
|
+
themedContext.getJSModule(RCTEventEmitter::class.java)
|
|
253
|
+
.receiveEvent(id, "onPictureTaken", event)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private fun sendRectangleDetectEvent(rectangle: Rectangle?, quality: RectangleQuality) {
|
|
257
|
+
val event = Arguments.createMap().apply {
|
|
258
|
+
putInt("stableCounter", stableCounter)
|
|
259
|
+
putInt("lastDetectionType", quality.ordinal)
|
|
260
|
+
putMap("rectangleCoordinates", rectangle?.toMap()?.toWritableMap())
|
|
261
|
+
putMap("previewSize", Arguments.createMap().apply {
|
|
262
|
+
putInt("width", width)
|
|
263
|
+
putInt("height", height)
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
themedContext.getJSModule(RCTEventEmitter::class.java)
|
|
267
|
+
.receiveEvent(id, "onRectangleDetect", event)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private fun sendErrorEvent(error: String) {
|
|
271
|
+
val event = Arguments.createMap().apply {
|
|
272
|
+
putString("error", error)
|
|
273
|
+
}
|
|
274
|
+
themedContext.getJSModule(RCTEventEmitter::class.java)
|
|
275
|
+
.receiveEvent(id, "onPictureTaken", event)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fun setEnableTorch(enabled: Boolean) {
|
|
279
|
+
this.enableTorch = enabled
|
|
280
|
+
cameraController?.setTorchEnabled(enabled)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
fun setUseFrontCam(enabled: Boolean) {
|
|
284
|
+
if (this.useFrontCam != enabled) {
|
|
285
|
+
this.useFrontCam = enabled
|
|
286
|
+
cameraController?.stopCamera()
|
|
287
|
+
setupCamera()
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fun startCamera() {
|
|
292
|
+
cameraController?.startCamera(useFrontCam, !manualOnly)
|
|
293
|
+
startDetectionLoop()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fun stopCamera() {
|
|
297
|
+
detectionRunnable?.let { detectionHandler.removeCallbacks(it) }
|
|
298
|
+
cameraController?.stopCamera()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
override fun onDetachedFromWindow() {
|
|
302
|
+
super.onDetachedFromWindow()
|
|
303
|
+
stopCamera()
|
|
304
|
+
cameraController?.shutdown()
|
|
305
|
+
scope.cancel()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Overlay view for drawing detected rectangle
|
|
310
|
+
*/
|
|
311
|
+
private class OverlayView(context: Context) : View(context) {
|
|
312
|
+
private var rectangle: Rectangle? = null
|
|
313
|
+
private var overlayColor: Int = Color.parseColor("#80FFFFFF")
|
|
314
|
+
private val paint = Paint().apply {
|
|
315
|
+
style = Paint.Style.FILL
|
|
316
|
+
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fun setRectangle(rect: Rectangle?, color: Int) {
|
|
320
|
+
this.rectangle = rect
|
|
321
|
+
this.overlayColor = color
|
|
322
|
+
invalidate()
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
override fun onDraw(canvas: Canvas) {
|
|
326
|
+
super.onDraw(canvas)
|
|
327
|
+
|
|
328
|
+
rectangle?.let { rect ->
|
|
329
|
+
paint.color = overlayColor
|
|
330
|
+
|
|
331
|
+
// Draw the rectangle overlay (simplified - just a filled polygon)
|
|
332
|
+
val path = android.graphics.Path().apply {
|
|
333
|
+
moveTo(rect.topLeft.x.toFloat(), rect.topLeft.y.toFloat())
|
|
334
|
+
lineTo(rect.topRight.x.toFloat(), rect.topRight.y.toFloat())
|
|
335
|
+
lineTo(rect.bottomRight.x.toFloat(), rect.bottomRight.y.toFloat())
|
|
336
|
+
lineTo(rect.bottomLeft.x.toFloat(), rect.bottomLeft.y.toFloat())
|
|
337
|
+
close()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
canvas.drawPath(path, paint)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Extension function to convert Map to WritableMap
|
|
348
|
+
*/
|
|
349
|
+
private fun Map<String, Any?>.toWritableMap(): WritableMap {
|
|
350
|
+
val map = Arguments.createMap()
|
|
351
|
+
forEach { (key, value) ->
|
|
352
|
+
when (value) {
|
|
353
|
+
null -> map.putNull(key)
|
|
354
|
+
is Boolean -> map.putBoolean(key, value)
|
|
355
|
+
is Double -> map.putDouble(key, value)
|
|
356
|
+
is Int -> map.putInt(key, value)
|
|
357
|
+
is String -> map.putString(key, value)
|
|
358
|
+
is Map<*, *> -> map.putMap(key, (value as Map<String, Any?>).toWritableMap())
|
|
359
|
+
else -> Log.w("DocumentScannerView", "Unknown type for key $key: ${value::class.java}")
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return map
|
|
363
|
+
}
|
package/android/src/main/kotlin/com/reactnativerectangledocscanner/DocumentScannerViewManager.kt
ADDED
|
@@ -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
|
+
}
|