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.
@@ -0,0 +1,78 @@
1
+ buildscript {
2
+ ext.kotlin_version = '1.8.21'
3
+ repositories {
4
+ google()
5
+ mavenCentral()
6
+ }
7
+ dependencies {
8
+ classpath "com.android.tools.build:gradle:7.4.2"
9
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
10
+ }
11
+ }
12
+
13
+ apply plugin: 'com.android.library'
14
+ apply plugin: 'kotlin-android'
15
+
16
+ def safeExtGet(prop, fallback) {
17
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
18
+ }
19
+
20
+ android {
21
+ compileSdkVersion safeExtGet('compileSdkVersion', 33)
22
+
23
+ defaultConfig {
24
+ minSdkVersion safeExtGet('minSdkVersion', 21)
25
+ targetSdkVersion safeExtGet('targetSdkVersion', 33)
26
+ versionCode 1
27
+ versionName "1.0"
28
+ }
29
+
30
+ buildTypes {
31
+ release {
32
+ minifyEnabled false
33
+ }
34
+ }
35
+
36
+ compileOptions {
37
+ sourceCompatibility JavaVersion.VERSION_1_8
38
+ targetCompatibility JavaVersion.VERSION_1_8
39
+ }
40
+
41
+ kotlinOptions {
42
+ jvmTarget = '1.8'
43
+ }
44
+
45
+ sourceSets {
46
+ main {
47
+ java.srcDirs += 'src/main/kotlin'
48
+ }
49
+ }
50
+ }
51
+
52
+ repositories {
53
+ google()
54
+ mavenCentral()
55
+ }
56
+
57
+ dependencies {
58
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
59
+ implementation "com.facebook.react:react-native:+"
60
+
61
+ // CameraX dependencies
62
+ def camerax_version = "1.3.0"
63
+ implementation "androidx.camera:camera-core:${camerax_version}"
64
+ implementation "androidx.camera:camera-camera2:${camerax_version}"
65
+ implementation "androidx.camera:camera-lifecycle:${camerax_version}"
66
+ implementation "androidx.camera:camera-view:${camerax_version}"
67
+
68
+ // OpenCV for document detection
69
+ implementation 'org.opencv:opencv:4.8.0'
70
+
71
+ // Coroutines for async operations
72
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
73
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
74
+
75
+ // AndroidX
76
+ implementation 'androidx.core:core-ktx:1.10.1'
77
+ implementation 'androidx.appcompat:appcompat:1.6.1'
78
+ }
@@ -3,6 +3,14 @@
3
3
  package="com.reactnativerectangledocscanner">
4
4
 
5
5
  <uses-permission android:name="android.permission.CAMERA" />
6
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
7
+ android:maxSdkVersion="28" />
8
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
9
+ android:maxSdkVersion="32" />
10
+
11
+ <uses-feature android:name="android.hardware.camera" android:required="true" />
12
+ <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
13
+ <uses-feature android:name="android.hardware.camera.flash" android:required="false" />
6
14
 
7
15
  <application>
8
16
  <!-- Placeholder application entry, not used for libraries. -->
