react-native-rectangle-doc-scanner 13.2.0 → 13.4.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,25 @@ 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()
154
+ // Check if device is tablet (screen width >= 600dp)
155
+ val isTablet = (viewWidth / resources.displayMetrics.density) >= 600
156
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
157
+ if (isTablet) {
158
+ // Tablet: Fill entire view to prevent squished appearance
159
+ previewView.layout(0, 0, viewWidth, viewHeight)
160
+ overlayView.layout(0, 0, viewWidth, viewHeight)
161
+ } else {
162
+ // Phone: Use 4:3 aspect ratio with letterboxing
163
+ val targetAspectRatio = 3f / 4f // width/height
164
+ val targetWidth = viewWidth
165
+ val targetHeight = (viewWidth / targetAspectRatio).toInt()
166
+
167
+ val top = (viewHeight - targetHeight) / 2
168
+ val bottom = top + targetHeight
162
169
 
163
- previewView.layout(left, top, right, bottom)
164
- overlayView.layout(left, top, right, bottom)
170
+ previewView.layout(0, top, viewWidth, bottom)
171
+ overlayView.layout(0, top, viewWidth, bottom)
172
+ }
165
173
  }
166
174
 
167
175
  private fun initializeCameraWhenReady() {
@@ -264,36 +272,72 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
264
272
  }
265
273
  }
266
274
 
