react-native-rectangle-doc-scanner 3.209.0 → 3.211.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,164 +3,79 @@ 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.ImageFormat
|
|
9
|
-
import android.graphics.Matrix
|
|
10
|
-
import android.graphics.Rect
|
|
11
|
-
import android.graphics.RectF
|
|
12
|
-
import android.graphics.SurfaceTexture
|
|
13
|
-
import android.graphics.YuvImage
|
|
14
|
-
import android.hardware.camera2.CameraCaptureSession
|
|
15
|
-
import android.hardware.camera2.CameraCharacteristics
|
|
16
|
-
import android.hardware.camera2.CameraDevice
|
|
17
|
-
import android.hardware.camera2.CameraManager
|
|
18
|
-
import android.hardware.camera2.CaptureRequest
|
|
19
|
-
import android.media.Image
|
|
20
|
-
import android.media.ImageReader
|
|
21
|
-
import android.os.Handler
|
|
22
|
-
import android.os.HandlerThread
|
|
23
6
|
import android.util.Log
|
|
24
7
|
import android.util.Size
|
|
25
|
-
import android.view.Gravity
|
|
26
8
|
import android.view.Surface
|
|
27
|
-
import
|
|
9
|
+
import androidx.camera.core.AspectRatio
|
|
10
|
+
import androidx.camera.core.Camera
|
|
11
|
+
import androidx.camera.core.CameraSelector
|
|
12
|
+
import androidx.camera.core.ImageAnalysis
|
|
13
|
+
import androidx.camera.core.ImageCapture
|
|
14
|
+
import androidx.camera.core.ImageCaptureException
|
|
15
|
+
import androidx.camera.core.Preview
|
|
16
|
+
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
17
|
+
import androidx.camera.view.PreviewView
|
|
28
18
|
import androidx.core.content.ContextCompat
|
|
29
19
|
import androidx.lifecycle.LifecycleOwner
|
|
30
|
-
import
|
|
20
|
+
import com.google.common.util.concurrent.ListenableFuture
|
|
31
21
|
import java.io.File
|
|
32
|
-
import java.
|
|
33
|
-
import java.util.concurrent.
|
|
22
|
+
import java.util.concurrent.ExecutorService
|
|
23
|
+
import java.util.concurrent.Executors
|
|
34
24
|
|
|
35
25
|
class CameraController(
|
|
36
26
|
private val context: Context,
|
|
37
27
|
private val lifecycleOwner: LifecycleOwner,
|
|
38
|
-
private val previewView:
|
|
28
|
+
private val previewView: PreviewView
|
|
39
29
|
) {
|
|
40
|
-
private
|
|
41
|
-
private var
|
|
42
|
-
private var
|
|
43
|
-
private var
|
|
44
|
-
private var
|
|
45
|
-
private var
|
|
46
|
-
private
|
|
47
|
-
private var previewLayoutListener: android.view.View.OnLayoutChangeListener? = null
|
|
30
|
+
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
|
|
31
|
+
private var cameraProvider: ProcessCameraProvider? = null
|
|
32
|
+
private var preview: Preview? = null
|
|
33
|
+
private var imageAnalysis: ImageAnalysis? = null
|
|
34
|
+
private var imageCapture: ImageCapture? = null
|
|
35
|
+
private var camera: Camera? = null
|
|
36
|
+
private val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
48
37
|
|
|
49
|
-
private var cameraId: String? = null
|
|
50
|
-
private var sensorOrientation: Int = 0
|
|
51
|
-
private var sensorAspectRatio: Float? = null
|
|
52
|
-
private var previewSize: Size? = null
|
|
53
|
-
private var analysisSize: Size? = null
|
|
54
|
-
private var previewChoices: Array<Size> = emptyArray()
|
|
55
|
-
private var analysisChoices: Array<Size> = emptyArray()
|
|
56
38
|
private var useFrontCamera = false
|
|
57
|
-
private var torchEnabled = false
|
|
58
39
|
private var detectionEnabled = true
|
|
59
|
-
private var hasStarted = false
|
|
60
|
-
|
|
61
|
-
private val isOpening = AtomicBoolean(false)
|
|
62
|
-
private val lastFrameLock = Any()
|
|
63
|
-
private var lastFrame: LastFrame? = null
|
|
64
40
|
|
|
65
41
|
var onFrameAnalyzed: ((Rectangle?, Int, Int) -> Unit)? = null
|
|
66
42
|
|
|
67
43
|
companion object {
|
|
68
44
|
private const val TAG = "CameraController"
|
|
69
|
-
private const val
|
|
70
|
-
private const val
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
private data class LastFrame(
|
|
74
|
-
val nv21: ByteArray,
|
|
75
|
-
val width: Int,
|
|
76
|
-
val height: Int,
|
|
77
|
-
val rotationDegrees: Int,
|
|
78
|
-
val isFront: Boolean
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
private val textureListener = object : TextureView.SurfaceTextureListener {
|
|
82
|
-
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
|
|
83
|
-
Log.d(TAG, "[CAMERA2] Texture available: ${width}x${height}")
|
|
84
|
-
createPreviewSession()
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
|
|
88
|
-
Log.d(TAG, "[CAMERA2] Texture size changed: ${width}x${height}")
|
|
89
|
-
updatePreviewTransform()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
|
|
93
|
-
Log.d(TAG, "[CAMERA2] Texture destroyed")
|
|
94
|
-
return true
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) = Unit
|
|
45
|
+
private const val ANALYSIS_WIDTH = 1280
|
|
46
|
+
private const val ANALYSIS_HEIGHT = 720
|
|
98
47
|
}
|
|
99
48
|
|
|
100
49
|
fun startCamera(
|
|
101
50
|
useFrontCam: Boolean = false,
|
|
102
51
|
enableDetection: Boolean = true
|
|
103
52
|
) {
|
|
104
|
-
Log.d(TAG, "
|
|
105
|
-
Log.d(TAG, "[CAMERA2] startCamera called")
|
|
106
|
-
Log.d(TAG, "[CAMERA2] useFrontCam: $useFrontCam")
|
|
107
|
-
Log.d(TAG, "[CAMERA2] enableDetection: $enableDetection")
|
|
108
|
-
Log.d(TAG, "[CAMERA2] lifecycleOwner: $lifecycleOwner")
|
|
109
|
-
Log.d(TAG, "========================================")
|
|
110
|
-
|
|
53
|
+
Log.d(TAG, "[CAMERAX] startCamera called")
|
|
111
54
|
this.useFrontCamera = useFrontCam
|
|
112
55
|
this.detectionEnabled = enableDetection
|
|
113
56
|
|
|
114
|
-
if (hasStarted) {
|
|
115
|
-
Log.d(TAG, "[CAMERA2] Already started, skipping")
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
hasStarted = true
|
|
119
|
-
|
|
120
57
|
if (!hasCameraPermission()) {
|
|
121
|
-
Log.e(TAG, "[
|
|
58
|
+
Log.e(TAG, "[CAMERAX] Camera permission not granted")
|
|
122
59
|
return
|
|
123
60
|
}
|
|
124
61
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (previewLayoutListener == null) {
|
|
129
|
-
previewLayoutListener = android.view.View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
|
130
|
-
updatePreviewTransform()
|
|
131
|
-
}
|
|
132
|
-
previewView.addOnLayoutChangeListener(previewLayoutListener)
|
|
62
|
+
if (cameraProviderFuture == null) {
|
|
63
|
+
cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
|
133
64
|
}
|
|
134
65
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
66
|
+
cameraProviderFuture?.addListener({
|
|
67
|
+
try {
|
|
68
|
+
cameraProvider = cameraProviderFuture?.get()
|
|
69
|
+
bindCameraUseCases()
|
|
70
|
+
} catch (e: Exception) {
|
|
71
|
+
Log.e(TAG, "[CAMERAX] Failed to get camera provider", e)
|
|
72
|
+
}
|
|
73
|
+
}, ContextCompat.getMainExecutor(context))
|
|
140
74
|
}
|
|
141
75
|
|
|
142
76
|
fun stopCamera() {
|
|
143
|
-
Log.d(TAG, "[
|
|
144
|
-
|
|
145
|
-
previewView.removeOnLayoutChangeListener(listener)
|
|
146
|
-
}
|
|
147
|
-
previewLayoutListener = null
|
|
148
|
-
try {
|
|
149
|
-
captureSession?.close()
|
|
150
|
-
captureSession = null
|
|
151
|
-
} catch (e: Exception) {
|
|
152
|
-
Log.w(TAG, "[CAMERA2] Failed to close session", e)
|
|
153
|
-
}
|
|
154
|
-
try {
|
|
155
|
-
cameraDevice?.close()
|
|
156
|
-
cameraDevice = null
|
|
157
|
-
} catch (e: Exception) {
|
|
158
|
-
Log.w(TAG, "[CAMERA2] Failed to close camera device", e)
|
|
159
|
-
}
|
|
160
|
-
imageReader?.close()
|
|
161
|
-
imageReader = null
|
|
162
|
-
stopBackgroundThread()
|
|
163
|
-
hasStarted = false
|
|
77
|
+
Log.d(TAG, "[CAMERAX] stopCamera called")
|
|
78
|
+
cameraProvider?.unbindAll()
|
|
164
79
|
}
|
|
165
80
|
|
|
166
81
|
fun capturePhoto(
|
|
@@ -168,551 +83,136 @@ class CameraController(
|
|
|
168
83
|
onImageCaptured: (File) -> Unit,
|
|
169
84
|
onError: (Exception) -> Unit
|
|
170
85
|
) {
|
|
171
|
-
val
|
|
172
|
-
if (
|
|
173
|
-
onError(
|
|
86
|
+
val capture = imageCapture
|
|
87
|
+
if (capture == null) {
|
|
88
|
+
onError(IllegalStateException("ImageCapture not initialized"))
|
|
174
89
|
return
|
|
175
90
|
}
|
|
176
91
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
val photoFile = File(
|
|
180
|
-
outputDirectory,
|
|
181
|
-
"doc_scan_${System.currentTimeMillis()}.jpg"
|
|
182
|
-
)
|
|
92
|
+
val photoFile = File(outputDirectory, "doc_scan_${System.currentTimeMillis()}.jpg")
|
|
93
|
+
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
|
|
183
94
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
if (rotated != bitmap) {
|
|
193
|
-
rotated.recycle()
|
|
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)
|
|
194
102
|
}
|
|
195
|
-
bitmap.recycle()
|
|
196
103
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
onError(e)
|
|
104
|
+
override fun onError(exception: ImageCaptureException) {
|
|
105
|
+
Log.e(TAG, "[CAMERAX] Photo capture failed", exception)
|
|
106
|
+
onError(exception)
|
|
107
|
+
}
|
|
202
108
|
}
|
|
203
|
-
|
|
109
|
+
)
|
|
204
110
|
}
|
|
205
111
|
|
|
206
112
|
fun setTorchEnabled(enabled: Boolean) {
|
|
207
|
-
|
|
208
|
-
val builder = previewRequestBuilder ?: return
|
|
209
|
-
builder.set(CaptureRequest.FLASH_MODE, if (enabled) CaptureRequest.FLASH_MODE_TORCH else CaptureRequest.FLASH_MODE_OFF)
|
|
210
|
-
try {
|
|
211
|
-
captureSession?.setRepeatingRequest(builder.build(), null, backgroundHandler)
|
|
212
|
-
} catch (e: Exception) {
|
|
213
|
-
Log.w(TAG, "[CAMERA2] Failed to update torch", e)
|
|
214
|
-
}
|
|
113
|
+
camera?.cameraControl?.enableTorch(enabled)
|
|
215
114
|
}
|
|
216
115
|
|
|
217
116
|
fun switchCamera() {
|
|
218
117
|
useFrontCamera = !useFrontCamera
|
|
219
|
-
|
|
220
|
-
startCamera(useFrontCamera, detectionEnabled)
|
|
118
|
+
bindCameraUseCases()
|
|
221
119
|
}
|
|
222
120
|
|
|
223
121
|
fun isTorchAvailable(): Boolean {
|
|
224
|
-
|
|
225
|
-
val characteristics = cameraManager.getCameraCharacteristics(id)
|
|
226
|
-
return characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true
|
|
122
|
+
return camera?.cameraInfo?.hasFlashUnit() == true
|
|
227
123
|
}
|
|
228
124
|
|
|
229
125
|
fun focusAt(x: Float, y: Float) {
|
|
230
|
-
// No-op for now.
|
|
126
|
+
// No-op for now.
|
|
231
127
|
}
|
|
232
128
|
|
|
233
129
|
fun shutdown() {
|
|
234
130
|
stopCamera()
|
|
131
|
+
cameraExecutor.shutdown()
|
|
235
132
|
}
|
|
236
133
|
|
|
237
|
-
private fun
|
|
238
|
-
val
|
|
239
|
-
|
|
240
|
-
} else {
|
|
241
|
-
CameraCharacteristics.LENS_FACING_BACK
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
val ids = cameraManager.cameraIdList
|
|
245
|
-
val selected = ids.firstOrNull { id ->
|
|
246
|
-
val characteristics = cameraManager.getCameraCharacteristics(id)
|
|
247
|
-
characteristics.get(CameraCharacteristics.LENS_FACING) == lensFacing
|
|
248
|
-
} ?: ids.firstOrNull()
|
|
134
|
+
private fun bindCameraUseCases() {
|
|
135
|
+
val provider = cameraProvider ?: return
|
|
136
|
+
provider.unbindAll()
|
|
249
137
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
138
|
+
val rotation = previewView.display?.rotation ?: Surface.ROTATION_0
|
|
139
|
+
preview = Preview.Builder()
|
|
140
|
+
.setTargetRotation(rotation)
|
|
141
|
+
.build()
|
|
142
|
+
.also {
|
|
143
|
+
it.setSurfaceProvider(previewView.surfaceProvider)
|
|
144
|
+
}
|
|
254
145
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
146
|
+
imageAnalysis = ImageAnalysis.Builder()
|
|
147
|
+
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
148
|
+
.setTargetResolution(Size(ANALYSIS_WIDTH, ANALYSIS_HEIGHT))
|
|
149
|
+
.setTargetRotation(rotation)
|
|
150
|
+
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
|
|
151
|
+
.build()
|
|
152
|
+
.also {
|
|
153
|
+
it.setAnalyzer(cameraExecutor, DocumentAnalyzer())
|
|
154
|
+
}
|
|
262
155
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
156
|
+
imageCapture = ImageCapture.Builder()
|
|
157
|
+
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
|
158
|
+
.setTargetRotation(rotation)
|
|
159
|
+
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
|
|
160
|
+
.build()
|
|
266
161
|
|
|
267
|
-
val
|
|
268
|
-
|
|
269
|
-
val targetRatio = if (viewWidth > 0 && viewHeight > 0) {
|
|
270
|
-
viewWidth.toFloat() / viewHeight.toFloat()
|
|
162
|
+
val cameraSelector = if (useFrontCamera) {
|
|
163
|
+
CameraSelector.DEFAULT_FRONT_CAMERA
|
|
271
164
|
} else {
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
logSizeCandidates("preview", previewChoices, targetRatio, sensorAspectRatio)
|
|
276
|
-
logSizeCandidates("analysis", analysisChoices, targetRatio, sensorAspectRatio)
|
|
277
|
-
|
|
278
|
-
previewSize = choosePreviewSize(previewChoices, targetRatio, sensorAspectRatio)
|
|
279
|
-
analysisSize = chooseAnalysisSize(analysisChoices, targetRatio, sensorAspectRatio)
|
|
280
|
-
Log.d(
|
|
281
|
-
TAG,
|
|
282
|
-
"[CAMERA2] chooseCamera view=${viewWidth}x${viewHeight} ratio=$targetRatio " +
|
|
283
|
-
"sensorOrientation=$sensorOrientation sensorRatio=$sensorAspectRatio " +
|
|
284
|
-
"preview=$previewSize analysis=$analysisSize"
|
|
285
|
-
)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
private fun openCamera() {
|
|
289
|
-
val id = cameraId ?: run {
|
|
290
|
-
Log.e(TAG, "[CAMERA2] Camera id not set")
|
|
291
|
-
return
|
|
292
|
-
}
|
|
293
|
-
if (isOpening.getAndSet(true)) {
|
|
294
|
-
return
|
|
165
|
+
CameraSelector.DEFAULT_BACK_CAMERA
|
|
295
166
|
}
|
|
296
167
|
|
|
297
168
|
try {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
override fun onDisconnected(device: CameraDevice) {
|
|
307
|
-
Log.w(TAG, "[CAMERA2] Camera disconnected")
|
|
308
|
-
isOpening.set(false)
|
|
309
|
-
device.close()
|
|
310
|
-
cameraDevice = null
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
override fun onError(device: CameraDevice, error: Int) {
|
|
314
|
-
Log.e(TAG, "[CAMERA2] Camera error: $error")
|
|
315
|
-
isOpening.set(false)
|
|
316
|
-
device.close()
|
|
317
|
-
cameraDevice = null
|
|
318
|
-
}
|
|
319
|
-
}, backgroundHandler)
|
|
320
|
-
} catch (e: SecurityException) {
|
|
321
|
-
isOpening.set(false)
|
|
322
|
-
Log.e(TAG, "[CAMERA2] Camera permission missing", e)
|
|
323
|
-
} catch (e: Exception) {
|
|
324
|
-
isOpening.set(false)
|
|
325
|
-
Log.e(TAG, "[CAMERA2] Failed to open camera", e)
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
private fun createPreviewSession() {
|
|
330
|
-
val device = cameraDevice ?: return
|
|
331
|
-
val texture = previewView.surfaceTexture ?: return
|
|
332
|
-
val sizes = ensurePreviewSizes()
|
|
333
|
-
val previewSize = sizes.first ?: return
|
|
334
|
-
val analysisSize = sizes.second ?: previewSize
|
|
335
|
-
|
|
336
|
-
Log.d(
|
|
337
|
-
TAG,
|
|
338
|
-
"[CAMERA2] createPreviewSession view=${previewView.width}x${previewView.height} " +
|
|
339
|
-
"preview=${previewSize.width}x${previewSize.height} analysis=${analysisSize.width}x${analysisSize.height}"
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
texture.setDefaultBufferSize(previewSize.width, previewSize.height)
|
|
343
|
-
val previewSurface = Surface(texture)
|
|
344
|
-
|
|
345
|
-
imageReader?.close()
|
|
346
|
-
imageReader = ImageReader.newInstance(
|
|
347
|
-
analysisSize.width,
|
|
348
|
-
analysisSize.height,
|
|
349
|
-
ImageFormat.YUV_420_888,
|
|
350
|
-
2
|
|
351
|
-
).apply {
|
|
352
|
-
setOnImageAvailableListener({ reader ->
|
|
353
|
-
val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener
|
|
354
|
-
handleImage(image)
|
|
355
|
-
}, backgroundHandler)
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
val surfaces = listOf(previewSurface, imageReader!!.surface)
|
|
359
|
-
try {
|
|
360
|
-
device.createCaptureSession(
|
|
361
|
-
surfaces,
|
|
362
|
-
object : CameraCaptureSession.StateCallback() {
|
|
363
|
-
override fun onConfigured(session: CameraCaptureSession) {
|
|
364
|
-
if (cameraDevice == null) {
|
|
365
|
-
return
|
|
366
|
-
}
|
|
367
|
-
captureSession = session
|
|
368
|
-
previewRequestBuilder = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
|
|
369
|
-
addTarget(previewSurface)
|
|
370
|
-
addTarget(imageReader!!.surface)
|
|
371
|
-
set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO)
|
|
372
|
-
set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
|
|
373
|
-
set(CaptureRequest.FLASH_MODE, if (torchEnabled) CaptureRequest.FLASH_MODE_TORCH else CaptureRequest.FLASH_MODE_OFF)
|
|
374
|
-
}
|
|
375
|
-
try {
|
|
376
|
-
session.setRepeatingRequest(previewRequestBuilder!!.build(), null, backgroundHandler)
|
|
377
|
-
Log.d(TAG, "[CAMERA2] Preview session started")
|
|
378
|
-
updatePreviewTransform()
|
|
379
|
-
} catch (e: Exception) {
|
|
380
|
-
Log.e(TAG, "[CAMERA2] Failed to start preview", e)
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
override fun onConfigureFailed(session: CameraCaptureSession) {
|
|
385
|
-
Log.e(TAG, "[CAMERA2] Preview session configure failed")
|
|
386
|
-
}
|
|
387
|
-
},
|
|
388
|
-
backgroundHandler
|
|
169
|
+
camera = provider.bindToLifecycle(
|
|
170
|
+
lifecycleOwner,
|
|
171
|
+
cameraSelector,
|
|
172
|
+
preview,
|
|
173
|
+
imageAnalysis,
|
|
174
|
+
imageCapture
|
|
389
175
|
)
|
|
176
|
+
Log.d(TAG, "[CAMERAX] Camera bound successfully")
|
|
390
177
|
} catch (e: Exception) {
|
|
391
|
-
Log.e(TAG, "[
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
private fun ensurePreviewSizes(): Pair<Size?, Size?> {
|
|
396
|
-
if (previewChoices.isEmpty()) {
|
|
397
|
-
return Pair(previewSize, analysisSize)
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
val viewWidth = if (previewView.width > 0) previewView.width else context.resources.displayMetrics.widthPixels
|
|
401
|
-
val viewHeight = if (previewView.height > 0) previewView.height else context.resources.displayMetrics.heightPixels
|
|
402
|
-
val targetRatio = if (viewWidth > 0 && viewHeight > 0) {
|
|
403
|
-
viewWidth.toFloat() / viewHeight.toFloat()
|
|
404
|
-
} else {
|
|
405
|
-
null
|
|
178
|
+
Log.e(TAG, "[CAMERAX] Failed to bind camera", e)
|
|
406
179
|
}
|
|
407
|
-
|
|
408
|
-
val newPreview = choosePreviewSize(previewChoices, targetRatio, sensorAspectRatio)
|
|
409
|
-
val newAnalysis = chooseAnalysisSize(analysisChoices, targetRatio, sensorAspectRatio)
|
|
410
|
-
|
|
411
|
-
if (newPreview != null && newPreview != previewSize) {
|
|
412
|
-
previewSize = newPreview
|
|
413
|
-
}
|
|
414
|
-
if (newAnalysis != null && newAnalysis != analysisSize) {
|
|
415
|
-
analysisSize = newAnalysis
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
Log.d(
|
|
419
|
-
TAG,
|
|
420
|
-
"[CAMERA2] ensurePreviewSizes view=${viewWidth}x${viewHeight} ratio=$targetRatio " +
|
|
421
|
-
"preview=${previewSize?.width}x${previewSize?.height} analysis=${analysisSize?.width}x${analysisSize?.height}"
|
|
422
|
-
)
|
|
423
|
-
return Pair(previewSize, analysisSize)
|
|
424
180
|
}
|
|
425
181
|
|
|
426
|
-
private
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
val viewHeight = previewView.height
|
|
432
|
-
if (viewWidth == 0 || viewHeight == 0) {
|
|
433
|
-
return
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
val rotationDegrees = getRotationDegrees()
|
|
437
|
-
val bufferRect = if (rotationDegrees == 90 || rotationDegrees == 270) {
|
|
438
|
-
RectF(0f, 0f, previewSize.height.toFloat(), previewSize.width.toFloat())
|
|
439
|
-
} else {
|
|
440
|
-
RectF(0f, 0f, previewSize.width.toFloat(), previewSize.height.toFloat())
|
|
441
|
-
}
|
|
442
|
-
val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat())
|
|
443
|
-
val centerX = viewRect.centerX()
|
|
444
|
-
val centerY = viewRect.centerY()
|
|
445
|
-
|
|
446
|
-
bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY())
|
|
447
|
-
|
|
448
|
-
val matrix = Matrix()
|
|
449
|
-
// Scale to fill (center-crop) and then apply rotation around center.
|
|
450
|
-
matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL)
|
|
451
|
-
if (rotationDegrees != 0) {
|
|
452
|
-
matrix.postRotate(rotationDegrees.toFloat(), centerX, centerY)
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
previewView.setTransform(matrix)
|
|
456
|
-
Log.d(
|
|
457
|
-
TAG,
|
|
458
|
-
"[CAMERA2] transform view=${viewWidth}x${viewHeight} buffer=${previewSize.width}x${previewSize.height} " +
|
|
459
|
-
"rotation=$rotationDegrees"
|
|
460
|
-
)
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
private fun ensureMatchParent() {
|
|
464
|
-
val parentView = previewView.parent as? android.view.View ?: return
|
|
465
|
-
val parentWidth = parentView.width
|
|
466
|
-
val parentHeight = parentView.height
|
|
467
|
-
if (parentWidth == 0 || parentHeight == 0) {
|
|
468
|
-
return
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
val layoutParams = (previewView.layoutParams as? android.widget.FrameLayout.LayoutParams)
|
|
472
|
-
?: android.widget.FrameLayout.LayoutParams(
|
|
473
|
-
android.widget.FrameLayout.LayoutParams.MATCH_PARENT,
|
|
474
|
-
android.widget.FrameLayout.LayoutParams.MATCH_PARENT
|
|
475
|
-
)
|
|
476
|
-
if (layoutParams.width != android.widget.FrameLayout.LayoutParams.MATCH_PARENT ||
|
|
477
|
-
layoutParams.height != android.widget.FrameLayout.LayoutParams.MATCH_PARENT
|
|
478
|
-
) {
|
|
479
|
-
layoutParams.width = android.widget.FrameLayout.LayoutParams.MATCH_PARENT
|
|
480
|
-
layoutParams.height = android.widget.FrameLayout.LayoutParams.MATCH_PARENT
|
|
481
|
-
layoutParams.gravity = Gravity.CENTER
|
|
482
|
-
previewView.layoutParams = layoutParams
|
|
483
|
-
}
|
|
484
|
-
Log.d(TAG, "[CAMERA2] parent=${parentWidth}x${parentHeight} previewView=${previewView.width}x${previewView.height}")
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
private fun handleImage(image: Image) {
|
|
488
|
-
try {
|
|
489
|
-
val rotationDegrees = getRotationDegrees()
|
|
490
|
-
val width = image.width
|
|
491
|
-
val height = image.height
|
|
492
|
-
val nv21 = imageToNV21(image)
|
|
493
|
-
|
|
494
|
-
val frameWidth = if (rotationDegrees == 90 || rotationDegrees == 270) height else width
|
|
495
|
-
val frameHeight = if (rotationDegrees == 90 || rotationDegrees == 270) width else height
|
|
496
|
-
|
|
497
|
-
synchronized(lastFrameLock) {
|
|
498
|
-
lastFrame = LastFrame(nv21, width, height, rotationDegrees, useFrontCamera)
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (detectionEnabled) {
|
|
502
|
-
val rectangle = DocumentDetector.detectRectangleInYUV(
|
|
503
|
-
nv21,
|
|
504
|
-
width,
|
|
505
|
-
height,
|
|
506
|
-
rotationDegrees
|
|
507
|
-
)
|
|
508
|
-
onFrameAnalyzed?.invoke(rectangle, frameWidth, frameHeight)
|
|
509
|
-
} else {
|
|
510
|
-
onFrameAnalyzed?.invoke(null, frameWidth, frameHeight)
|
|
511
|
-
}
|
|
512
|
-
} catch (e: Exception) {
|
|
513
|
-
Log.e(TAG, "[CAMERA2] Error analyzing frame", e)
|
|
514
|
-
} finally {
|
|
515
|
-
image.close()
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
private fun imageToNV21(image: Image): ByteArray {
|
|
520
|
-
val width = image.width
|
|
521
|
-
val height = image.height
|
|
522
|
-
|
|
523
|
-
val ySize = width * height
|
|
524
|
-
val uvSize = width * height / 2
|
|
525
|
-
val nv21 = ByteArray(ySize + uvSize)
|
|
526
|
-
|
|
527
|
-
val yBuffer = image.planes[0].buffer
|
|
528
|
-
val uBuffer = image.planes[1].buffer
|
|
529
|
-
val vBuffer = image.planes[2].buffer
|
|
530
|
-
|
|
531
|
-
val yRowStride = image.planes[0].rowStride
|
|
532
|
-
val yPixelStride = image.planes[0].pixelStride
|
|
533
|
-
var outputOffset = 0
|
|
534
|
-
for (row in 0 until height) {
|
|
535
|
-
var inputOffset = row * yRowStride
|
|
536
|
-
for (col in 0 until width) {
|
|
537
|
-
nv21[outputOffset++] = yBuffer.get(inputOffset)
|
|
538
|
-
inputOffset += yPixelStride
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
val uvRowStride = image.planes[1].rowStride
|
|
543
|
-
val uvPixelStride = image.planes[1].pixelStride
|
|
544
|
-
val vRowStride = image.planes[2].rowStride
|
|
545
|
-
val vPixelStride = image.planes[2].pixelStride
|
|
546
|
-
|
|
547
|
-
val uvHeight = height / 2
|
|
548
|
-
val uvWidth = width / 2
|
|
549
|
-
for (row in 0 until uvHeight) {
|
|
550
|
-
var uInputOffset = row * uvRowStride
|
|
551
|
-
var vInputOffset = row * vRowStride
|
|
552
|
-
for (col in 0 until uvWidth) {
|
|
553
|
-
nv21[outputOffset++] = vBuffer.get(vInputOffset)
|
|
554
|
-
nv21[outputOffset++] = uBuffer.get(uInputOffset)
|
|
555
|
-
uInputOffset += uvPixelStride
|
|
556
|
-
vInputOffset += vPixelStride
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
return nv21
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
private fun nv21ToJpeg(nv21: ByteArray, width: Int, height: Int, quality: Int): ByteArray {
|
|
564
|
-
val yuv = YuvImage(nv21, ImageFormat.NV21, width, height, null)
|
|
565
|
-
val out = ByteArrayOutputStream()
|
|
566
|
-
yuv.compressToJpeg(Rect(0, 0, width, height), quality, out)
|
|
567
|
-
return out.toByteArray()
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
private fun rotateAndMirror(bitmap: Bitmap, rotationDegrees: Int, mirror: Boolean): Bitmap {
|
|
571
|
-
if (rotationDegrees == 0 && !mirror) {
|
|
572
|
-
return bitmap
|
|
573
|
-
}
|
|
574
|
-
val matrix = Matrix()
|
|
575
|
-
if (mirror) {
|
|
576
|
-
matrix.postScale(-1f, 1f, bitmap.width / 2f, bitmap.height / 2f)
|
|
577
|
-
}
|
|
578
|
-
if (rotationDegrees != 0) {
|
|
579
|
-
matrix.postRotate(rotationDegrees.toFloat(), bitmap.width / 2f, bitmap.height / 2f)
|
|
580
|
-
}
|
|
581
|
-
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
private fun getRotationDegrees(): Int {
|
|
585
|
-
val displayRotation = previewView.display?.rotation ?: Surface.ROTATION_0
|
|
586
|
-
val displayDegrees = when (displayRotation) {
|
|
587
|
-
Surface.ROTATION_0 -> 0
|
|
588
|
-
Surface.ROTATION_90 -> 90
|
|
589
|
-
Surface.ROTATION_180 -> 180
|
|
590
|
-
Surface.ROTATION_270 -> 270
|
|
591
|
-
else -> 0
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
return if (useFrontCamera) {
|
|
595
|
-
(sensorOrientation + displayDegrees) % 360
|
|
596
|
-
} else {
|
|
597
|
-
(sensorOrientation - displayDegrees + 360) % 360
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
private fun choosePreviewSize(
|
|
602
|
-
choices: Array<Size>,
|
|
603
|
-
targetRatio: Float?,
|
|
604
|
-
sensorRatio: Float?
|
|
605
|
-
): Size? {
|
|
606
|
-
if (choices.isEmpty()) {
|
|
607
|
-
return null
|
|
608
|
-
}
|
|
609
|
-
val candidates = choices.toList()
|
|
610
|
-
|
|
611
|
-
val ratioBase = sensorRatio ?: targetRatio
|
|
612
|
-
if (ratioBase == null) {
|
|
613
|
-
return candidates.maxByOrNull { it.width * it.height }
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
val normalizedTarget = ratioBase
|
|
617
|
-
val sorted = candidates.sortedWith(
|
|
618
|
-
compareBy<Size> { size ->
|
|
619
|
-
val ratio = size.width.toFloat() / size.height.toFloat()
|
|
620
|
-
kotlin.math.abs(ratio - normalizedTarget)
|
|
621
|
-
}.thenByDescending { size ->
|
|
622
|
-
size.width * size.height
|
|
623
|
-
}
|
|
624
|
-
)
|
|
625
|
-
return sorted.first()
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
private fun chooseAnalysisSize(
|
|
629
|
-
choices: Array<Size>,
|
|
630
|
-
targetRatio: Float?,
|
|
631
|
-
sensorRatio: Float?
|
|
632
|
-
): Size? {
|
|
633
|
-
if (choices.isEmpty()) {
|
|
634
|
-
return null
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
val capped = choices.filter { it.width <= MAX_ANALYSIS_WIDTH && it.height <= MAX_ANALYSIS_HEIGHT }
|
|
638
|
-
val candidates = if (capped.isNotEmpty()) capped else choices.toList()
|
|
639
|
-
|
|
640
|
-
val ratioBase = sensorRatio ?: targetRatio
|
|
641
|
-
if (ratioBase == null) {
|
|
642
|
-
return candidates.maxByOrNull { it.width * it.height }
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
val normalizedTarget = ratioBase
|
|
646
|
-
val sorted = candidates.sortedWith(
|
|
647
|
-
compareBy<Size> { size ->
|
|
648
|
-
val ratio = size.width.toFloat() / size.height.toFloat()
|
|
649
|
-
kotlin.math.abs(ratio - normalizedTarget)
|
|
650
|
-
}.thenByDescending { size ->
|
|
651
|
-
size.width * size.height
|
|
652
|
-
}
|
|
653
|
-
)
|
|
654
|
-
return sorted.first()
|
|
655
|
-
}
|
|
182
|
+
private inner class DocumentAnalyzer : ImageAnalysis.Analyzer {
|
|
183
|
+
override fun analyze(imageProxy: androidx.camera.core.ImageProxy) {
|
|
184
|
+
try {
|
|
185
|
+
val rotationDegrees = imageProxy.imageInfo.rotationDegrees
|
|
186
|
+
val nv21 = imageProxy.toNv21()
|
|
656
187
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
) {
|
|
663
|
-
if (choices.isEmpty()) {
|
|
664
|
-
Log.d(TAG, "[CAMERA2] $label sizes: none")
|
|
665
|
-
return
|
|
666
|
-
}
|
|
188
|
+
val frameWidth = if (rotationDegrees == 90 || rotationDegrees == 270) {
|
|
189
|
+
imageProxy.height
|
|
190
|
+
} else {
|
|
191
|
+
imageProxy.width
|
|
192
|
+
}
|
|
667
193
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
194
|
+
val frameHeight = if (rotationDegrees == 90 || rotationDegrees == 270) {
|
|
195
|
+
imageProxy.width
|
|
196
|
+
} else {
|
|
197
|
+
imageProxy.height
|
|
198
|
+
}
|
|
673
199
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
200
|
+
if (detectionEnabled) {
|
|
201
|
+
val rectangle = DocumentDetector.detectRectangleInYUV(
|
|
202
|
+
nv21,
|
|
203
|
+
imageProxy.width,
|
|
204
|
+
imageProxy.height,
|
|
205
|
+
rotationDegrees
|
|
206
|
+
)
|
|
207
|
+
onFrameAnalyzed?.invoke(rectangle, frameWidth, frameHeight)
|
|
208
|
+
} else {
|
|
209
|
+
onFrameAnalyzed?.invoke(null, frameWidth, frameHeight)
|
|
210
|
+
}
|
|
211
|
+
} catch (e: Exception) {
|
|
212
|
+
Log.e(TAG, "[CAMERAX] Error analyzing frame", e)
|
|
213
|
+
} finally {
|
|
214
|
+
imageProxy.close()
|
|
681
215
|
}
|
|
682
|
-
)
|
|
683
|
-
|
|
684
|
-
val top = sorted.take(5).joinToString { size ->
|
|
685
|
-
val ratio = size.width.toFloat() / size.height.toFloat()
|
|
686
|
-
val diff = kotlin.math.abs(ratio - normalizedTarget)
|
|
687
|
-
"${size.width}x${size.height}(r=${"%.3f".format(ratio)},d=${"%.3f".format(diff)})"
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
Log.d(
|
|
691
|
-
TAG,
|
|
692
|
-
"[CAMERA2] $label sizes: ${choices.size}, ratioBase=${"%.3f".format(normalizedTarget)} " +
|
|
693
|
-
"sensor=${sensorRatio?.let { "%.3f".format(it) }} target=${targetRatio?.let { "%.3f".format(it) }} top=$top"
|
|
694
|
-
)
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
private fun startBackgroundThread() {
|
|
698
|
-
if (backgroundThread != null) {
|
|
699
|
-
return
|
|
700
|
-
}
|
|
701
|
-
backgroundThread = HandlerThread("Camera2Background").also {
|
|
702
|
-
it.start()
|
|
703
|
-
backgroundHandler = Handler(it.looper)
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
private fun stopBackgroundThread() {
|
|
708
|
-
try {
|
|
709
|
-
backgroundThread?.quitSafely()
|
|
710
|
-
backgroundThread?.join()
|
|
711
|
-
} catch (e: InterruptedException) {
|
|
712
|
-
Log.w(TAG, "[CAMERA2] Background thread shutdown interrupted", e)
|
|
713
|
-
} finally {
|
|
714
|
-
backgroundThread = null
|
|
715
|
-
backgroundHandler = null
|
|
716
216
|
}
|
|
717
217
|
}
|
|
718
218
|
|
|
@@ -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
|
|
12
11
|
import android.view.View
|
|
13
12
|
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: PreviewView
|
|
29
29
|
private val overlayView: OverlayView
|
|
30
30
|
private var cameraController: CameraController? = null
|
|
31
31
|
private val lifecycleRegistry = LifecycleRegistry(this)
|
|
@@ -73,23 +73,20 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
73
73
|
Log.d(TAG, "[INIT] Lifecycle state: ${lifecycleRegistry.currentState}")
|
|
74
74
|
|
|
75
75
|
// Create preview view
|
|
76
|
-
Log.d(TAG, "[INIT] Creating
|
|
77
|
-
previewView =
|
|
76
|
+
Log.d(TAG, "[INIT] Creating PreviewView...")
|
|
77
|
+
previewView = PreviewView(context).apply {
|
|
78
78
|
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
79
79
|
visibility = View.VISIBLE
|
|
80
80
|
keepScreenOn = true
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// Ensure the view is on top
|
|
84
|
-
bringToFront()
|
|
85
|
-
requestLayout()
|
|
81
|
+
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
|
|
82
|
+
scaleType = PreviewView.ScaleType.FILL_CENTER
|
|
86
83
|
}
|
|
87
|
-
Log.d(TAG, "[INIT]
|
|
88
|
-
Log.d(TAG, "[INIT]
|
|
84
|
+
Log.d(TAG, "[INIT] PreviewView created: $previewView")
|
|
85
|
+
Log.d(TAG, "[INIT] PreviewView visibility: ${previewView.visibility}")
|
|
89
86
|
|
|
90
|
-
Log.d(TAG, "[INIT] Adding
|
|
87
|
+
Log.d(TAG, "[INIT] Adding PreviewView to parent...")
|
|
91
88
|
addView(previewView)
|
|
92
|
-
Log.d(TAG, "[INIT]
|
|
89
|
+
Log.d(TAG, "[INIT] PreviewView added, childCount: $childCount")
|
|
93
90
|
|
|
94
91
|
// Create overlay view for drawing rectangle
|
|
95
92
|
Log.d(TAG, "[INIT] Creating OverlayView...")
|
package/package.json
CHANGED