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 android.view.TextureView
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 java.io.ByteArrayOutputStream
20
+ import com.google.common.util.concurrent.ListenableFuture
31
21
  import java.io.File
32
- import java.io.FileOutputStream
33
- import java.util.concurrent.atomic.AtomicBoolean
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: TextureView
28
+ private val previewView: PreviewView
39
29
  ) {
40
- private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
41
- private var cameraDevice: CameraDevice? = null
42
- private var captureSession: CameraCaptureSession? = null
43
- private var previewRequestBuilder: CaptureRequest.Builder? = null
44
- private var imageReader: ImageReader? = null
45
- private var backgroundThread: HandlerThread? = null
46
- private var backgroundHandler: Handler? = null
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 MAX_ANALYSIS_WIDTH = 1280
70
- private const val MAX_ANALYSIS_HEIGHT = 720
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, "[CAMERA2] Camera permission not granted")
58
+ Log.e(TAG, "[CAMERAX] Camera permission not granted")
122
59
  return
123
60
  }
124
61
 
125
- startBackgroundThread()
126
- chooseCamera()
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
- if (previewView.isAvailable) {
136
- openCamera()
137
- } else {
138
- previewView.surfaceTextureListener = textureListener
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, "[CAMERA2] stopCamera called")
144
- previewLayoutListener?.let { listener ->
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 frame = synchronized(lastFrameLock) { lastFrame }
172
- if (frame == null) {
173
- onError(Exception("No frame available for capture"))
86
+ val capture = imageCapture
87
+ if (capture == null) {
88
+ onError(IllegalStateException("ImageCapture not initialized"))
174
89
  return
175
90
  }
176
91
 
177
- backgroundHandler?.post {
178
- try {
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
- val jpegBytes = nv21ToJpeg(frame.nv21, frame.width, frame.height, 95)
185
- val bitmap = BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size)
186
- ?: throw IllegalStateException("Failed to decode JPEG")
187
-
188
- val rotated = rotateAndMirror(bitmap, frame.rotationDegrees, frame.isFront)
189
- FileOutputStream(photoFile).use { out ->
190
- rotated.compress(Bitmap.CompressFormat.JPEG, 95, out)
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
- Log.d(TAG, "[CAMERA2] Photo capture succeeded: ${photoFile.absolutePath}")
198
- onImageCaptured(photoFile)
199
- } catch (e: Exception) {
200
- Log.e(TAG, "[CAMERA2] Photo capture failed", e)
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
- torchEnabled = enabled
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
- stopCamera()
220
- startCamera(useFrontCamera, detectionEnabled)
118
+ bindCameraUseCases()
221
119
  }
222
120
 
223
121
  fun isTorchAvailable(): Boolean {
224
- val id = cameraId ?: return false
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. Camera2 focus metering can be added if needed.
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 chooseCamera() {
238
- val lensFacing = if (useFrontCamera) {
239
- CameraCharacteristics.LENS_FACING_FRONT
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
- if (selected == null) {
251
- Log.e(TAG, "[CAMERA2] No camera available")
252
- return
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
- cameraId = selected
256
- val characteristics = cameraManager.getCameraCharacteristics(selected)
257
- sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
258
- val activeArray = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)
259
- sensorAspectRatio = activeArray?.let { rect ->
260
- if (rect.height() != 0) rect.width().toFloat() / rect.height().toFloat() else null
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
- val streamConfig = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
264
- previewChoices = streamConfig?.getOutputSizes(SurfaceTexture::class.java) ?: emptyArray()
265
- analysisChoices = streamConfig?.getOutputSizes(ImageFormat.YUV_420_888) ?: emptyArray()
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 viewWidth = if (previewView.width > 0) previewView.width else context.resources.displayMetrics.widthPixels
268
- val viewHeight = if (previewView.height > 0) previewView.height else context.resources.displayMetrics.heightPixels
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
- null
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
- cameraManager.openCamera(id, object : CameraDevice.StateCallback() {
299
- override fun onOpened(device: CameraDevice) {
300
- Log.d(TAG, "[CAMERA2] Camera opened")
301
- isOpening.set(false)
302
- cameraDevice = device
303
- createPreviewSession()
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, "[CAMERA2] Failed to create preview session", e)
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 fun updatePreviewTransform() {
427
- val previewSize = previewSize ?: return
428
- ensureMatchParent()
429
-
430
- val viewWidth = previewView.width
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
- private fun logSizeCandidates(
658
- label: String,
659
- choices: Array<Size>,
660
- targetRatio: Float?,
661
- sensorRatio: Float?
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
- val ratioBase = sensorRatio ?: targetRatio
669
- if (ratioBase == null) {
670
- Log.d(TAG, "[CAMERA2] $label sizes: ${choices.size}, ratioBase=null")
671
- return
672
- }
194
+ val frameHeight = if (rotationDegrees == 90 || rotationDegrees == 270) {
195
+ imageProxy.width
196
+ } else {
197
+ imageProxy.height
198
+ }
673
199
 
674
- val normalizedTarget = ratioBase
675
- val sorted = choices.sortedWith(
676
- compareBy<Size> { size ->
677
- val ratio = size.width.toFloat() / size.height.toFloat()
678
- kotlin.math.abs(ratio - normalizedTarget)
679
- }.thenByDescending { size ->
680
- size.width * size.height
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: TextureView
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 TextureView...")
77
- previewView = TextureView(context).apply {
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
- // Force view to be drawn
82
- setWillNotDraw(false)
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] TextureView created: $previewView")
88
- Log.d(TAG, "[INIT] TextureView visibility: ${previewView.visibility}")
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 TextureView to parent...")
87
+ Log.d(TAG, "[INIT] Adding PreviewView to parent...")
91
88
  addView(previewView)
92
- Log.d(TAG, "[INIT] TextureView added, childCount: $childCount")
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.209.0",
3
+ "version": "3.211.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",