react-native-rectangle-doc-scanner 13.0.0 → 13.2.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.
@@ -123,6 +123,7 @@ class CameraController(
123
123
  Log.d(TAG, "[CAMERAX] Setting target rotation to ROTATION_0 (portrait-only app)")
124
124
 
125
125
  preview = Preview.Builder()
126
+ .setTargetAspectRatio(AspectRatio.RATIO_4_3)
126
127
  .setTargetRotation(targetRotation) // Force portrait
127
128
  .build()
128
129
  .also { previewUseCase ->
@@ -185,7 +186,8 @@ class CameraController(
185
186
  // ImageAnalysis UseCase for document detection
186
187
  imageAnalyzer = ImageAnalysis.Builder()
187
188
  .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
188
- .setTargetResolution(android.util.Size(1920, 1440)) // Higher resolution for better small-edge detection
189
+ // Match preview aspect ratio to avoid square analysis frames on some devices.
190
+ .setTargetAspectRatio(AspectRatio.RATIO_4_3)
189
191
  .setTargetRotation(targetRotation) // Match preview rotation
190
192
  .build()
191
193
  .also {
@@ -201,6 +203,7 @@ class CameraController(
201
203
  // ImageCapture UseCase
202
204
  imageCapture = ImageCapture.Builder()
203
205
  .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
206
+ .setTargetAspectRatio(AspectRatio.RATIO_4_3)
204
207
  .setTargetRotation(targetRotation) // Match preview rotation
205
208
  .build()
206
209
 
@@ -265,10 +268,11 @@ class CameraController(
265
268
  return
266
269
  }
267
270
 
268
- val rotationDegrees = imageProxy.imageInfo.rotationDegrees
269
271
  val imageWidth = imageProxy.width
270
272
  val imageHeight = imageProxy.height
271
273
 
274
+ val rotationDegrees = imageProxy.imageInfo.rotationDegrees
275
+
272
276
  // Calculate rotation using the same logic as TextureView transform
273
277
  val sensorOrientation = getCameraSensorOrientation()
274
278
  val displayRotationDegrees = when (textureView.display?.rotation ?: Surface.ROTATION_0) {
@@ -281,16 +285,16 @@ class CameraController(
281
285
 
282
286
  // Use the same rotation logic as updateTextureViewTransform
283
287
  val tabletUpsideDownFix = if (sensorOrientation == 0 && displayRotationDegrees == 90) 180 else 0
284
- val effectiveRotation = if (sensorOrientation == 0) {
285
- (displayRotationDegrees + tabletUpsideDownFix) % 360
286
- } else {
287
- sensorOrientation
288
- }
288
+ val effectiveRotation = (displayRotationDegrees + tabletUpsideDownFix) % 360
289
289
 
290
- Log.d(TAG, "[ANALYZE] Sensor: $sensorOrientation°, Display: $displayRotationDegrees°, Effective: $effectiveRotation°")
290
+ Log.d(
291
+ TAG,
292
+ "[ANALYZE] Sensor: $sensorOrientation°, Display: $displayRotationDegrees°, " +
293
+ "ImageProxy: $rotationDegrees°, Effective: $effectiveRotation°"
294
+ )
291
295
 
292
- // Try ML Kit first
293
- val inputImage = InputImage.fromMediaImage(mediaImage, rotationDegrees)
296
+ // Try ML Kit first (use the same rotation as preview/OpenCV)
297
+ val inputImage = InputImage.fromMediaImage(mediaImage, effectiveRotation)
294
298
 
295
299
  objectDetector.process(inputImage)
296
300
  .addOnSuccessListener { objects ->
@@ -490,10 +494,10 @@ class CameraController(
490
494
  val finalWidth = imageWidth
491
495
  val finalHeight = imageHeight
492
496
 
493
- // Apply fit-center scaling to match TextureView display.
497
+ // Apply center-crop scaling to match TextureView display.
494
498
  val scaleX = viewWidth / finalWidth.toFloat()
495
499
  val scaleY = viewHeight / finalHeight.toFloat()
496
- val scale = scaleX.coerceAtMost(scaleY)
500
+ val scale = scaleX.coerceAtLeast(scaleY)
497
501
 
498
502
  val scaledWidth = finalWidth * scale
499
503
  val scaledHeight = finalHeight * scale
@@ -579,26 +583,17 @@ class CameraController(
579
583
  bufferHeight
580
584
  }
581
585
 
582
- // Scale to fit within the view while maintaining aspect ratio (no zoom/crop)
586
+ // Scale to fill the view while maintaining aspect ratio (center-crop)
583
587
  val scaleX = viewWidth.toFloat() / rotatedBufferWidth.toFloat()
584
588
  val scaleY = viewHeight.toFloat() / rotatedBufferHeight.toFloat()
585
- val scale = scaleX.coerceAtMost(scaleY) // Use min to fit
589
+ val scale = scaleX.coerceAtLeast(scaleY) // Use max to fill
586
590
 
587
591
  Log.d(TAG, "[TRANSFORM] Rotated buffer: ${rotatedBufferWidth}x${rotatedBufferHeight}, ScaleX: $scaleX, ScaleY: $scaleY, Using: $scale")
588
592
 
589
593
  matrix.postScale(scale, scale, centerX, centerY)
590
594
 
591
- // Track the actual preview viewport within the view for clipping overlays.
592
- val scaledWidth = rotatedBufferWidth * scale
593
- val scaledHeight = rotatedBufferHeight * scale
594
- val offsetX = (viewWidth - scaledWidth) / 2f
595
- val offsetY = (viewHeight - scaledHeight) / 2f
596
- previewViewport = android.graphics.RectF(
597
- offsetX,
598
- offsetY,
599
- offsetX + scaledWidth,
600
- offsetY + scaledHeight
601
- )
595
+ // With center-crop, the preview fully covers the view bounds.
596
+ previewViewport = android.graphics.RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat())
602
597
 
603
598
  textureView.setTransform(matrix)
604
599
  Log.d(TAG, "[TRANSFORM] Transform applied successfully")
@@ -67,6 +67,7 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
67
67
 
68
68
  companion object {
69
69
  private const val TAG = "DocumentScannerView"
70
+ private const val PREVIEW_ASPECT_RATIO = 3f / 4f // width:height (matches 3:4)
70
71
  }
71
72
 
72
73
  override val lifecycle: Lifecycle
@@ -111,6 +112,9 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
111
112
  addView(overlayView, 1) // Add at index 1 (front)
112
113
  Log.d(TAG, "[INIT] OverlayView added, childCount: $childCount")
113
114
 
115
+ // Match camera UI look with letterboxing when preview doesn't fill the view.
116
+ setBackgroundColor(android.graphics.Color.BLACK)
117
+
114
118
  Log.d(TAG, "╔════════════════════════════════════════╗")
115
119
  Log.d(TAG, "║ DocumentScannerView INIT COMPLETE ║")
116
120
  Log.d(TAG, "╚════════════════════════════════════════╝")
@@ -137,11 +141,29 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
137
141
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
138
142
  super.onLayout(changed, left, top, right, bottom)
139
143
  if (changed) {
144
+ layoutPreviewAndOverlay(right - left, bottom - top)
140
145
  Log.d(TAG, "[LAYOUT] View size: ${right - left}x${bottom - top}, PreviewView: ${previewView.width}x${previewView.height}")
141
146
  cameraController?.refreshTransform()
142
147
  }
143
148
  }
144
149
 
150
+ private fun layoutPreviewAndOverlay(viewWidth: Int, viewHeight: Int) {
151
+ if (viewWidth <= 0 || viewHeight <= 0) return
152
+
153
+ // Always fill width to avoid left/right letterboxing.
154
+ val targetWidth = viewWidth
155
+ val targetHeight = (viewWidth / PREVIEW_ASPECT_RATIO).toInt()
156
+
157
+ val left = 0
158
+ // Center vertically; allows top/bottom crop when targetHeight > viewHeight.
159
+ val top = (viewHeight - targetHeight) / 2
160
+ val right = left + targetWidth
161
+ val bottom = top + targetHeight
162
+
163
+ previewView.layout(left, top, right, bottom)
164
+ overlayView.layout(left, top, right, bottom)
165
+ }
166
+
145
167
  private fun initializeCameraWhenReady() {
146
168
  // If view is already laid out, start camera immediately
147
169
  if (width > 0 && height > 0) {
@@ -213,17 +235,26 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
213
235
  lastDetectedImageHeight = imageHeight
214
236
  }
215
237
 
216
- val rectangleOnScreen = if (rectangle != null && width > 0 && height > 0) {
238
+ val previewWidth = previewView.width
239
+ val previewHeight = previewView.height
240
+
241
+ val rectangleOnScreen = if (rectangle != null && previewWidth > 0 && previewHeight > 0) {
217
242
  cameraController?.mapRectangleToView(rectangle, imageWidth, imageHeight)
218
- ?: DocumentDetector.transformRectangleToViewCoordinates(rectangle, imageWidth, imageHeight, width, height)
243
+ ?: DocumentDetector.transformRectangleToViewCoordinates(
244
+ rectangle,
245
+ imageWidth,
246
+ imageHeight,
247
+ previewWidth,
248
+ previewHeight
249
+ )
219
250
  } else {
220
251
  null
221
252
  }
222
- val smoothedRectangleOnScreen = smoothRectangle(rectangleOnScreen, width, height)
253
+ val smoothedRectangleOnScreen = smoothRectangle(rectangleOnScreen, previewWidth, previewHeight)
223
254
  lastRectangleOnScreen = smoothedRectangleOnScreen
224
255
  val quality = when {
225
- smoothedRectangleOnScreen != null && width > 0 && height > 0 ->
226
- DocumentDetector.evaluateRectangleQualityInView(smoothedRectangleOnScreen, width, height)
256
+ smoothedRectangleOnScreen != null && previewWidth > 0 && previewHeight > 0 ->
257
+ DocumentDetector.evaluateRectangleQualityInView(smoothedRectangleOnScreen, previewWidth, previewHeight)
227
258
  rectangle != null -> DocumentDetector.evaluateRectangleQuality(rectangle, imageWidth, imageHeight)
228
259
  else -> RectangleQuality.TOO_FAR
229
260
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "13.0.0",
3
+ "version": "13.2.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",