react-native-rectangle-doc-scanner 13.2.0 → 13.3.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.
@@ -53,7 +53,9 @@ class CameraController(
53
53
 
54
54
  companion object {
55
55
  private const val TAG = "CameraController"
56
- private const val ANALYSIS_TARGET_RESOLUTION = 1920 // Max dimension for analysis
56
+ // Increased resolution for better corner detection (was causing 640x480 downscale)
57
+ private const val ANALYSIS_TARGET_WIDTH = 1280
58
+ private const val ANALYSIS_TARGET_HEIGHT = 960
57
59
  }
58
60
 
59
61
  private fun getCameraSensorOrientation(): Int {
@@ -183,11 +185,11 @@ class CameraController(
183
185
  Log.d(TAG, "[CAMERAX] SurfaceProvider set successfully")
184
186
  }
185
187
 
186
- // ImageAnalysis UseCase for document detection
188
+ // ImageAnalysis UseCase for document detection with fixed high resolution
187
189
  imageAnalyzer = ImageAnalysis.Builder()
188
190
  .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
189
- // Match preview aspect ratio to avoid square analysis frames on some devices.
190
- .setTargetAspectRatio(AspectRatio.RATIO_4_3)
191
+ // Use explicit resolution instead of aspect ratio to prevent 640x480 downscale
192
+ .setTargetResolution(Size(ANALYSIS_TARGET_WIDTH, ANALYSIS_TARGET_HEIGHT))
191
193
  .setTargetRotation(targetRotation) // Match preview rotation
192
194
  .build()
193
195
  .also {
@@ -67,7 +67,8 @@ 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
+ // Removed fixed aspect ratio to prevent squished preview on tablets
71
+ // Preview will now properly fill the view using center-crop scaling
71
72
  }
72
73
 
73
74
  override val lifecycle: Lifecycle
@@ -150,18 +151,10 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
150
151
  private fun layoutPreviewAndOverlay(viewWidth: Int, viewHeight: Int) {
151
152
  if (viewWidth <= 0 || viewHeight <= 0) return
152
153
 
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)
154
+ // Fill entire view to prevent squished appearance on tablets
155
+ // CameraX will handle aspect ratio via center-crop in updateTextureViewTransform
156
+ previewView.layout(0, 0, viewWidth, viewHeight)
157
+ overlayView.layout(0, 0, viewWidth, viewHeight)
165
158
  }
166
159
 
167
160
  private fun initializeCameraWhenReady() {
@@ -264,36 +257,72 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
264
257
  }
265
258
  }
266
259
 
