react-native-rectangle-doc-scanner 10.22.0 → 10.24.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.
@@ -355,7 +355,22 @@ class CameraController(
355
355
  mlBox: android.graphics.Rect?
356
356
  ): Rectangle? {
357
357
  return try {
358
- DocumentDetector.detectRectangleInYUV(nv21, width, height, rotation)
358
+ if (mlBox != null) {
359
+ val frameWidth = if (rotation == 90 || rotation == 270) height else width
360
+ val frameHeight = if (rotation == 90 || rotation == 270) width else height
361
+ val padX = (mlBox.width() * 0.15f).toInt().coerceAtLeast(24)
362
+ val padY = (mlBox.height() * 0.15f).toInt().coerceAtLeast(24)
363
+ val roi = android.graphics.Rect(
364
+ (mlBox.left - padX).coerceAtLeast(0),
365
+ (mlBox.top - padY).coerceAtLeast(0),
366
+ (mlBox.right + padX).coerceAtMost(frameWidth),
367
+ (mlBox.bottom + padY).coerceAtMost(frameHeight)
368
+ )
369
+ DocumentDetector.detectRectangleInYUVWithRoi(nv21, width, height, rotation, roi)
370
+ ?: DocumentDetector.detectRectangleInYUV(nv21, width, height, rotation)
371
+ } else {
372
+ DocumentDetector.detectRectangleInYUV(nv21, width, height, rotation)
373
+ }
359
374
  } catch (e: Exception) {
360
375
  Log.w(TAG, "[CAMERAX] OpenCV detection failed", e)
361
376
  null
@@ -421,20 +436,24 @@ class CameraController(
421
436
  fun mapRectangleToView(rectangle: Rectangle?, imageWidth: Int, imageHeight: Int): Rectangle? {
422
437
  if (rectangle == null || imageWidth <= 0 || imageHeight <= 0) return null
423
438
 
424
- // Simple proportional scaling for TextureView
439
+ // Fit-center scaling to avoid zoom/crop and keep mapping aligned with preview.
425
440
  val viewWidth = textureView.width.toFloat()
426
441
  val viewHeight = textureView.height.toFloat()
427
442
 
428
443
  if (viewWidth <= 0 || viewHeight <= 0) return null
429
444
 
430
- // Simple proportional scaling
431
445
  val scaleX = viewWidth / imageWidth.toFloat()
432
446
  val scaleY = viewHeight / imageHeight.toFloat()
447
+ val scale = scaleX.coerceAtMost(scaleY)
448
+ val scaledWidth = imageWidth * scale
449
+ val scaledHeight = imageHeight * scale
450
+ val offsetX = (viewWidth - scaledWidth) / 2f
451
+ val offsetY = (viewHeight - scaledHeight) / 2f
433
452
 
434
453
  fun scalePoint(point: org.opencv.core.Point): org.opencv.core.Point {
435
454
  return org.opencv.core.Point(
436
- point.x * scaleX,
437
- point.y * scaleY
455
+ point.x * scale + offsetX,
456
+ point.y * scale + offsetY
438
457
  )
439
458
  }
440
459
 
@@ -509,10 +528,10 @@ class CameraController(
509
528
  bufferHeight
510
529
  }
511
530
 
512
- // Scale to fill the view while maintaining aspect ratio
531
+ // Scale to fit within the view while maintaining aspect ratio (no zoom/crop)
513
532
  val scaleX = viewWidth.toFloat() / rotatedBufferWidth.toFloat()
514
533
  val scaleY = viewHeight.toFloat() / rotatedBufferHeight.toFloat()
515
- val scale = scaleX.coerceAtLeast(scaleY) // Use max to fill
534
+ val scale = scaleX.coerceAtMost(scaleY) // Use min to fit
516
535
 
517
536
  Log.d(TAG, "[TRANSFORM] Rotated buffer: ${rotatedBufferWidth}x${rotatedBufferHeight}, ScaleX: $scaleX, ScaleY: $scaleY, Using: $scale")
518
537
 
@@ -25,6 +25,7 @@ import kotlinx.coroutines.delay
25
25
  import java.io.File
26
26
  import kotlin.math.min
27
27
  import kotlin.math.max
28
+ import kotlin.math.hypot
28
29
 
29
30
  class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), LifecycleOwner {
30
31
  private val themedContext = context
@@ -58,6 +59,7 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
58
59
  private var lastDetectedImageWidth = 0
59
60
  private var lastDetectedImageHeight = 0
60
61
  private var lastRectangleOnScreen: Rectangle? = null
62
+ private var lastSmoothedRectangleOnScreen: Rectangle? = null
61
63
 
62
64
  // Coroutine scope for async operations
63
65
  private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
@@ -215,19 +217,82 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
215
217
  } else {
216
218
  null
217
219
  }
218
- lastRectangleOnScreen = rectangleOnScreen
220
+ val smoothedRectangleOnScreen = smoothRectangle(rectangleOnScreen, width, height)
221
+ lastRectangleOnScreen = smoothedRectangleOnScreen
219
222
  val quality = when {
220
- rectangleOnScreen != null && width > 0 && height > 0 ->
221
- DocumentDetector.evaluateRectangleQualityInView(rectangleOnScreen, width, height)
223
+ smoothedRectangleOnScreen != null && width > 0 && height > 0 ->
224
+ DocumentDetector.evaluateRectangleQualityInView(smoothedRectangleOnScreen, width, height)
222
225
  rectangle != null -> DocumentDetector.evaluateRectangleQuality(rectangle, imageWidth, imageHeight)
223
226
  else -> RectangleQuality.TOO_FAR
224
227
  }
225
228
 
226
229
  post {
227
- onRectangleDetected(rectangleOnScreen, rectangle, quality, imageWidth, imageHeight)
230
+ onRectangleDetected(smoothedRectangleOnScreen, rectangle, quality, imageWidth, imageHeight)
228
231
  }
229
232
  }
230
233
 
234
+ private fun smoothRectangle(rectangle: Rectangle?, viewWidth: Int, viewHeight: Int): Rectangle? {
235
+ if (rectangle == null || viewWidth <= 0 || viewHeight <= 0) {
236
+ lastSmoothedRectangleOnScreen = null
237
+ return null
238
+ }
239
+
240
+ val prev = lastSmoothedRectangleOnScreen
241
+ if (prev == null) {
242
+ lastSmoothedRectangleOnScreen = rectangle
243
+ return rectangle
244
+ }
245
+
246
+ val minDim = min(viewWidth, viewHeight).toDouble()
247
+ val snapThreshold = max(12.0, minDim * 0.015)
248
+ val smoothThreshold = max(24.0, minDim * 0.06)
249
+ val maxCornerDistance = max(
250
+ maxCornerDistance(prev, rectangle),
251
+ maxCornerDistance(rectangle, prev)
252
+ )
253
+
254
+ val result = when {
255
+ maxCornerDistance <= snapThreshold -> prev
256
+ maxCornerDistance <= smoothThreshold -> blendRectangle(prev, rectangle, 0.35)
257
+ else -> rectangle
258
+ }
259
+
260
+ lastSmoothedRectangleOnScreen = result
261
+ return result
262
+ }
263
+
264
+ private fun maxCornerDistance(a: Rectangle, b: Rectangle): Double {
265
+ return max(
266
+ max(
267
+ distance(a.topLeft, b.topLeft),
268
+ distance(a.topRight, b.topRight)
269
+ ),
270
+ max(
271
+ distance(a.bottomLeft, b.bottomLeft),
272
+ distance(a.bottomRight, b.bottomRight)
273
+ )
274
+ )
275
+ }
276
+
277
+ private fun distance(p1: Point, p2: Point): Double {
278
+ return hypot(p1.x - p2.x, p1.y - p2.y)
279
+ }
280
+
281
+ private fun blendRectangle(a: Rectangle, b: Rectangle, t: Double): Rectangle {
282
+ fun lerp(p1: Point, p2: Point): Point {
283
+ val x = p1.x + (p2.x - p1.x) * t
284
+ val y = p1.y + (p2.y - p1.y) * t
285
+ return Point(x, y)
286
+ }
287
+
288
+ return Rectangle(
289
+ lerp(a.topLeft, b.topLeft),
290
+ lerp(a.topRight, b.topRight),
291
+ lerp(a.bottomLeft, b.bottomLeft),
292
+ lerp(a.bottomRight, b.bottomRight)
293
+ )
294
+ }
295
+
231
296
  private fun onRectangleDetected(
232
297
  rectangleOnScreen: Rectangle?,
233
298
  rectangleCoordinates: Rectangle?,
@@ -684,18 +749,18 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
684
749
  if (viewWidth == 0 || viewHeight == 0 || imageWidth == 0 || imageHeight == 0) {
685
750
  return rectangle
686
751
  }
687
- val scale = max(
752
+ val scale = min(
688
753
  viewWidth.toDouble() / imageWidth.toDouble(),
689
754
  viewHeight.toDouble() / imageHeight.toDouble()
690
755
  )
691
756
  val scaledImageWidth = imageWidth.toDouble() * scale
692
757
  val scaledImageHeight = imageHeight.toDouble() * scale
693
- val offsetX = (scaledImageWidth - viewWidth) / 2.0
694
- val offsetY = (scaledImageHeight - viewHeight) / 2.0
758
+ val offsetX = (viewWidth - scaledImageWidth) / 2.0
759
+ val offsetY = (viewHeight - scaledImageHeight) / 2.0
695
760
 
696
761
  fun mapPoint(point: Point): Point {
697
- val x = (point.x + offsetX) / scale
698
- val y = (point.y + offsetY) / scale
762
+ val x = (point.x - offsetX) / scale
763
+ val y = (point.y - offsetY) / scale
699
764
  return Point(
700
765
  x.coerceIn(0.0, imageWidth.toDouble()),
701
766
  y.coerceIn(0.0, imageHeight.toDouble())
@@ -421,19 +421,19 @@ class DocumentDetector {
421
421
  return rectangle
422
422
  }
423
423
 
424
- val scale = max(
424
+ val scale = min(
425
425
  viewWidth.toDouble() / imageWidth.toDouble(),
426
426
  viewHeight.toDouble() / imageHeight.toDouble()
427
427
  )
428
428
 
429
429
  val scaledImageWidth = imageWidth * scale
430
430
  val scaledImageHeight = imageHeight * scale
431
- val offsetX = (scaledImageWidth - viewWidth) / 2.0
432
- val offsetY = (scaledImageHeight - viewHeight) / 2.0
431
+ val offsetX = (viewWidth - scaledImageWidth) / 2.0
432
+ val offsetY = (viewHeight - scaledImageHeight) / 2.0
433
433
 
434
434
  fun mapPoint(point: Point): Point {
435
- val x = (point.x * scale) - offsetX
436
- val y = (point.y * scale) - offsetY
435
+ val x = (point.x * scale) + offsetX
436
+ val y = (point.y * scale) + offsetY
437
437
  return Point(
438
438
  x.coerceIn(0.0, viewWidth.toDouble()),
439
439
  y.coerceIn(0.0, viewHeight.toDouble())
@@ -121,14 +121,24 @@ const mirrorRectangleHorizontally = (rectangle, imageWidth) => ({
121
121
  const mapRectangleToView = (rectangle, imageWidth, imageHeight, viewWidth, viewHeight, density) => {
122
122
  const viewWidthPx = viewWidth * density;
123
123
  const viewHeightPx = viewHeight * density;
124
- const scale = Math.max(viewWidthPx / imageWidth, viewHeightPx / imageHeight);
124
+ const scale = react_native_1.Platform.OS === 'ios'
125
+ ? Math.max(viewWidthPx / imageWidth, viewHeightPx / imageHeight)
126
+ : Math.min(viewWidthPx / imageWidth, viewHeightPx / imageHeight);
125
127
  const scaledImageWidth = imageWidth * scale;
126
128
  const scaledImageHeight = imageHeight * scale;
127
- const offsetX = (scaledImageWidth - viewWidthPx) / 2;
128
- const offsetY = (scaledImageHeight - viewHeightPx) / 2;
129
+ const offsetX = react_native_1.Platform.OS === 'ios'
130
+ ? (scaledImageWidth - viewWidthPx) / 2
131
+ : (viewWidthPx - scaledImageWidth) / 2;
132
+ const offsetY = react_native_1.Platform.OS === 'ios'
133
+ ? (scaledImageHeight - viewHeightPx) / 2
134
+ : (viewHeightPx - scaledImageHeight) / 2;
129
135
  const mapPoint = (point) => ({
130
- x: (point.x * scale - offsetX) / density,
131
- y: (point.y * scale - offsetY) / density,
136
+ x: react_native_1.Platform.OS === 'ios'
137
+ ? (point.x * scale - offsetX) / density
138
+ : (point.x * scale + offsetX) / density,
139
+ y: react_native_1.Platform.OS === 'ios'
140
+ ? (point.y * scale - offsetY) / density
141
+ : (point.y * scale + offsetY) / density,
132
142
  });
133
143
  return {
134
144
  topLeft: mapPoint(rectangle.topLeft),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "10.22.0",
3
+ "version": "10.24.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -188,15 +188,30 @@ const mapRectangleToView = (
188
188
  ): Rectangle => {
189
189
  const viewWidthPx = viewWidth * density;
190
190
  const viewHeightPx = viewHeight * density;
191
- const scale = Math.max(viewWidthPx / imageWidth, viewHeightPx / imageHeight);
191
+ const scale =
192
+ Platform.OS === 'ios'
193
+ ? Math.max(viewWidthPx / imageWidth, viewHeightPx / imageHeight)
194
+ : Math.min(viewWidthPx / imageWidth, viewHeightPx / imageHeight);
192
195
  const scaledImageWidth = imageWidth * scale;
193
196
  const scaledImageHeight = imageHeight * scale;
194
- const offsetX = (scaledImageWidth - viewWidthPx) / 2;
195
- const offsetY = (scaledImageHeight - viewHeightPx) / 2;
197
+ const offsetX =
198
+ Platform.OS === 'ios'
199
+ ? (scaledImageWidth - viewWidthPx) / 2
200
+ : (viewWidthPx - scaledImageWidth) / 2;
201
+ const offsetY =
202
+ Platform.OS === 'ios'
203
+ ? (scaledImageHeight - viewHeightPx) / 2
204
+ : (viewHeightPx - scaledImageHeight) / 2;
196
205
 
197
206
  const mapPoint = (point: Point): Point => ({
198
- x: (point.x * scale - offsetX) / density,
199
- y: (point.y * scale - offsetY) / density,
207
+ x:
208
+ Platform.OS === 'ios'
209
+ ? (point.x * scale - offsetX) / density
210
+ : (point.x * scale + offsetX) / density,
211
+ y:
212
+ Platform.OS === 'ios'
213
+ ? (point.y * scale - offsetY) / density
214
+ : (point.y * scale + offsetY) / density,
200
215
  });
201
216
 
202
217
  return {