275
+ // Enhanced snap: Track corner movement per frame
276
+ private var consecutiveStableFrames = 0
277
+ private val requiredStableFrames = 3 // Require 3 consecutive stable frames for snap
278
+
267
279
  private fun smoothRectangle(rectangle: Rectangle?, viewWidth: Int, viewHeight: Int): Rectangle? {
268
280
  if (rectangle == null || viewWidth <= 0 || viewHeight <= 0) {
269
281
  lastSmoothedRectangleOnScreen = null
282
+ consecutiveStableFrames = 0
270
283
  return null
271
284
  }
272
285
 
273
286
  val prev = lastSmoothedRectangleOnScreen
274
287
  if (prev == null) {
275
288
  lastSmoothedRectangleOnScreen = rectangle
289
+ consecutiveStableFrames = 0
276
290
  return rectangle
277
291
  }
278
292
 
279
293
  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(
294
+ // Tighter snap threshold for better stability
295
+ val snapThreshold = max(10.0, minDim * 0.012)
296
+ val smoothThreshold = max(20.0, minDim * 0.05)
297
+
298
+ // Calculate max corner movement
299
+ val maxCornerDist = max(
283
300
  maxCornerDistance(prev, rectangle),
284
301
  maxCornerDistance(rectangle, prev)
285
302
  )
286
303
 
304
+ // Enhanced snap logic: Check individual corner distances
305
+ val allCornersStable = areAllCornersStable(prev, rectangle, snapThreshold)
306
+
287
307
  val result = when {
288
- maxCornerDistance <= snapThreshold -> prev
289
- maxCornerDistance <= smoothThreshold -> blendRectangle(prev, rectangle, 0.35)
290
- else -> rectangle
308
+ // Snap only if all corners are stable AND we have consecutive stable frames
309
+ allCornersStable && maxCornerDist <= snapThreshold -> {
310
+ consecutiveStableFrames++
311
+ if (consecutiveStableFrames >= requiredStableFrames) {
312
+ prev // Lock to previous position
313
+ } else {
314
+ blendRectangle(prev, rectangle, 0.25) // Small blend while building stability
315
+ }
316
+ }
317
+ maxCornerDist <= smoothThreshold -> {
318
+ consecutiveStableFrames = 0
319
+ blendRectangle(prev, rectangle, 0.35)
320
+ }
321
+ else -> {
322
+ consecutiveStableFrames = 0
323
+ rectangle
324
+ }
291
325
  }
292
326
 
293
327
  lastSmoothedRectangleOnScreen = result
294
328
  return result
295
329
  }
296
330
 
331
+ /**
332
+ * Check if all 4 corners are within the snap threshold (not just max distance)
333
+ */
334
+ private fun areAllCornersStable(prev: Rectangle, current: Rectangle, threshold: Double): Boolean {
335
+ return distance(prev.topLeft, current.topLeft) <= threshold &&
336
+ distance(prev.topRight, current.topRight) <= threshold &&
337
+ distance(prev.bottomLeft, current.bottomLeft) <= threshold &&
338
+ distance(prev.bottomRight, current.bottomRight) <= threshold
339
+ }
340
+
297
341
  private fun maxCornerDistance(a: Rectangle, b: Rectangle): Double {
298
342
  return max(
299
343
  max(
@@ -436,9 +436,104 @@ 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
+ // Made more lenient: 50°-130° (was 60°-120°)
442
+ if (!hasValidCornerAngles(rectangle)) {
443
+ return false
444
+ }
445
+
446
+ // Enhanced validation: Check edge straightness
447
+ // Made more lenient: 20° tolerance (was 15°)
448
+ if (!hasValidEdgeStraightness(rectangle, srcMat)) {
449
+ return false
450
+ }
451
+
452
+ // Enhanced validation: Check margin from view edges (avoid detecting screen borders)
453
+ // Made more lenient: 3% margin (was 5%)
454
+ val margin = minDim * 0.03
455
+ if (rectangle.topLeft.x < margin || rectangle.topLeft.y < margin ||
456
+ rectangle.topRight.x > srcMat.cols() - margin || rectangle.topRight.y < margin ||
457
+ rectangle.bottomLeft.x < margin || rectangle.bottomLeft.y > srcMat.rows() - margin ||
458
+ rectangle.bottomRight.x > srcMat.cols() - margin || rectangle.bottomRight.y > srcMat.rows() - margin) {
459
+ return false
460
+ }
461
+
439
462
  return true
440
463
  }
441
464
 
465
+ /**
466
+ * Check if all corners have angles close to 90° (within 60°-120° range)
467
+ */
468
+ private fun hasValidCornerAngles(rectangle: Rectangle): Boolean {
469
+ fun angleAt(p1: Point, vertex: Point, p2: Point): Double {
470
+ val v1x = p1.x - vertex.x
471
+ val v1y = p1.y - vertex.y
472
+ val v2x = p2.x - vertex.x
473
+ val v2y = p2.y - vertex.y
474
+
475
+ val dot = v1x * v2x + v1y * v2y
476
+ val len1 = sqrt(v1x * v1x + v1y * v1y)
477
+ val len2 = sqrt(v2x * v2x + v2y * v2y)
478
+
479
+ if (len1 < 1.0 || len2 < 1.0) return 90.0
480
+
481
+ val cosAngle = (dot / (len1 * len2)).coerceIn(-1.0, 1.0)
482
+ return Math.toDegrees(kotlin.math.acos(cosAngle))
483
+ }
484
+
485
+ val angleTL = angleAt(rectangle.topRight, rectangle.topLeft, rectangle.bottomLeft)
486
+ val angleTR = angleAt(rectangle.topLeft, rectangle.topRight, rectangle.bottomRight)
487
+ val angleBL = angleAt(rectangle.topLeft, rectangle.bottomLeft, rectangle.bottomRight)
488
+ val angleBR = angleAt(rectangle.topRight, rectangle.bottomRight, rectangle.bottomLeft)
489
+
490
+ // All angles should be within 50°-130° (allow ±40° from 90°, more lenient)
491
+ return angleTL in 50.0..130.0 && angleTR in 50.0..130.0 &&
492
+ angleBL in 50.0..130.0 && angleBR in 50.0..130.0
493
+ }
494
+
495
+ /**
496
+ * Check if edges are sufficiently straight (low deviation from fitted line)
497
+ */
498
+ private fun hasValidEdgeStraightness(rectangle: Rectangle, srcMat: Mat): Boolean {
499
+ val minDim = min(srcMat.cols(), srcMat.rows()).toDouble()
500
+ val maxDeviation = max(10.0, minDim * 0.02) // Allow 2% deviation
501
+
502
+ fun checkEdgeStraightness(p1: Point, p2: Point, p3: Point, p4: Point): Boolean {
503
+ // Check if points p1-p2 and p3-p4 form roughly parallel lines
504
+ val dx1 = p2.x - p1.x
505
+ val dy1 = p2.y - p1.y
506
+ val dx2 = p4.x - p3.x
507
+ val dy2 = p4.y - p3.y
508
+
509
+ val len1 = sqrt(dx1 * dx1 + dy1 * dy1)
510
+ val len2 = sqrt(dx2 * dx2 + dy2 * dy2)
511
+
512
+ if (len1 < 1.0 || len2 < 1.0) return true
513
+
514
+ // Normalize and compute angle difference
515
+ val dot = (dx1 * dx2 + dy1 * dy2) / (len1 * len2)
516
+ val angleDiff = Math.toDegrees(kotlin.math.acos(dot.coerceIn(-1.0, 1.0)))
517
+
518
+ // Parallel lines should have angle diff close to 0° or 180° (more lenient: 20° tolerance)
519
+ return angleDiff < 20.0 || angleDiff > 160.0
520
+ }
521
+
522
+ // Check top vs bottom edges
523
+ val topBottomOk = checkEdgeStraightness(
524
+ rectangle.topLeft, rectangle.topRight,
525
+ rectangle.bottomLeft, rectangle.bottomRight
526
+ )
527
+
528
+ // Check left vs right edges
529
+ val leftRightOk = checkEdgeStraightness(
530
+ rectangle.topLeft, rectangle.bottomLeft,
531
+ rectangle.topRight, rectangle.bottomRight
532
+ )
533
+
534
+ return topBottomOk && leftRightOk
535
+ }
536
+
442
537
  /**
443
538
  * Evaluate rectangle quality (matching iOS logic)
444
539
  */
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.4.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",