react-native-rectangle-doc-scanner 3.211.0 → 3.212.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.
@@ -3,6 +3,11 @@ package com.reactnativerectangledocscanner
3
3
  import android.Manifest
4
4
  import android.content.Context
5
5
  import android.content.pm.PackageManager
6
+ import android.graphics.Bitmap
7
+ import android.graphics.BitmapFactory
8
+ import android.graphics.Matrix
9
+ import android.graphics.Rect
10
+ import android.graphics.YuvImage
6
11
  import android.util.Log
7
12
  import android.util.Size
8
13
  import android.view.Surface
@@ -10,17 +15,18 @@ import androidx.camera.core.AspectRatio
10
15
  import androidx.camera.core.Camera
11
16
  import androidx.camera.core.CameraSelector
12
17
  import androidx.camera.core.ImageAnalysis
13
- import androidx.camera.core.ImageCapture
14
- import androidx.camera.core.ImageCaptureException
15
18
  import androidx.camera.core.Preview
16
19
  import androidx.camera.lifecycle.ProcessCameraProvider
17
20
  import androidx.camera.view.PreviewView
18
21
  import androidx.core.content.ContextCompat
19
22
  import androidx.lifecycle.LifecycleOwner
20
23
  import com.google.common.util.concurrent.ListenableFuture
24
+ import java.io.ByteArrayOutputStream
21
25
  import java.io.File
26
+ import java.io.FileOutputStream
22
27
  import java.util.concurrent.ExecutorService
23
28
  import java.util.concurrent.Executors
29
+ import java.util.concurrent.atomic.AtomicReference
24
30
 