260
+ // Enhanced snap: Track corner movement per frame
261
+ private var consecutiveStableFrames = 0
262
+ private val requiredStableFrames = 3 // Require 3 consecutive stable frames for snap
263
+
267
264
  private fun smoothRectangle(rectangle: Rectangle?, viewWidth: Int, viewHeight: Int): Rectangle? {
268
265
  if (rectangle == null || viewWidth <= 0 || viewHeight <= 0) {
269
266
  lastSmoothedRectangleOnScreen = null
267
+ consecutiveStableFrames = 0
270
268
  return null
271
269
  }
272
270
 
273
271
  val prev = lastSmoothedRectangleOnScreen
274
272
  if (prev == null) {
275
273
  lastSmoothedRectangleOnScreen = rectangle
274
+ consecutiveStableFrames = 0
276
275
  return rectangle
277
276
  }
278
277
 
279
278
  val minDim = min(viewWidth, viewHeight).toDouble()
280
- val snapThreshold = max(12.0, minDim * 0.015)
281
- val smoothThreshold = max(24.0, minDim * 0.06)
282
- val maxCornerDistance = max(
279
+ // Tighter snap threshold for better stability
280
+ val snapThreshold = max(10.0, minDim * 0.012)
281
+ val smoothThreshold = max(20.0, minDim * 0.05)
282
+
283
+ // Calculate max corner movement
284
+ val maxCornerDist = max(
283
285
  maxCornerDistance(prev, rectangle),
284
286
  maxCornerDistance(rectangle, prev)
285
287
  )
286
288
 
289
+ // Enhanced snap logic: Check individual corner distances
290
+ val allCornersStable = areAllCornersStable(prev, rectangle, snapThreshold)
291
+
287
292
  val result = when {
288
- maxCornerDistance <= snapThreshold -> prev
289
- maxCornerDistance <= smoothThreshold -> blendRectangle(prev, rectangle, 0.35)
290
- else -> rectangle
293
+ // Snap only if all corners are stable AND we have consecutive stable frames
294
+ allCornersStable && maxCornerDist <= snapThreshold -> {
295
+ consecutiveStableFrames++
296
+ if (consecutiveStableFrames >= requiredStableFrames) {
297
+ prev // Lock to previous position
298
+ } else {
299
+ blendRectangle(prev, rectangle, 0.25) // Small blend while building stability
300
+ }
301
+ }
302
+ maxCornerDist <= smoothThreshold -> {
303
+ consecutiveStableFrames = 0
304
+ blendRectangle(prev, rectangle, 0.35)
305
+ }
306
+ else -> {
307
+ consecutiveStableFrames = 0
308
+ rectangle
309
+ }
291
310
  }
292
311
 
293
312
  lastSmoothedRectangleOnScreen = result
294
313
  return result
295
314
  }
296
315
 
316
+ /**
317
+ * Check if all 4 corners are within the snap threshold (not just max distance)
318
+ */
319
+ private fun areAllCornersStable(prev: Rectangle, current: Rectangle, threshold: Double): Boolean {
320
+ return distance(prev.topLeft, current.topLeft) <= threshold &&
321
+ distance(prev.topRight, current.topRight) <= threshold &&
322
+ distance(prev.bottomLeft, current.bottomLeft) <= threshold &&
323
+ distance(prev.bottomRight, current.bottomRight) <= threshold
324
+ }
325
+
297
326
  private fun maxCornerDistance(a: Rectangle, b: Rectangle): Double {
298
327
  return max(
299
328
  max(
@@ -436,9 +436,101 @@ class DocumentDetector {
436
436
  if (aspect < 0.45 || aspect > 2.8) {
437
437
  return false
438
438
  }
439
+
440
+ // Enhanced validation: Check corner angles (should be close to 90°)
441
+ if (!hasValidCornerAngles(rectangle)) {
442
+ return false
443
+ }
444
+
445
+ // Enhanced validation: Check edge straightness
446
+ if (!hasValidEdgeStraightness(rectangle, srcMat)) {
447
+ return false
448
+ }
449
+
450
+ // Enhanced validation: Check margin from view edges (avoid detecting screen borders)
451
+ val margin = minDim * 0.05 // 5% margin
452
+ if (rectangle.topLeft.x < margin || rectangle.topLeft.y < margin ||
453
+ rectangle.topRight.x > srcMat.cols() - margin || rectangle.topRight.y < margin ||
454
+ rectangle.bottomLeft.x < margin || rectangle.bottomLeft.y > srcMat.rows() - margin ||
455
+ rectangle.bottomRight.x > srcMat.cols() - margin || rectangle.bottomRight.y > srcMat.rows() - margin) {
456
+ return false
457
+ }
458
+
439
459
  return true
440
460
  }
441
461
 
462
+ /**
463
+ * Check if all corners have angles close to 90° (within 60°-120° range)
464
+ */
465
+ private fun hasValidCornerAngles(rectangle: Rectangle): Boolean {
466
+ fun angleAt(p1: Point, vertex: Point, p2: Point): Double {
467
+ val v1x = p1.x - vertex.x
468
+ val v1y = p1.y - vertex.y
469
+ val v2x = p2.x - vertex.x
470
+ val v2y = p2.y - vertex.y
471
+
472
+ val dot = v1x * v2x + v1y * v2y
473
+ val len1 = sqrt(v1x * v1x + v1y * v1y)
474
+ val len2 = sqrt(v2x * v2x + v2y * v2y)
475
+
476
+ if (len1 < 1.0 || len2 < 1.0) return 90.0
477
+
478
+ val cosAngle = (dot / (len1 * len2)).coerceIn(-1.0, 1.0)
479
+ return Math.toDegrees(kotlin.math.acos(cosAngle))
480
+ }
481
+
482
+ val angleTL = angleAt(rectangle.topRight, rectangle.topLeft, rectangle.bottomLeft)
483
+ val angleTR = angleAt(rectangle.topLeft, rectangle.topRight, rectangle.bottomRight)
484
+ val angleBL = angleAt(rectangle.topLeft, rectangle.bottomLeft, rectangle.bottomRight)
485
+ val angleBR = angleAt(rectangle.topRight, rectangle.bottomRight, rectangle.bottomLeft)
486
+
487
+ // All angles should be within 60°-120° (allow ±30° from 90°)
488
+ return angleTL in 60.0..120.0 && angleTR in 60.0..120.0 &&
489
+ angleBL in 60.0..120.0 && angleBR in 60.0..120.0
490
+ }
491
+
492
+ /**
493
+ * Check if edges are sufficiently straight (low deviation from fitted line)
494
+ */
495
+ private fun hasValidEdgeStraightness(rectangle: Rectangle, srcMat: Mat): Boolean {
496
+ val minDim = min(srcMat.cols(), srcMat.rows()).toDouble()
497
+ val maxDeviation = max(10.0, minDim * 0.02) // Allow 2% deviation
498
+
499
+ fun checkEdgeStraightness(p1: Point, p2: Point, p3: Point, p4: Point): Boolean {
500
+ // Check if points p1-p2 and p3-p4 form roughly parallel lines
501
+ val dx1 = p2.x - p1.x
502
+ val dy1 = p2.y - p1.y
503
+ val dx2 = p4.x - p3.x
504
+ val dy2 = p4.y - p3.y
505
+
506
+ val len1 = sqrt(dx1 * dx1 + dy1 * dy1)
507
+ val len2 = sqrt(dx2 * dx2 + dy2 * dy2)
508
+
509
+ if (len1 < 1.0 || len2 < 1.0) return true
510
+
511
+ // Normalize and compute angle difference
512
+ val dot = (dx1 * dx2 + dy1 * dy2) / (len1 * len2)
513
+ val angleDiff = Math.toDegrees(kotlin.math.acos(dot.coerceIn(-1.0, 1.0)))
514
+
515
+ // Parallel lines should have angle diff close to 0° or 180°
516
+ return angleDiff < 15.0 || angleDiff > 165.0
517
+ }
518
+
519
+ // Check top vs bottom edges
520
+ val topBottomOk = checkEdgeStraightness(
521
+ rectangle.topLeft, rectangle.topRight,
522
+ rectangle.bottomLeft, rectangle.bottomRight
523
+ )
524
+
525
+ // Check left vs right edges
526
+ val leftRightOk = checkEdgeStraightness(
527
+ rectangle.topLeft, rectangle.bottomLeft,
528
+ rectangle.topRight, rectangle.bottomRight
529
+ )
530
+
531
+ return topBottomOk && leftRightOk
532
+ }
533
+
442
534
  /**
443
535
  * Evaluate rectangle quality (matching iOS logic)
444
536
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "13.2.0",
3
+ "version": "13.3.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",