@@ -0,0 +1,229 @@
1
+ package com.reactnativerectangledocscanner
2
+
3
+ import android.content.Context
4
+ import android.util.Log
5
+ import android.util.Size
6
+ import androidx.camera.core.*
7
+ import androidx.camera.lifecycle.ProcessCameraProvider
8
+ import androidx.camera.view.PreviewView
9
+ import androidx.core.content.ContextCompat
10
+ import androidx.lifecycle.LifecycleOwner
11
+ import java.io.File
12
+ import java.util.concurrent.ExecutorService
13
+ import java.util.concurrent.Executors
14
+
15
+ class CameraController(
16
+ private val context: Context,
17
+ private val lifecycleOwner: LifecycleOwner,
18
+ private val previewView: PreviewView
19
+ ) {
20
+ private var camera: Camera? = null
21
+ private var cameraProvider: ProcessCameraProvider? = null
22
+ private var imageCapture: ImageCapture? = null
23
+ private var imageAnalysis: ImageAnalysis? = null
24
+ private val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
25
+
26
+ private var useFrontCamera = false
27
+ private var torchEnabled = false
28
+
29
+ var onFrameAnalyzed: ((Rectangle?) -> Unit)? = null
30
+
31
+ companion object {
32
+ private const val TAG = "CameraController"
33
+ }
34
+
35
+ /**
36
+ * Start camera with preview and analysis
37
+ */
38
+ fun startCamera(
39
+ useFrontCam: Boolean = false,
40
+ enableDetection: Boolean = true
41
+ ) {
42
+ this.useFrontCamera = useFrontCam
43
+
44
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
45
+
46
+ cameraProviderFuture.addListener({
47
+ try {
48
+ cameraProvider = cameraProviderFuture.get()
49
+ bindCameraUseCases(enableDetection)
50
+ } catch (e: Exception) {
51
+ Log.e(TAG, "Failed to start camera", e)
52
+ }
53
+ }, ContextCompat.getMainExecutor(context))
54
+ }
55
+
56
+ /**
57
+ * Stop camera and release resources
58
+ */
59
+ fun stopCamera() {
60
+ cameraProvider?.unbindAll()
61
+ camera = null
62
+ }
63
+
64
+ /**
65
+ * Bind camera use cases (preview, capture, analysis)
66
+ */
67
+ private fun bindCameraUseCases(enableDetection: Boolean) {
68
+ val cameraProvider = cameraProvider ?: return
69
+
70
+ // Select camera
71
+ val cameraSelector = if (useFrontCamera) {
72
+ CameraSelector.DEFAULT_FRONT_CAMERA
73
+ } else {
74
+ CameraSelector.DEFAULT_BACK_CAMERA
75
+ }
76
+
77
+ // Preview use case
78
+ val preview = Preview.Builder()
79
+ .setTargetResolution(Size(1080, 1920))
80
+ .build()
81
+ .also {
82
+ it.setSurfaceProvider(previewView.surfaceProvider)
83
+ }
84
+
85
+ // Image capture use case (high resolution for document scanning)
86
+ imageCapture = ImageCapture.Builder()
87
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
88
+ .setTargetResolution(Size(1920, 2560))
89
+ .build()
90
+
91
+ // Image analysis use case for rectangle detection
92
+ imageAnalysis = if (enableDetection) {
93
+ ImageAnalysis.Builder()
94
+ .setTargetResolution(Size(720, 1280))
95
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
96
+ .build()
97
+ .also { analysis ->
98
+ analysis.setAnalyzer(cameraExecutor) { imageProxy ->
99
+ analyzeFrame(imageProxy)
100
+ }
101
+ }
102
+ } else {
103
+ null
104
+ }
105
+
106
+ try {
107
+ // Unbind all use cases before rebinding
108
+ cameraProvider.unbindAll()
109
+
110
+ // Bind use cases to camera
111
+ val useCases = mutableListOf<UseCase>(preview, imageCapture!!)
112
+ if (imageAnalysis != null) {
113
+ useCases.add(imageAnalysis!!)
114
+ }
115
+
116
+ camera = cameraProvider.bindToLifecycle(
117
+ lifecycleOwner,
118
+ cameraSelector,
119
+ *useCases.toTypedArray()
120
+ )
121
+
122
+ // Restore torch state if it was enabled
123
+ if (torchEnabled) {
124
+ setTorchEnabled(true)
125
+ }
126
+
127
+ Log.d(TAG, "Camera started successfully")
128
+ } catch (e: Exception) {
129
+ Log.e(TAG, "Failed to bind camera use cases", e)
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Analyze frame for rectangle detection
135
+ */
136
+ private fun analyzeFrame(imageProxy: ImageProxy) {
137
+ try {
138
+ val buffer = imageProxy.planes[0].buffer
139
+ val bytes = ByteArray(buffer.remaining())
140
+ buffer.get(bytes)
141
+
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)
145
+ } catch (e: Exception) {
146
+ Log.e(TAG, "Error analyzing frame", e)
147
+ } finally {
148
+ imageProxy.close()
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Capture photo
154
+ */
155
+ fun capturePhoto(
156
+ outputDirectory: File,
157
+ onImageCaptured: (File) -> Unit,
158
+ onError: (Exception) -> Unit
159
+ ) {
160
+ val imageCapture = imageCapture ?: run {
161
+ onError(Exception("Image capture not initialized"))
162
+ return
163
+ }
164
+
165
+ val photoFile = File(
166
+ outputDirectory,
167
+ "doc_scan_${System.currentTimeMillis()}.jpg"
168
+ )
169
+
170
+ val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
171
+
172
+ imageCapture.takePicture(
173
+ outputOptions,
174
+ ContextCompat.getMainExecutor(context),
175
+ object : ImageCapture.OnImageSavedCallback {
176
+ override fun onImageSaved(output: ImageCapture.OutputFileResults) {
177
+ Log.d(TAG, "Photo capture succeeded: ${photoFile.absolutePath}")
178
+ onImageCaptured(photoFile)
179
+ }
180
+
181
+ override fun onError(exception: ImageCaptureException) {
182
+ Log.e(TAG, "Photo capture failed", exception)
183
+ onError(exception)
184
+ }
185
+ }
186
+ )
187
+ }
188
+
189
+ /**
190
+ * Enable or disable torch (flashlight)
191
+ */
192
+ fun setTorchEnabled(enabled: Boolean) {
193
+ torchEnabled = enabled
194
+ camera?.cameraControl?.enableTorch(enabled)
195
+ }
196
+
197
+ /**
198
+ * Switch between front and back camera
199
+ */
200
+ fun switchCamera() {
201
+ useFrontCamera = !useFrontCamera
202
+ startCamera(useFrontCamera)
203
+ }
204
+
205
+ /**
206
+ * Check if torch is available
207
+ */
208
+ fun isTorchAvailable(): Boolean {
209
+ return camera?.cameraInfo?.hasFlashUnit() == true
210
+ }
211
+
212
+ /**
213
+ * Focus at specific point
214
+ */
215
+ fun focusAt(x: Float, y: Float) {
216
+ val factory = previewView.meteringPointFactory
217
+ val point = factory.createPoint(x, y)
218
+ val action = FocusMeteringAction.Builder(point).build()
219
+ camera?.cameraControl?.startFocusAndMetering(action)
220
+ }
221
+
222
+ /**
223
+ * Cleanup resources
224
+ */
225
+ fun shutdown() {
226
+ cameraExecutor.shutdown()
227
+ stopCamera()
228
+ }
229
+ }
@@ -0,0 +1,263 @@
1
+ package com.reactnativerectangledocscanner
2
+
3
+ import android.graphics.Bitmap
4
+ import android.util.Log
5
+ import org.opencv.android.Utils
6
+ import org.opencv.core.*
7
+ import org.opencv.imgproc.Imgproc
8
+ import kotlin.math.abs
9
+ import kotlin.math.sqrt
10
+
11
+ data class Rectangle(
12
+ val topLeft: Point,
13
+ val topRight: Point,
14
+ val bottomLeft: Point,
15
+ val bottomRight: Point
16
+ ) {
17
+ fun toMap(): Map<String, Map<String, Double>> {
18
+ return mapOf(
19
+ "topLeft" to mapOf("x" to topLeft.x, "y" to topLeft.y),
20
+ "topRight" to mapOf("x" to topRight.x, "y" to topRight.y),
21
+ "bottomLeft" to mapOf("x" to bottomLeft.x, "y" to bottomLeft.y),
22
+ "bottomRight" to mapOf("x" to bottomRight.x, "y" to bottomRight.y)
23
+ )
24
+ }
25
+ }
26
+
27
+ enum class RectangleQuality {
28
+ GOOD,
29
+ BAD_ANGLE,
30
+ TOO_FAR
31
+ }
32
+
33
+ class DocumentDetector {
34
+ companion object {
35
+ private const val TAG = "DocumentDetector"
36
+
37
+ init {
38
+ try {
39
+ System.loadLibrary("opencv_java4")
40
+ Log.d(TAG, "OpenCV library loaded successfully")
41
+ } catch (e: Exception) {
42
+ Log.e(TAG, "Failed to load OpenCV library", e)
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Detect document rectangle in the bitmap
48
+ * Returns the largest detected rectangle or null if none found
49
+ */
50
+ fun detectRectangle(bitmap: Bitmap): Rectangle? {
51
+ val mat = Mat()
52
+ Utils.bitmapToMat(bitmap, mat)
53
+
54
+ val rectangle = detectRectangleInMat(mat)
55
+
56
+ mat.release()
57
+
58
+ return rectangle
59
+ }
60
+
61
+ /**
62
+ * Detect document rectangle in YUV image format (from camera)
63
+ */
64
+ fun detectRectangleInYUV(
65
+ yuvBytes: ByteArray,
66
+ width: Int,
67
+ height: Int,
68
+ rotation: Int
69
+ ): Rectangle? {
70
+ // Create Mat from YUV data
71
+ val yuvMat = Mat(height + height / 2, width, CvType.CV_8UC1)
72
+ yuvMat.put(0, 0, yuvBytes)
73
+
74
+ // Convert YUV to RGB
75
+ val rgbMat = Mat()
76
+ Imgproc.cvtColor(yuvMat, rgbMat, Imgproc.COLOR_YUV2RGB_NV21)
77
+
78
+ // Rotate if needed
79
+ if (rotation != 0) {
80
+ val rotationCode = when (rotation) {
81
+ 90 -> Core.ROTATE_90_CLOCKWISE
82
+ 180 -> Core.ROTATE_180
83
+ 270 -> Core.ROTATE_90_COUNTERCLOCKWISE
84
+ else -> null
85
+ }
86
+ if (rotationCode != null) {
87
+ Core.rotate(rgbMat, rgbMat, rotationCode)
88
+ }
89
+ }
90
+
91
+ val rectangle = detectRectangleInMat(rgbMat)
92
+
93
+ yuvMat.release()
94
+ rgbMat.release()
95
+
96
+ return rectangle
97
+ }
98
+
99
+ /**
100
+ * Core detection algorithm using OpenCV
101
+ */
102
+ private fun detectRectangleInMat(srcMat: Mat): Rectangle? {
103
+ val grayMat = Mat()
104
+ val blurredMat = Mat()
105
+ val cannyMat = Mat()
106
+
107
+ try {
108
+ // Convert to grayscale
109
+ if (srcMat.channels() > 1) {
110
+ Imgproc.cvtColor(srcMat, grayMat, Imgproc.COLOR_RGB2GRAY)
111
+ } else {
112
+ srcMat.copyTo(grayMat)
113
+ }
114
+
115
+ // Apply Gaussian blur to reduce noise
116
+ Imgproc.GaussianBlur(grayMat, blurredMat, Size(5.0, 5.0), 0.0)
117
+
118
+ // Apply Canny edge detection
119
+ Imgproc.Canny(blurredMat, cannyMat, 75.0, 200.0)
120
+
121
+ // Find contours
122
+ val contours = mutableListOf<MatOfPoint>()
123
+ val hierarchy = Mat()
124
+ Imgproc.findContours(
125
+ cannyMat,
126
+ contours,
127
+ hierarchy,
128
+ Imgproc.RETR_EXTERNAL,
129
+ Imgproc.CHAIN_APPROX_SIMPLE
130
+ )
131
+
132
+ // Find the largest contour that approximates to a quadrilateral
133
+ var largestRectangle: Rectangle? = null
134
+ var largestArea = 0.0
135
+
136
+ for (contour in contours) {
137
+ val contourArea = Imgproc.contourArea(contour)
138
+
139
+ // Filter small contours
140
+ if (contourArea < 1000) continue
141
+
142
+ // Approximate contour to polygon
143
+ val approx = MatOfPoint2f()
144
+ val contour2f = MatOfPoint2f(*contour.toArray())
145
+ val epsilon = 0.02 * Imgproc.arcLength(contour2f, true)
146
+ Imgproc.approxPolyDP(contour2f, approx, epsilon, true)
147
+
148
+ // Check if it's a quadrilateral
149
+ if (approx.total() == 4L && Imgproc.isContourConvex(MatOfPoint(*approx.toArray()))) {
150
+ val points = approx.toArray()
151
+
152
+ if (contourArea > largestArea) {
153
+ largestArea = contourArea
154
+ largestRectangle = orderPoints(points)
155
+ }
156
+ }
157
+
158
+ approx.release()
159
+ contour2f.release()
160
+ }
161
+
162
+ hierarchy.release()
163
+ contours.forEach { it.release() }
164
+
165
+ return largestRectangle
166
+ } finally {
167
+ grayMat.release()
168
+ blurredMat.release()
169
+ cannyMat.release()
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Order points in consistent order: topLeft, topRight, bottomLeft, bottomRight
175
+ */
176
+ private fun orderPoints(points: Array<Point>): Rectangle {
177
+ // Sort by y-coordinate
178
+ val sorted = points.sortedBy { it.y }
179
+
180
+ // Top two points
181
+ val topPoints = sorted.take(2).sortedBy { it.x }
182
+ val topLeft = topPoints[0]
183
+ val topRight = topPoints[1]
184
+
185
+ // Bottom two points
186
+ val bottomPoints = sorted.takeLast(2).sortedBy { it.x }
187
+ val bottomLeft = bottomPoints[0]
188
+ val bottomRight = bottomPoints[1]
189
+
190
+ return Rectangle(topLeft, topRight, bottomLeft, bottomRight)
191
+ }
192
+
193
+ /**
194
+ * Evaluate rectangle quality (matching iOS logic)
195
+ */
196
+ fun evaluateRectangleQuality(
197
+ rectangle: Rectangle,
198
+ imageWidth: Int,
199
+ imageHeight: Int
200
+ ): RectangleQuality {
201
+ // Check for bad angles
202
+ val topYDiff = abs(rectangle.topRight.y - rectangle.topLeft.y)
203
+ val bottomYDiff = abs(rectangle.bottomLeft.y - rectangle.bottomRight.y)
204
+ val leftXDiff = abs(rectangle.topLeft.x - rectangle.bottomLeft.x)
205
+ val rightXDiff = abs(rectangle.topRight.x - rectangle.bottomRight.x)
206
+
207
+ if (topYDiff > 100 || bottomYDiff > 100 || leftXDiff > 100 || rightXDiff > 100) {
208
+ return RectangleQuality.BAD_ANGLE
209
+ }
210
+
211
+ // Check if rectangle is too far from edges (too small)
212
+ val margin = 150.0
213
+ if (rectangle.topLeft.y > margin ||
214
+ rectangle.topRight.y > margin ||
215
+ rectangle.bottomLeft.y < (imageHeight - margin) ||
216
+ rectangle.bottomRight.y < (imageHeight - margin)
217
+ ) {
218
+ return RectangleQuality.TOO_FAR
219
+ }
220
+
221
+ return RectangleQuality.GOOD
222
+ }
223
+
224
+ /**
225
+ * Calculate perimeter of rectangle
226
+ */
227
+ fun calculatePerimeter(rectangle: Rectangle): Double {
228
+ val width = distance(rectangle.topLeft, rectangle.topRight)
229
+ val height = distance(rectangle.topLeft, rectangle.bottomLeft)
230
+ return 2 * (width + height)
231
+ }
232
+
233
+ /**
234
+ * Calculate distance between two points
235
+ */
236
+ private fun distance(p1: Point, p2: Point): Double {
237
+ val dx = p1.x - p2.x
238
+ val dy = p1.y - p2.y
239
+ return sqrt(dx * dx + dy * dy)
240
+ }
241
+
242
+ /**
243
+ * Transform rectangle coordinates from image space to view space
244
+ */
245
+ fun transformRectangleToViewCoordinates(
246
+ rectangle: Rectangle,
247
+ imageWidth: Int,
248
+ imageHeight: Int,
249
+ viewWidth: Int,
250
+ viewHeight: Int
251
+ ): Rectangle {
252
+ val scaleX = viewWidth.toDouble() / imageWidth
253
+ val scaleY = viewHeight.toDouble() / imageHeight
254
+
255
+ return Rectangle(
256
+ Point(rectangle.topLeft.x * scaleX, rectangle.topLeft.y * scaleY),
257
+ Point(rectangle.topRight.x * scaleX, rectangle.topRight.y * scaleY),
258
+ Point(rectangle.bottomLeft.x * scaleX, rectangle.bottomLeft.y * scaleY),
259
+ Point(rectangle.bottomRight.x * scaleX, rectangle.bottomRight.y * scaleY)
260
+ )
261
+ }
262
+ }
263
+ }