25
31
  class CameraController(
26
32
  private val context: Context,
@@ -31,9 +37,9 @@ class CameraController(
31
37
  private var cameraProvider: ProcessCameraProvider? = null
32
38
  private var preview: Preview? = null
33
39
  private var imageAnalysis: ImageAnalysis? = null
34
- private var imageCapture: ImageCapture? = null
35
40
  private var camera: Camera? = null
36
41
  private val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
42
+ private val lastFrame = AtomicReference<LastFrame?>()
37
43
 
38
44
  private var useFrontCamera = false
39
45
  private var detectionEnabled = true
@@ -46,6 +52,14 @@ class CameraController(
46
52
  private const val ANALYSIS_HEIGHT = 720
47
53
  }
48
54
 
55
+ private data class LastFrame(
56
+ val nv21: ByteArray,
57
+ val width: Int,
58
+ val height: Int,
59
+ val rotationDegrees: Int,
60
+ val isFront: Boolean
61
+ )
62
+
49
63
  fun startCamera(
50
64
  useFrontCam: Boolean = false,
51
65
  enableDetection: Boolean = true
@@ -83,30 +97,35 @@ class CameraController(
83
97
  onImageCaptured: (File) -> Unit,
84
98
  onError: (Exception) -> Unit
85
99
  ) {
86
- val capture = imageCapture
87
- if (capture == null) {
88
- onError(IllegalStateException("ImageCapture not initialized"))
100
+ val frame = lastFrame.get()
101
+ if (frame == null) {
102
+ onError(IllegalStateException("No frame available for capture"))
89
103
  return
90
104
  }
91
105
 
92
- val photoFile = File(outputDirectory, "doc_scan_${System.currentTimeMillis()}.jpg")
93
- val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
106
+ cameraExecutor.execute {
107
+ try {
108
+ val photoFile = File(outputDirectory, "doc_scan_${System.currentTimeMillis()}.jpg")
109
+ val jpegBytes = nv21ToJpeg(frame.nv21, frame.width, frame.height, 95)
110
+ val bitmap = BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size)
111
+ ?: throw IllegalStateException("Failed to decode JPEG")
94
112
 
95
- capture.takePicture(
96
- outputOptions,
97
- cameraExecutor,
98
- object : ImageCapture.OnImageSavedCallback {
99
- override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
100
- Log.d(TAG, "[CAMERAX] Photo capture succeeded: ${photoFile.absolutePath}")
101
- onImageCaptured(photoFile)
113
+ val rotated = rotateAndMirror(bitmap, frame.rotationDegrees, frame.isFront)
114
+ FileOutputStream(photoFile).use { out ->
115
+ rotated.compress(Bitmap.CompressFormat.JPEG, 95, out)
102
116
  }
103
-
104
- override fun onError(exception: ImageCaptureException) {
105
- Log.e(TAG, "[CAMERAX] Photo capture failed", exception)
106
- onError(exception)
117
+ if (rotated != bitmap) {
118
+ rotated.recycle()
107
119
  }
120
+ bitmap.recycle()
121
+
122
+ Log.d(TAG, "[CAMERAX] Photo capture succeeded: ${photoFile.absolutePath}")
123
+ onImageCaptured(photoFile)
124
+ } catch (e: Exception) {
125
+ Log.e(TAG, "[CAMERAX] Photo capture failed", e)
126
+ onError(e)
108
127
  }
109
- )
128
+ }
110
129
  }
111
130
 
112
131
  fun setTorchEnabled(enabled: Boolean) {
@@ -153,12 +172,6 @@ class CameraController(
153
172
  it.setAnalyzer(cameraExecutor, DocumentAnalyzer())
154
173
  }
155
174
 
156
- imageCapture = ImageCapture.Builder()
157
- .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
158
- .setTargetRotation(rotation)
159
- .setTargetAspectRatio(AspectRatio.RATIO_4_3)
160
- .build()
161
-
162
175
  val cameraSelector = if (useFrontCamera) {
163
176
  CameraSelector.DEFAULT_FRONT_CAMERA
164
177
  } else {
@@ -170,8 +183,7 @@ class CameraController(
170
183
  lifecycleOwner,
171
184
  cameraSelector,
172
185
  preview,
173
- imageAnalysis,
174
- imageCapture
186
+ imageAnalysis
175
187
  )
176
188
  Log.d(TAG, "[CAMERAX] Camera bound successfully")
177
189
  } catch (e: Exception) {
@@ -184,6 +196,15 @@ class CameraController(
184
196
  try {
185
197
  val rotationDegrees = imageProxy.imageInfo.rotationDegrees
186
198
  val nv21 = imageProxy.toNv21()
199
+ lastFrame.set(
200
+ LastFrame(
201
+ nv21,
202
+ imageProxy.width,
203
+ imageProxy.height,
204
+ rotationDegrees,
205
+ useFrontCamera
206
+ )
207
+ )
187
208
 
188
209
  val frameWidth = if (rotationDegrees == 90 || rotationDegrees == 270) {
189
210
  imageProxy.height
@@ -216,6 +237,27 @@ class CameraController(
216
237
  }
217
238
  }
218
239
 
240
+ private fun nv21ToJpeg(nv21: ByteArray, width: Int, height: Int, quality: Int): ByteArray {
241
+ val yuv = YuvImage(nv21, android.graphics.ImageFormat.NV21, width, height, null)
242
+ val out = ByteArrayOutputStream()
243
+ yuv.compressToJpeg(Rect(0, 0, width, height), quality, out)
244
+ return out.toByteArray()
245
+ }
246
+
247
+ private fun rotateAndMirror(bitmap: Bitmap, rotationDegrees: Int, mirror: Boolean): Bitmap {
248
+ if (rotationDegrees == 0 && !mirror) {
249
+ return bitmap
250
+ }
251
+ val matrix = Matrix()
252
+ if (mirror) {
253
+ matrix.postScale(-1f, 1f, bitmap.width / 2f, bitmap.height / 2f)
254
+ }
255
+ if (rotationDegrees != 0) {
256
+ matrix.postRotate(rotationDegrees.toFloat(), bitmap.width / 2f, bitmap.height / 2f)
257
+ }
258
+ return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
259
+ }
260
+
219
261
  private fun hasCameraPermission(): Boolean {
220
262
  return ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
221
263
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.211.0",
3
+ "version": "3.212.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",