react-native-rectangle-doc-scanner 3.129.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,220 @@
|
|
|
1
|
+
package com.reactnativerectangledocscanner
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.BitmapFactory
|
|
5
|
+
import android.graphics.Matrix
|
|
6
|
+
import android.util.Base64
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import org.opencv.android.Utils
|
|
9
|
+
import org.opencv.core.*
|
|
10
|
+
import org.opencv.imgproc.Imgproc
|
|
11
|
+
import java.io.ByteArrayOutputStream
|
|
12
|
+
import java.io.File
|
|
13
|
+
import java.io.FileOutputStream
|
|
14
|
+
import kotlin.math.max
|
|
15
|
+
import kotlin.math.min
|
|
16
|
+
|
|
17
|
+
object ImageProcessor {
|
|
18
|
+
private const val TAG = "ImageProcessor"
|
|
19
|
+
|
|
20
|
+
init {
|
|
21
|
+
try {
|
|
22
|
+
System.loadLibrary("opencv_java4")
|
|
23
|
+
Log.d(TAG, "OpenCV library loaded successfully")
|
|
24
|
+
} catch (e: Exception) {
|
|
25
|
+
Log.e(TAG, "Failed to load OpenCV library", e)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
data class ProcessedImage(
|
|
30
|
+
val croppedImage: Bitmap,
|
|
31
|
+
val initialImage: Bitmap,
|
|
32
|
+
val rectangle: Rectangle?
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Apply perspective correction to crop document from detected rectangle
|
|
37
|
+
*/
|
|
38
|
+
fun cropAndCorrectPerspective(
|
|
39
|
+
bitmap: Bitmap,
|
|
40
|
+
rectangle: Rectangle
|
|
41
|
+
): Bitmap {
|
|
42
|
+
val srcMat = Mat()
|
|
43
|
+
Utils.bitmapToMat(bitmap, srcMat)
|
|
44
|
+
|
|
45
|
+
// Convert to proper format if needed
|
|
46
|
+
if (srcMat.channels() == 4) {
|
|
47
|
+
Imgproc.cvtColor(srcMat, srcMat, Imgproc.COLOR_RGBA2RGB)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Create source points from rectangle (match iOS order)
|
|
51
|
+
val srcPoints = MatOfPoint2f(
|
|
52
|
+
Point(rectangle.topLeft.x, rectangle.topLeft.y),
|
|
53
|
+
Point(rectangle.topRight.x, rectangle.topRight.y),
|
|
54
|
+
Point(rectangle.bottomLeft.x, rectangle.bottomLeft.y),
|
|
55
|
+
Point(rectangle.bottomRight.x, rectangle.bottomRight.y)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// Calculate destination rectangle dimensions
|
|
59
|
+
val widthTop = distance(rectangle.topLeft, rectangle.topRight)
|
|
60
|
+
val widthBottom = distance(rectangle.bottomLeft, rectangle.bottomRight)
|
|
61
|
+
val heightLeft = distance(rectangle.topLeft, rectangle.bottomLeft)
|
|
62
|
+
val heightRight = distance(rectangle.topRight, rectangle.bottomRight)
|
|
63
|
+
|
|
64
|
+
val maxWidth = max(widthTop, widthBottom).toInt()
|
|
65
|
+
val maxHeight = max(heightLeft, heightRight).toInt()
|
|
66
|
+
|
|
67
|
+
// Create destination points for a rectangle
|
|
68
|
+
val dstPoints = MatOfPoint2f(
|
|
69
|
+
Point(0.0, 0.0),
|
|
70
|
+
Point(maxWidth.toDouble(), 0.0),
|
|
71
|
+
Point(0.0, maxHeight.toDouble()),
|
|
72
|
+
Point(maxWidth.toDouble(), maxHeight.toDouble())
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
// Get perspective transform matrix and apply it
|
|
76
|
+
val transformMatrix = Imgproc.getPerspectiveTransform(srcPoints, dstPoints)
|
|
77
|
+
val dstMat = Mat()
|
|
78
|
+
Imgproc.warpPerspective(srcMat, dstMat, transformMatrix, Size(maxWidth.toDouble(), maxHeight.toDouble()))
|
|
79
|
+
|
|
80
|
+
// Convert back to bitmap
|
|
81
|
+
val croppedBitmap = Bitmap.createBitmap(maxWidth, maxHeight, Bitmap.Config.ARGB_8888)
|
|
82
|
+
Utils.matToBitmap(dstMat, croppedBitmap)
|
|
83
|
+
|
|
84
|
+
// Cleanup
|
|
85
|
+
srcMat.release()
|
|
86
|
+
dstMat.release()
|
|
87
|
+
transformMatrix.release()
|
|
88
|
+
srcPoints.release()
|
|
89
|
+
dstPoints.release()
|
|
90
|
+
|
|
91
|
+
return croppedBitmap
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Apply color adjustments (brightness, contrast, saturation)
|
|
96
|
+
*/
|
|
97
|
+
fun applyColorControls(
|
|
98
|
+
bitmap: Bitmap,
|
|
99
|
+
brightness: Float = 0f,
|
|
100
|
+
contrast: Float = 1f,
|
|
101
|
+
saturation: Float = 1f
|
|
102
|
+
): Bitmap {
|
|
103
|
+
val srcMat = Mat()
|
|
104
|
+
Utils.bitmapToMat(bitmap, srcMat)
|
|
105
|
+
|
|
106
|
+
// Convert to HSV for saturation adjustment
|
|
107
|
+
if (saturation != 1f) {
|
|
108
|
+
val hsvMat = Mat()
|
|
109
|
+
Imgproc.cvtColor(srcMat, hsvMat, Imgproc.COLOR_RGB2HSV)
|
|
110
|
+
|
|
111
|
+
val channels = mutableListOf<Mat>()
|
|
112
|
+
Core.split(hsvMat, channels)
|
|
113
|
+
|
|
114
|
+
// Adjust saturation channel
|
|
115
|
+
channels[1].convertTo(channels[1], -1, saturation.toDouble(), 0.0)
|
|
116
|
+
|
|
117
|
+
Core.merge(channels, hsvMat)
|
|
118
|
+
Imgproc.cvtColor(hsvMat, srcMat, Imgproc.COLOR_HSV2RGB)
|
|
119
|
+
|
|
120
|
+
hsvMat.release()
|
|
121
|
+
channels.forEach { it.release() }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Apply brightness and contrast
|
|
125
|
+
val dstMat = Mat()
|
|
126
|
+
srcMat.convertTo(dstMat, -1, contrast.toDouble(), brightness * 255.0)
|
|
127
|
+
|
|
128
|
+
val resultBitmap = Bitmap.createBitmap(bitmap.width, bitmap.height, bitmap.config)
|
|
129
|
+
Utils.matToBitmap(dstMat, resultBitmap)
|
|
130
|
+
|
|
131
|
+
srcMat.release()
|
|
132
|
+
dstMat.release()
|
|
133
|
+
|
|
134
|
+
return resultBitmap
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Save bitmap to file with specified quality
|
|
139
|
+
*/
|
|
140
|
+
fun saveBitmapToFile(
|
|
141
|
+
bitmap: Bitmap,
|
|
142
|
+
directory: File,
|
|
143
|
+
filename: String,
|
|
144
|
+
quality: Float
|
|
145
|
+
): String {
|
|
146
|
+
val qualityPercent = max(95f, min(100f, quality * 100f)).toInt()
|
|
147
|
+
val file = File(directory, filename)
|
|
148
|
+
|
|
149
|
+
FileOutputStream(file).use { out ->
|
|
150
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, qualityPercent, out)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return file.absolutePath
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Convert bitmap to Base64 string
|
|
158
|
+
*/
|
|
159
|
+
fun bitmapToBase64(bitmap: Bitmap, quality: Float): String {
|
|
160
|
+
val qualityPercent = max(95f, min(100f, quality * 100f)).toInt()
|
|
161
|
+
val byteArrayOutputStream = ByteArrayOutputStream()
|
|
162
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, qualityPercent, byteArrayOutputStream)
|
|
163
|
+
val byteArray = byteArrayOutputStream.toByteArray()
|
|
164
|
+
return Base64.encodeToString(byteArray, Base64.NO_WRAP)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Calculate Euclidean distance between two points
|
|
169
|
+
*/
|
|
170
|
+
private fun distance(p1: Point, p2: Point): Double {
|
|
171
|
+
val dx = p1.x - p2.x
|
|
172
|
+
val dy = p1.y - p2.y
|
|
173
|
+
return kotlin.math.sqrt(dx * dx + dy * dy)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Rotate bitmap by specified degrees
|
|
178
|
+
*/
|
|
179
|
+
fun rotateBitmap(bitmap: Bitmap, degrees: Float): Bitmap {
|
|
180
|
+
val matrix = Matrix()
|
|
181
|
+
matrix.postRotate(degrees)
|
|
182
|
+
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Process captured image with optional cropping and color adjustments
|
|
187
|
+
*/
|
|
188
|
+
fun processImage(
|
|
189
|
+
imagePath: String,
|
|
190
|
+
rectangle: Rectangle?,
|
|
191
|
+
brightness: Float = 0f,
|
|
192
|
+
contrast: Float = 1f,
|
|
193
|
+
saturation: Float = 1f,
|
|
194
|
+
shouldCrop: Boolean = true
|
|
195
|
+
): ProcessedImage {
|
|
196
|
+
var initialBitmap = BitmapFactory.decodeFile(imagePath)
|
|
197
|
+
var croppedBitmap = initialBitmap
|
|
198
|
+
|
|
199
|
+
// Apply perspective correction if rectangle detected and cropping enabled
|
|
200
|
+
if (shouldCrop && rectangle != null) {
|
|
201
|
+
try {
|
|
202
|
+
croppedBitmap = cropAndCorrectPerspective(initialBitmap, rectangle)
|
|
203
|
+
} catch (e: Exception) {
|
|
204
|
+
Log.e(TAG, "Failed to crop image, using original", e)
|
|
205
|
+
croppedBitmap = initialBitmap
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Apply color adjustments to cropped image
|
|
210
|
+
if (brightness != 0f || contrast != 1f || saturation != 1f) {
|
|
211
|
+
try {
|
|
212
|
+
croppedBitmap = applyColorControls(croppedBitmap, brightness, contrast, saturation)
|
|
213
|
+
} catch (e: Exception) {
|
|
214
|
+
Log.e(TAG, "Failed to apply color controls", e)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return ProcessedImage(croppedBitmap, initialBitmap, rectangle)
|
|
219
|
+
}
|
|
220
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-rectangle-doc-scanner",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.131.0",
|
|
4
4
|
"description": "Native-backed document scanner for React Native with customizable overlays.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -49,5 +49,9 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"expo-image-manipulator": "^12.0.5"
|
|
52
|
+
},
|
|
53
|
+
"react-native": {
|
|
54
|
+
"android": "./android",
|
|
55
|
+
"ios": "./ios"
|
|
52
56
|
}
|
|
53
57
|
}
|