react-native-rectangle-doc-scanner 3.229.0 → 3.231.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.
|
@@ -6,112 +6,114 @@ import android.content.pm.PackageManager
|
|
|
6
6
|
import android.graphics.Bitmap
|
|
7
7
|
import android.graphics.BitmapFactory
|
|
8
8
|
import android.graphics.Matrix
|
|
9
|
-
import android.graphics.
|
|
10
|
-
import android.graphics.
|
|
9
|
+
import android.graphics.SurfaceTexture
|
|
10
|
+
import android.graphics.ImageFormat
|
|
11
|
+
import android.hardware.camera2.CameraCaptureSession
|
|
12
|
+
import android.hardware.camera2.CameraCharacteristics
|
|
13
|
+
import android.hardware.camera2.CameraDevice
|
|
14
|
+
import android.hardware.camera2.CameraManager
|
|
15
|
+
import android.hardware.camera2.CaptureRequest
|
|
16
|
+
import android.media.Image
|
|
17
|
+
import android.media.ImageReader
|
|
18
|
+
import android.os.Handler
|
|
19
|
+
import android.os.HandlerThread
|
|
11
20
|
import android.util.Log
|
|
12
21
|
import android.util.Size
|
|
13
22
|
import android.view.Surface
|
|
14
|
-
import
|
|
15
|
-
import androidx.camera.core.CameraSelector
|
|
16
|
-
import androidx.camera.core.ImageAnalysis
|
|
17
|
-
import androidx.camera.core.ImageCapture
|
|
18
|
-
import androidx.camera.core.ImageCaptureException
|
|
19
|
-
import androidx.camera.core.Preview
|
|
20
|
-
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
21
|
-
import androidx.camera.view.PreviewView
|
|
23
|
+
import android.view.TextureView
|
|
22
24
|
import androidx.core.content.ContextCompat
|
|
23
|
-
import androidx.lifecycle.LifecycleOwner
|
|
24
|
-
import com.google.common.util.concurrent.ListenableFuture
|
|
25
|
-
import java.io.ByteArrayOutputStream
|
|
26
25
|
import java.io.File
|
|
27
26
|
import java.io.FileOutputStream
|
|
28
|
-
import java.util.concurrent.ExecutorService
|
|
29
|
-
import java.util.concurrent.Executors
|
|
30
27
|
import java.util.concurrent.atomic.AtomicReference
|
|
28
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
29
|
+
import kotlin.math.abs
|
|
30
|
+
import kotlin.math.max
|
|
31
31
|
|
|
32
32
|
class CameraController(
|
|
33
33
|
private val context: Context,
|
|
34
|
-
private val lifecycleOwner: LifecycleOwner,
|
|
35
|
-
private val previewView:
|
|
34
|
+
private val lifecycleOwner: androidx.lifecycle.LifecycleOwner,
|
|
35
|
+
private val previewView: TextureView
|
|
36
36
|
) {
|
|
37
|
-
private
|
|
38
|
-
private var
|
|
39
|
-
private var
|
|
40
|
-
private var
|
|
41
|
-
|
|
42
|
-
private var
|
|
43
|
-
private
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
private var
|
|
47
|
-
private var
|
|
48
|
-
|
|
49
|
-
private
|
|
37
|
+
private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
|
38
|
+
private var cameraDevice: CameraDevice? = null
|
|
39
|
+
private var captureSession: CameraCaptureSession? = null
|
|
40
|
+
private var previewRequestBuilder: CaptureRequest.Builder? = null
|
|
41
|
+
|
|
42
|
+
private var previewSize: Size? = null
|
|
43
|
+
private var analysisSize: Size? = null
|
|
44
|
+
private var captureSize: Size? = null
|
|
45
|
+
|
|
46
|
+
private var yuvReader: ImageReader? = null
|
|
47
|
+
private var jpegReader: ImageReader? = null
|
|
48
|
+
|
|
49
|
+
private val cameraThread = HandlerThread("Camera2Thread").apply { start() }
|
|
50
|
+
private val cameraHandler = Handler(cameraThread.looper)
|
|
51
|
+
private val analysisThread = HandlerThread("Camera2Analysis").apply { start() }
|
|
52
|
+
private val analysisHandler = Handler(analysisThread.looper)
|
|
50
53
|
|
|
51
54
|
private var useFrontCamera = false
|
|
52
55
|
private var detectionEnabled = true
|
|
56
|
+
private var torchEnabled = false
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
private
|
|
56
|
-
private val analysisHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
|
57
|
-
private val analysisRunnable = object : Runnable {
|
|
58
|
-
override fun run() {
|
|
59
|
-
if (isAnalysisActive && onFrameAnalyzed != null) {
|
|
60
|
-
captureFrameForAnalysis()
|
|
61
|
-
analysisHandler.postDelayed(this, 200) // Capture every 200ms
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
58
|
+
private val pendingCapture = AtomicReference<PendingCapture?>()
|
|
59
|
+
private val analysisInFlight = AtomicBoolean(false)
|
|
65
60
|
|
|
66
61
|
var onFrameAnalyzed: ((Rectangle?, Int, Int) -> Unit)? = null
|
|
67
62
|
|
|
68
63
|
companion object {
|
|
69
64
|
private const val TAG = "CameraController"
|
|
65
|
+
private const val ANALYSIS_MAX_AREA = 1920 * 1080
|
|
66
|
+
private const val ANALYSIS_ASPECT_TOLERANCE = 0.15
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
private data class
|
|
73
|
-
val
|
|
74
|
-
val
|
|
75
|
-
val
|
|
76
|
-
val rotationDegrees: Int,
|
|
77
|
-
val isFront: Boolean
|
|
69
|
+
private data class PendingCapture(
|
|
70
|
+
val outputDirectory: File,
|
|
71
|
+
val onImageCaptured: (File) -> Unit,
|
|
72
|
+
val onError: (Exception) -> Unit
|
|
78
73
|
)
|
|
79
74
|
|
|
75
|
+
private val textureListener = object : TextureView.SurfaceTextureListener {
|
|
76
|
+
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
|
|
77
|
+
openCamera()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
|
|
81
|
+
configureTransform()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
|
|
89
|
+
// no-op
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
fun startCamera(
|
|
81
94
|
useFrontCam: Boolean = false,
|
|
82
95
|
enableDetection: Boolean = true
|
|
83
96
|
) {
|
|
84
|
-
Log.d(TAG, "[
|
|
97
|
+
Log.d(TAG, "[CAMERA2] startCamera called")
|
|
85
98
|
this.useFrontCamera = useFrontCam
|
|
86
99
|
this.detectionEnabled = enableDetection
|
|
87
|
-
triedLowResFallback = false
|
|
88
100
|
|
|
89
101
|
if (!hasCameraPermission()) {
|
|
90
|
-
Log.e(TAG, "[
|
|
102
|
+
Log.e(TAG, "[CAMERA2] Camera permission not granted")
|
|
91
103
|
return
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
if (
|
|
95
|
-
|
|
106
|
+
if (previewView.isAvailable) {
|
|
107
|
+
openCamera()
|
|
108
|
+
} else {
|
|
109
|
+
previewView.surfaceTextureListener = textureListener
|
|
96
110
|
}
|
|
97
|
-
|
|
98
|
-
cameraProviderFuture?.addListener({
|
|
99
|
-
try {
|
|
100
|
-
cameraProvider = cameraProviderFuture?.get()
|
|
101
|
-
bindCameraUseCases()
|
|
102
|
-
} catch (e: Exception) {
|
|
103
|
-
Log.e(TAG, "[CAMERAX-V6] Failed to get camera provider", e)
|
|
104
|
-
}
|
|
105
|
-
}, ContextCompat.getMainExecutor(context))
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
fun stopCamera() {
|
|
109
|
-
Log.d(TAG, "[
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
streamCheckRunnable?.let { streamCheckHandler.removeCallbacks(it) }
|
|
113
|
-
cameraProvider?.unbindAll()
|
|
114
|
-
analysisBound = false
|
|
114
|
+
Log.d(TAG, "[CAMERA2] stopCamera called")
|
|
115
|
+
previewView.surfaceTextureListener = null
|
|
116
|
+
closeSession()
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
fun capturePhoto(
|
|
@@ -119,195 +121,360 @@ class CameraController(
|
|
|
119
121
|
onImageCaptured: (File) -> Unit,
|
|
120
122
|
onError: (Exception) -> Unit
|
|
121
123
|
) {
|
|
122
|
-
val
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
val device = cameraDevice
|
|
125
|
+
val session = captureSession
|
|
126
|
+
val reader = jpegReader
|
|
127
|
+
if (device == null || session == null || reader == null) {
|
|
128
|
+
onError(IllegalStateException("Camera not ready for capture"))
|
|
125
129
|
return
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
val bitmap = BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size)
|
|
133
|
-
?: throw IllegalStateException("Failed to decode JPEG")
|
|
132
|
+
if (!pendingCapture.compareAndSet(null, PendingCapture(outputDirectory, onImageCaptured, onError))) {
|
|
133
|
+
onError(IllegalStateException("Capture already in progress"))
|
|
134
|
+
return
|
|
135
|
+
}
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
try {
|
|
138
|
+
val requestBuilder = device.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE).apply {
|
|
139
|
+
addTarget(reader.surface)
|
|
140
|
+
set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
|
|
141
|
+
set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
|
|
142
|
+
if (torchEnabled) {
|
|
143
|
+
set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)
|
|
141
144
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
Log.d(TAG, "[CAMERAX-V6] Photo capture succeeded: ${photoFile.absolutePath}")
|
|
145
|
-
onImageCaptured(photoFile)
|
|
146
|
-
} catch (e: Exception) {
|
|
147
|
-
Log.e(TAG, "[CAMERAX-V6] Photo capture failed", e)
|
|
148
|
-
onError(e)
|
|
145
|
+
set(CaptureRequest.JPEG_ORIENTATION, 0)
|
|
149
146
|
}
|
|
147
|
+
|
|
148
|
+
session.capture(requestBuilder.build(), object : CameraCaptureSession.CaptureCallback() {}, cameraHandler)
|
|
149
|
+
} catch (e: Exception) {
|
|
150
|
+
pendingCapture.getAndSet(null)?.onError?.invoke(e)
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
fun setTorchEnabled(enabled: Boolean) {
|
|
154
|
-
|
|
155
|
+
torchEnabled = enabled
|
|
156
|
+
updateRepeatingRequest()
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
fun switchCamera() {
|
|
158
160
|
useFrontCamera = !useFrontCamera
|
|
159
|
-
|
|
161
|
+
closeSession()
|
|
162
|
+
openCamera()
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
fun isTorchAvailable(): Boolean {
|
|
163
|
-
return
|
|
166
|
+
return try {
|
|
167
|
+
val cameraId = selectCameraId() ?: return false
|
|
168
|
+
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
|
|
169
|
+
characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true
|
|
170
|
+
} catch (e: Exception) {
|
|
171
|
+
false
|
|
172
|
+
}
|
|
164
173
|
}
|
|
165
174
|
|
|
166
175
|
fun focusAt(x: Float, y: Float) {
|
|
167
|
-
//
|
|
176
|
+
// Optional: implement touch-to-focus if needed.
|
|
168
177
|
}
|
|
169
178
|
|
|
170
179
|
fun shutdown() {
|
|
171
180
|
stopCamera()
|
|
172
|
-
|
|
181
|
+
cameraThread.quitSafely()
|
|
182
|
+
analysisThread.quitSafely()
|
|
173
183
|
}
|
|
174
184
|
|
|
175
|
-
private fun
|
|
176
|
-
if (
|
|
177
|
-
if (pendingBindAttempts < 5) {
|
|
178
|
-
pendingBindAttempts++
|
|
179
|
-
Log.d(TAG, "[CAMERAX-V9] PreviewView not ready (attached=${previewView.isAttachedToWindow}, w=${previewView.width}, h=${previewView.height}), retrying...")
|
|
180
|
-
previewView.post { bindCameraUseCases() }
|
|
181
|
-
} else {
|
|
182
|
-
Log.w(TAG, "[CAMERAX-V9] PreviewView still not ready after retries, aborting bind")
|
|
183
|
-
}
|
|
185
|
+
private fun openCamera() {
|
|
186
|
+
if (cameraDevice != null) {
|
|
184
187
|
return
|
|
185
188
|
}
|
|
186
|
-
|
|
189
|
+
val cameraId = selectCameraId() ?: return
|
|
190
|
+
try {
|
|
191
|
+
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
|
|
192
|
+
val streamConfigMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
|
|
193
|
+
?: return
|
|
194
|
+
|
|
195
|
+
val viewAspect = if (previewView.height == 0) {
|
|
196
|
+
1.0
|
|
197
|
+
} else {
|
|
198
|
+
previewView.width.toDouble() / previewView.height.toDouble()
|
|
199
|
+
}
|
|
187
200
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
analysisBound = false
|
|
191
|
-
isAnalysisActive = false
|
|
201
|
+
val previewSizes = streamConfigMap.getOutputSizes(SurfaceTexture::class.java)
|
|
202
|
+
previewSize = chooseBestSize(previewSizes, viewAspect, null)
|
|
192
203
|
|
|
193
|
-
|
|
204
|
+
val analysisSizes = streamConfigMap.getOutputSizes(ImageFormat.YUV_420_888)
|
|
205
|
+
analysisSize = chooseBestSize(analysisSizes, viewAspect, ANALYSIS_MAX_AREA)
|
|
194
206
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
207
|
+
val captureSizes = streamConfigMap.getOutputSizes(ImageFormat.JPEG)
|
|
208
|
+
captureSize = captureSizes?.maxByOrNull { it.width * it.height }
|
|
209
|
+
|
|
210
|
+
setupImageReaders()
|
|
211
|
+
|
|
212
|
+
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
|
213
|
+
Log.e(TAG, "[CAMERA2] Camera permission not granted")
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
|
|
218
|
+
override fun onOpened(camera: CameraDevice) {
|
|
219
|
+
cameraDevice = camera
|
|
220
|
+
createCaptureSession()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
override fun onDisconnected(camera: CameraDevice) {
|
|
224
|
+
camera.close()
|
|
225
|
+
cameraDevice = null
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
override fun onError(camera: CameraDevice, error: Int) {
|
|
229
|
+
Log.e(TAG, "[CAMERA2] CameraDevice error: $error")
|
|
230
|
+
camera.close()
|
|
231
|
+
cameraDevice = null
|
|
232
|
+
}
|
|
233
|
+
}, cameraHandler)
|
|
234
|
+
} catch (e: Exception) {
|
|
235
|
+
Log.e(TAG, "[CAMERA2] Failed to open camera", e)
|
|
204
236
|
}
|
|
237
|
+
}
|
|
205
238
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
239
|
+
private fun setupImageReaders() {
|
|
240
|
+
val analysis = analysisSize
|
|
241
|
+
val capture = captureSize
|
|
242
|
+
|
|
243
|
+
yuvReader?.close()
|
|
244
|
+
jpegReader?.close()
|
|
245
|
+
|
|
246
|
+
if (analysis != null) {
|
|
247
|
+
yuvReader = ImageReader.newInstance(analysis.width, analysis.height, ImageFormat.YUV_420_888, 2).apply {
|
|
248
|
+
setOnImageAvailableListener({ reader ->
|
|
249
|
+
if (!detectionEnabled || onFrameAnalyzed == null) {
|
|
250
|
+
try {
|
|
251
|
+
reader.acquireLatestImage()?.close()
|
|
252
|
+
} catch (e: Exception) {
|
|
253
|
+
Log.w(TAG, "[CAMERA2] Failed to drain analysis image", e)
|
|
254
|
+
}
|
|
255
|
+
return@setOnImageAvailableListener
|
|
256
|
+
}
|
|
257
|
+
if (!analysisInFlight.compareAndSet(false, true)) {
|
|
258
|
+
try {
|
|
259
|
+
reader.acquireLatestImage()?.close()
|
|
260
|
+
} catch (e: Exception) {
|
|
261
|
+
Log.w(TAG, "[CAMERA2] Failed to drop analysis image", e)
|
|
262
|
+
}
|
|
263
|
+
return@setOnImageAvailableListener
|
|
264
|
+
}
|
|
265
|
+
val image = try {
|
|
266
|
+
reader.acquireLatestImage()
|
|
267
|
+
} catch (e: Exception) {
|
|
268
|
+
analysisInFlight.set(false)
|
|
269
|
+
Log.w(TAG, "[CAMERA2] acquireLatestImage failed", e)
|
|
270
|
+
null
|
|
271
|
+
}
|
|
272
|
+
if (image == null) {
|
|
273
|
+
analysisInFlight.set(false)
|
|
274
|
+
return@setOnImageAvailableListener
|
|
275
|
+
}
|
|
276
|
+
analysisHandler.post { analyzeImage(image) }
|
|
277
|
+
}, cameraHandler)
|
|
278
|
+
}
|
|
210
279
|
}
|
|
211
280
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
Log.d(TAG, "[CAMERAX-V9] Preview bound, waiting for capture session to configure...")
|
|
281
|
+
if (capture != null) {
|
|
282
|
+
jpegReader = ImageReader.newInstance(capture.width, capture.height, ImageFormat.JPEG, 2).apply {
|
|
283
|
+
setOnImageAvailableListener({ reader ->
|
|
284
|
+
val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener
|
|
285
|
+
val pending = pendingCapture.getAndSet(null)
|
|
286
|
+
if (pending == null) {
|
|
287
|
+
image.close()
|
|
288
|
+
return@setOnImageAvailableListener
|
|
289
|
+
}
|
|
290
|
+
analysisHandler.post { processCapture(image, pending) }
|
|
291
|
+
}, cameraHandler)
|
|
224
292
|
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private fun createCaptureSession() {
|
|
297
|
+
val device = cameraDevice ?: return
|
|
298
|
+
val surfaceTexture = previewView.surfaceTexture ?: return
|
|
299
|
+
val preview = previewSize ?: return
|
|
300
|
+
|
|
301
|
+
surfaceTexture.setDefaultBufferSize(preview.width, preview.height)
|
|
302
|
+
val previewSurface = Surface(surfaceTexture)
|
|
225
303
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
304
|
+
val targets = mutableListOf(previewSurface)
|
|
305
|
+
yuvReader?.surface?.let { targets.add(it) }
|
|
306
|
+
jpegReader?.surface?.let { targets.add(it) }
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
device.createCaptureSession(targets, object : CameraCaptureSession.StateCallback() {
|
|
310
|
+
override fun onConfigured(session: CameraCaptureSession) {
|
|
311
|
+
captureSession = session
|
|
312
|
+
configureTransform()
|
|
313
|
+
startRepeating(previewSurface)
|
|
314
|
+
}
|
|
230
315
|
|
|
231
|
-
|
|
316
|
+
override fun onConfigureFailed(session: CameraCaptureSession) {
|
|
317
|
+
Log.e(TAG, "[CAMERA2] Failed to configure capture session")
|
|
318
|
+
}
|
|
319
|
+
}, cameraHandler)
|
|
232
320
|
} catch (e: Exception) {
|
|
233
|
-
Log.e(TAG, "[
|
|
321
|
+
Log.e(TAG, "[CAMERA2] Failed to create capture session", e)
|
|
234
322
|
}
|
|
235
323
|
}
|
|
236
324
|
|
|
237
|
-
private fun
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
325
|
+
private fun startRepeating(previewSurface: Surface) {
|
|
326
|
+
val device = cameraDevice ?: return
|
|
327
|
+
try {
|
|
328
|
+
previewRequestBuilder = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
|
|
329
|
+
addTarget(previewSurface)
|
|
330
|
+
yuvReader?.surface?.let { addTarget(it) }
|
|
331
|
+
set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
|
|
332
|
+
set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
|
|
333
|
+
if (torchEnabled) {
|
|
334
|
+
set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH)
|
|
335
|
+
}
|
|
248
336
|
}
|
|
337
|
+
captureSession?.setRepeatingRequest(previewRequestBuilder?.build() ?: return, null, cameraHandler)
|
|
338
|
+
} catch (e: Exception) {
|
|
339
|
+
Log.e(TAG, "[CAMERA2] Failed to start repeating request", e)
|
|
249
340
|
}
|
|
250
|
-
streamCheckHandler.postDelayed(streamCheckRunnable!!, 6500)
|
|
251
341
|
}
|
|
252
342
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
val nv21 = image.toNv21()
|
|
263
|
-
|
|
264
|
-
lastFrame.set(
|
|
265
|
-
LastFrame(
|
|
266
|
-
nv21,
|
|
267
|
-
image.width,
|
|
268
|
-
image.height,
|
|
269
|
-
rotationDegrees,
|
|
270
|
-
useFrontCamera
|
|
271
|
-
)
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
val frameWidth = if (rotationDegrees == 90 || rotationDegrees == 270) {
|
|
275
|
-
image.height
|
|
276
|
-
} else {
|
|
277
|
-
image.width
|
|
278
|
-
}
|
|
343
|
+
private fun updateRepeatingRequest() {
|
|
344
|
+
val builder = previewRequestBuilder ?: return
|
|
345
|
+
builder.set(CaptureRequest.FLASH_MODE, if (torchEnabled) CaptureRequest.FLASH_MODE_TORCH else CaptureRequest.FLASH_MODE_OFF)
|
|
346
|
+
try {
|
|
347
|
+
captureSession?.setRepeatingRequest(builder.build(), null, cameraHandler)
|
|
348
|
+
} catch (e: Exception) {
|
|
349
|
+
Log.e(TAG, "[CAMERA2] Failed to update torch state", e)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
279
352
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
353
|
+
private fun analyzeImage(image: Image) {
|
|
354
|
+
try {
|
|
355
|
+
val nv21 = image.toNv21()
|
|
356
|
+
val rotationDegrees = computeRotationDegrees()
|
|
357
|
+
val rectangle = DocumentDetector.detectRectangleInYUV(nv21, image.width, image.height, rotationDegrees)
|
|
285
358
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
359
|
+
val frameWidth = if (rotationDegrees == 90 || rotationDegrees == 270) image.height else image.width
|
|
360
|
+
val frameHeight = if (rotationDegrees == 90 || rotationDegrees == 270) image.width else image.height
|
|
361
|
+
|
|
362
|
+
onFrameAnalyzed?.invoke(rectangle, frameWidth, frameHeight)
|
|
363
|
+
} catch (e: Exception) {
|
|
364
|
+
Log.e(TAG, "[CAMERA2] Error analyzing frame", e)
|
|
365
|
+
} finally {
|
|
366
|
+
image.close()
|
|
367
|
+
analysisInFlight.set(false)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private fun processCapture(image: Image, pending: PendingCapture) {
|
|
372
|
+
try {
|
|
373
|
+
val buffer = image.planes[0].buffer
|
|
374
|
+
val bytes = ByteArray(buffer.remaining())
|
|
375
|
+
buffer.get(bytes)
|
|
376
|
+
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
|
377
|
+
?: throw IllegalStateException("Failed to decode JPEG")
|
|
378
|
+
|
|
379
|
+
val rotated = rotateAndMirror(bitmap, computeRotationDegrees(), useFrontCamera)
|
|
380
|
+
val photoFile = File(pending.outputDirectory, "doc_scan_${System.currentTimeMillis()}.jpg")
|
|
381
|
+
FileOutputStream(photoFile).use { out ->
|
|
382
|
+
rotated.compress(Bitmap.CompressFormat.JPEG, 95, out)
|
|
298
383
|
}
|
|
299
384
|
|
|
300
|
-
|
|
301
|
-
|
|
385
|
+
if (rotated != bitmap) {
|
|
386
|
+
rotated.recycle()
|
|
302
387
|
}
|
|
303
|
-
|
|
388
|
+
bitmap.recycle()
|
|
389
|
+
|
|
390
|
+
pending.onImageCaptured(photoFile)
|
|
391
|
+
} catch (e: Exception) {
|
|
392
|
+
pending.onError(e)
|
|
393
|
+
} finally {
|
|
394
|
+
image.close()
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private fun closeSession() {
|
|
399
|
+
try {
|
|
400
|
+
captureSession?.close()
|
|
401
|
+
captureSession = null
|
|
402
|
+
cameraDevice?.close()
|
|
403
|
+
cameraDevice = null
|
|
404
|
+
} catch (e: Exception) {
|
|
405
|
+
Log.e(TAG, "[CAMERA2] Error closing camera", e)
|
|
406
|
+
} finally {
|
|
407
|
+
yuvReader?.close()
|
|
408
|
+
jpegReader?.close()
|
|
409
|
+
yuvReader = null
|
|
410
|
+
jpegReader = null
|
|
411
|
+
previewRequestBuilder = null
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private fun computeRotationDegrees(): Int {
|
|
416
|
+
val cameraId = selectCameraId() ?: return 0
|
|
417
|
+
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
|
|
418
|
+
val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
|
|
419
|
+
val displayRotation = displayRotationDegrees()
|
|
420
|
+
return if (useFrontCamera) {
|
|
421
|
+
(sensorOrientation + displayRotation) % 360
|
|
422
|
+
} else {
|
|
423
|
+
(sensorOrientation - displayRotation + 360) % 360
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private fun displayRotationDegrees(): Int {
|
|
428
|
+
val rotation = previewView.display?.rotation ?: Surface.ROTATION_0
|
|
429
|
+
return when (rotation) {
|
|
430
|
+
Surface.ROTATION_0 -> 0
|
|
431
|
+
Surface.ROTATION_90 -> 90
|
|
432
|
+
Surface.ROTATION_180 -> 180
|
|
433
|
+
Surface.ROTATION_270 -> 270
|
|
434
|
+
else -> 0
|
|
435
|
+
}
|
|
304
436
|
}
|
|
305
437
|
|
|
306
|
-
private fun
|
|
307
|
-
val
|
|
308
|
-
val
|
|
309
|
-
|
|
310
|
-
|
|
438
|
+
private fun configureTransform() {
|
|
439
|
+
val viewWidth = previewView.width.toFloat()
|
|
440
|
+
val viewHeight = previewView.height.toFloat()
|
|
441
|
+
val preview = previewSize ?: return
|
|
442
|
+
if (viewWidth == 0f || viewHeight == 0f) return
|
|
443
|
+
|
|
444
|
+
val rotation = displayRotationDegrees()
|
|
445
|
+
val bufferWidth = if (rotation == 90 || rotation == 270) preview.height.toFloat() else preview.width.toFloat()
|
|
446
|
+
val bufferHeight = if (rotation == 90 || rotation == 270) preview.width.toFloat() else preview.height.toFloat()
|
|
447
|
+
|
|
448
|
+
val scale = max(viewWidth / bufferWidth, viewHeight / bufferHeight)
|
|
449
|
+
val matrix = Matrix()
|
|
450
|
+
val centerX = viewWidth / 2f
|
|
451
|
+
val centerY = viewHeight / 2f
|
|
452
|
+
|
|
453
|
+
matrix.setScale(scale, scale, centerX, centerY)
|
|
454
|
+
matrix.postRotate(rotation.toFloat(), centerX, centerY)
|
|
455
|
+
previewView.setTransform(matrix)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private fun chooseBestSize(sizes: Array<Size>?, targetAspect: Double, maxArea: Int?): Size? {
|
|
459
|
+
if (sizes == null || sizes.isEmpty()) return null
|
|
460
|
+
val sorted = sizes.sortedByDescending { it.width * it.height }
|
|
461
|
+
|
|
462
|
+
val matching = sorted.filter {
|
|
463
|
+
val aspect = it.width.toDouble() / it.height.toDouble()
|
|
464
|
+
abs(aspect - targetAspect) <= ANALYSIS_ASPECT_TOLERANCE && (maxArea == null || it.width * it.height <= maxArea)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (matching.isNotEmpty()) {
|
|
468
|
+
return matching.first()
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
val capped = if (maxArea != null) {
|
|
472
|
+
sorted.filter { it.width * it.height <= maxArea }
|
|
473
|
+
} else {
|
|
474
|
+
sorted
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return capped.firstOrNull() ?: sorted.first()
|
|
311
478
|
}
|
|
312
479
|
|
|
313
480
|
private fun rotateAndMirror(bitmap: Bitmap, rotationDegrees: Int, mirror: Boolean): Bitmap {
|
|
@@ -324,7 +491,66 @@ class CameraController(
|
|
|
324
491
|
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
|
325
492
|
}
|
|
326
493
|
|
|
494
|
+
private fun Image.toNv21(): ByteArray {
|
|
495
|
+
val width = width
|
|
496
|
+
val height = height
|
|
497
|
+
val ySize = width * height
|
|
498
|
+
val uvSize = width * height / 2
|
|
499
|
+
val nv21 = ByteArray(ySize + uvSize)
|
|
500
|
+
|
|
501
|
+
val yBuffer = planes[0].buffer
|
|
502
|
+
val uBuffer = planes[1].buffer
|
|
503
|
+
val vBuffer = planes[2].buffer
|
|
504
|
+
|
|
505
|
+
val yRowStride = planes[0].rowStride
|
|
506
|
+
val yPixelStride = planes[0].pixelStride
|
|
507
|
+
var outputOffset = 0
|
|
508
|
+
for (row in 0 until height) {
|
|
509
|
+
var inputOffset = row * yRowStride
|
|
510
|
+
for (col in 0 until width) {
|
|
511
|
+
nv21[outputOffset++] = yBuffer.get(inputOffset)
|
|
512
|
+
inputOffset += yPixelStride
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
val uvRowStride = planes[1].rowStride
|
|
517
|
+
val uvPixelStride = planes[1].pixelStride
|
|
518
|
+
val vRowStride = planes[2].rowStride
|
|
519
|
+
val vPixelStride = planes[2].pixelStride
|
|
520
|
+
val uvHeight = height / 2
|
|
521
|
+
val uvWidth = width / 2
|
|
522
|
+
for (row in 0 until uvHeight) {
|
|
523
|
+
var uInputOffset = row * uvRowStride
|
|
524
|
+
var vInputOffset = row * vRowStride
|
|
525
|
+
for (col in 0 until uvWidth) {
|
|
526
|
+
nv21[outputOffset++] = vBuffer.get(vInputOffset)
|
|
527
|
+
nv21[outputOffset++] = uBuffer.get(uInputOffset)
|
|
528
|
+
uInputOffset += uvPixelStride
|
|
529
|
+
vInputOffset += vPixelStride
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return nv21
|
|
534
|
+
}
|
|
535
|
+
|
|
327
536
|
private fun hasCameraPermission(): Boolean {
|
|
328
537
|
return ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
|
329
538
|
}
|
|
539
|
+
|
|
540
|
+
private fun selectCameraId(): String? {
|
|
541
|
+
return try {
|
|
542
|
+
val desiredFacing = if (useFrontCamera) {
|
|
543
|
+
CameraCharacteristics.LENS_FACING_FRONT
|
|
544
|
+
} else {
|
|
545
|
+
CameraCharacteristics.LENS_FACING_BACK
|
|
546
|
+
}
|
|
547
|
+
cameraManager.cameraIdList.firstOrNull { id ->
|
|
548
|
+
val characteristics = cameraManager.getCameraCharacteristics(id)
|
|
549
|
+
characteristics.get(CameraCharacteristics.LENS_FACING) == desiredFacing
|
|
550
|
+
} ?: cameraManager.cameraIdList.firstOrNull()
|
|
551
|
+
} catch (e: Exception) {
|
|
552
|
+
Log.e(TAG, "[CAMERA2] Failed to select camera", e)
|
|
553
|
+
null
|
|
554
|
+
}
|
|
555
|
+
}
|
|
330
556
|
}
|
|
@@ -8,9 +8,9 @@ import android.graphics.Paint
|
|
|
8
8
|
import android.graphics.PorterDuff
|
|
9
9
|
import android.graphics.PorterDuffXfermode
|
|
10
10
|
import android.util.Log
|
|
11
|
+
import android.view.TextureView
|
|
11
12
|
import android.view.View
|
|
12
13
|
import android.widget.FrameLayout
|
|
13
|
-
import androidx.camera.view.PreviewView
|
|
14
14
|
import androidx.lifecycle.Lifecycle
|
|
15
15
|
import androidx.lifecycle.LifecycleOwner
|
|
16
16
|
import androidx.lifecycle.LifecycleRegistry
|
|
@@ -25,7 +25,7 @@ import kotlin.math.min
|
|
|
25
25
|
|
|
26
26
|
class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), LifecycleOwner {
|
|
27
27
|
private val themedContext = context
|
|
28
|
-
private val previewView:
|
|
28
|
+
private val previewView: TextureView
|
|
29
29
|
private val overlayView: OverlayView
|
|
30
30
|
private var cameraController: CameraController? = null
|
|
31
31
|
private val lifecycleRegistry = LifecycleRegistry(this)
|
|
@@ -74,13 +74,10 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
74
74
|
|
|
75
75
|
// Create preview view
|
|
76
76
|
Log.d(TAG, "[INIT] Creating PreviewView...")
|
|
77
|
-
previewView =
|
|
77
|
+
previewView = TextureView(context).apply {
|
|
78
78
|
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
79
79
|
visibility = View.VISIBLE
|
|
80
80
|
keepScreenOn = true
|
|
81
|
-
// TextureView mode avoids some device-specific Camera2 session timeouts.
|
|
82
|
-
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
|
|
83
|
-
scaleType = PreviewView.ScaleType.FILL_CENTER
|
|
84
81
|
}
|
|
85
82
|
Log.d(TAG, "[INIT] PreviewView created: $previewView")
|
|
86
83
|
Log.d(TAG, "[INIT] PreviewView visibility: ${previewView.visibility}")
|
package/package.json
CHANGED