react-native-rectangle-doc-scanner 11.0.0 → 11.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.
- package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/CameraController.kt +79 -32
- package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt +1 -1
- package/android/src/common/kotlin/com/reactnativerectangledocscanner/DocumentDetector.kt +5 -2
- package/dist/DocScanner.js +18 -7
- package/package.json +1 -1
- package/src/DocScanner.tsx +23 -7
|
@@ -118,11 +118,11 @@ class CameraController(
|
|
|
118
118
|
Log.d(TAG, "[CAMERAX] TextureView visibility: ${textureView.visibility}")
|
|
119
119
|
Log.d(TAG, "[CAMERAX] TextureView isAvailable: ${textureView.isAvailable}")
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
// Force portrait orientation (app is portrait-only)
|
|
122
|
+
val targetRotation = android.view.Surface.ROTATION_0
|
|
123
|
+
Log.d(TAG, "[CAMERAX] Setting target rotation to ROTATION_0 (portrait-only app)")
|
|
123
124
|
|
|
124
125
|
preview = Preview.Builder()
|
|
125
|
-
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
|
|
126
126
|
.setTargetRotation(targetRotation) // Force portrait
|
|
127
127
|
.build()
|
|
128
128
|
.also { previewUseCase ->
|
|
@@ -185,7 +185,7 @@ class CameraController(
|
|
|
185
185
|
// ImageAnalysis UseCase for document detection
|
|
186
186
|
imageAnalyzer = ImageAnalysis.Builder()
|
|
187
187
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
188
|
-
.
|
|
188
|
+
.setTargetResolution(android.util.Size(1920, 1440)) // Higher resolution for better small-edge detection
|
|
189
189
|
.setTargetRotation(targetRotation) // Match preview rotation
|
|
190
190
|
.build()
|
|
191
191
|
.also {
|
|
@@ -201,7 +201,6 @@ class CameraController(
|
|
|
201
201
|
// ImageCapture UseCase
|
|
202
202
|
imageCapture = ImageCapture.Builder()
|
|
203
203
|
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
|
|
204
|
-
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
|
|
205
204
|
.setTargetRotation(targetRotation) // Match preview rotation
|
|
206
205
|
.build()
|
|
207
206
|
|
|
@@ -388,18 +387,30 @@ class CameraController(
|
|
|
388
387
|
mlBox: android.graphics.Rect?
|
|
389
388
|
): Rectangle? {
|
|
390
389
|
return try {
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
val
|
|
396
|
-
val
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
390
|
+
val frameWidth = if (rotation == 90 || rotation == 270) height else width
|
|
391
|
+
val frameHeight = if (rotation == 90 || rotation == 270) width else height
|
|
392
|
+
val frameArea = frameWidth.toLong() * frameHeight.toLong()
|
|
393
|
+
val roiRect = mlBox?.let { box ->
|
|
394
|
+
val boxArea = box.width().toLong() * box.height().toLong()
|
|
395
|
+
val aspect = if (box.height() > 0) box.width().toDouble() / box.height().toDouble() else 0.0
|
|
396
|
+
val isValidSize = boxArea >= (frameArea * 0.08)
|
|
397
|
+
val isValidAspect = aspect in 0.4..2.5
|
|
398
|
+
if (!isValidSize || !isValidAspect) {
|
|
399
|
+
null
|
|
400
|
+
} else {
|
|
401
|
+
val padX = (box.width() * 0.25f).toInt().coerceAtLeast(32)
|
|
402
|
+
val padY = (box.height() * 0.25f).toInt().coerceAtLeast(32)
|
|
403
|
+
android.graphics.Rect(
|
|
404
|
+
(box.left - padX).coerceAtLeast(0),
|
|
405
|
+
(box.top - padY).coerceAtLeast(0),
|
|
406
|
+
(box.right + padX).coerceAtMost(frameWidth),
|
|
407
|
+
(box.bottom + padY).coerceAtMost(frameHeight)
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (roiRect != null) {
|
|
413
|
+
DocumentDetector.detectRectangleInYUVWithRoi(nv21, width, height, rotation, roiRect)
|
|
403
414
|
?: DocumentDetector.detectRectangleInYUV(nv21, width, height, rotation)
|
|
404
415
|
} else {
|
|
405
416
|
DocumentDetector.detectRectangleInYUV(nv21, width, height, rotation)
|
|
@@ -474,14 +485,35 @@ class CameraController(
|
|
|
474
485
|
|
|
475
486
|
if (viewWidth <= 0 || viewHeight <= 0) return null
|
|
476
487
|
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
val
|
|
488
|
+
// The image coordinates are in camera sensor space. We need to transform them
|
|
489
|
+
// to match how the TextureView displays the image (after rotation/scaling).
|
|
490
|
+
val sensorOrientation = getCameraSensorOrientation()
|
|
491
|
+
val displayRotationDegrees = when (textureView.display?.rotation ?: Surface.ROTATION_0) {
|
|
492
|
+
Surface.ROTATION_0 -> 0
|
|
493
|
+
Surface.ROTATION_90 -> 90
|
|
494
|
+
Surface.ROTATION_180 -> 180
|
|
495
|
+
Surface.ROTATION_270 -> 270
|
|
496
|
+
else -> 0
|
|
497
|
+
}
|
|
480
498
|
|
|
481
|
-
|
|
499
|
+
fun rotatePoint(point: org.opencv.core.Point): org.opencv.core.Point {
|
|
500
|
+
return if (sensorOrientation == 90) {
|
|
501
|
+
org.opencv.core.Point(
|
|
502
|
+
point.y,
|
|
503
|
+
imageWidth - point.x
|
|
504
|
+
)
|
|
505
|
+
} else {
|
|
506
|
+
point
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
val finalWidth = if (sensorOrientation == 90) imageHeight else imageWidth
|
|
511
|
+
val finalHeight = if (sensorOrientation == 90) imageWidth else imageHeight
|
|
512
|
+
|
|
513
|
+
// Then apply fit-center scaling
|
|
482
514
|
val scaleX = viewWidth / finalWidth.toFloat()
|
|
483
515
|
val scaleY = viewHeight / finalHeight.toFloat()
|
|
484
|
-
val scale = scaleX.
|
|
516
|
+
val scale = scaleX.coerceAtMost(scaleY)
|
|
485
517
|
|
|
486
518
|
val scaledWidth = finalWidth * scale
|
|
487
519
|
val scaledHeight = finalHeight * scale
|
|
@@ -489,9 +521,10 @@ class CameraController(
|
|
|
489
521
|
val offsetY = (viewHeight - scaledHeight) / 2f
|
|
490
522
|
|
|
491
523
|
fun transformPoint(point: org.opencv.core.Point): org.opencv.core.Point {
|
|
524
|
+
val rotated = rotatePoint(point)
|
|
492
525
|
return org.opencv.core.Point(
|
|
493
|
-
|
|
494
|
-
|
|
526
|
+
rotated.x * scale + offsetX,
|
|
527
|
+
rotated.y * scale + offsetY
|
|
495
528
|
)
|
|
496
529
|
}
|
|
497
530
|
|
|
@@ -502,9 +535,10 @@ class CameraController(
|
|
|
502
535
|
transformPoint(rectangle.bottomRight)
|
|
503
536
|
)
|
|
504
537
|
|
|
505
|
-
Log.d(TAG, "[MAPPING] Image: ${imageWidth}x${imageHeight} →
|
|
506
|
-
Log.d(TAG, "[MAPPING] Scale: $scale, Offset: ($offsetX, $offsetY)")
|
|
538
|
+
Log.d(TAG, "[MAPPING] Sensor: ${sensorOrientation}°, Image: ${imageWidth}x${imageHeight} → Final: ${finalWidth}x${finalHeight}")
|
|
539
|
+
Log.d(TAG, "[MAPPING] View: ${viewWidth.toInt()}x${viewHeight.toInt()}, Scale: $scale, Offset: ($offsetX, $offsetY)")
|
|
507
540
|
Log.d(TAG, "[MAPPING] TL: (${rectangle.topLeft.x}, ${rectangle.topLeft.y}) → " +
|
|
541
|
+
"Rotated: (${rotatePoint(rectangle.topLeft).x}, ${rotatePoint(rectangle.topLeft).y}) → " +
|
|
508
542
|
"Final: (${result.topLeft.x}, ${result.topLeft.y})")
|
|
509
543
|
|
|
510
544
|
return result
|
|
@@ -545,13 +579,17 @@ class CameraController(
|
|
|
545
579
|
val centerX = viewWidth / 2f
|
|
546
580
|
val centerY = viewHeight / 2f
|
|
547
581
|
|
|
548
|
-
|
|
582
|
+
// Calculate rotation from buffer to display coordinates.
|
|
583
|
+
// CameraX accounts for sensor orientation via targetRotation. Some tablets with landscape
|
|
584
|
+
// sensors report Display 90 in portrait but render upside down; add a 180° fix for that case.
|
|
585
|
+
val tabletUpsideDownFix = if (sensorOrientation == 0 && displayRotationDegrees == 90) 180 else 0
|
|
586
|
+
val rotationDegrees = ((displayRotationDegrees + tabletUpsideDownFix) % 360).toFloat()
|
|
549
587
|
|
|
550
588
|
if (rotationDegrees != 0f) {
|
|
551
|
-
Log.d(TAG, "[TRANSFORM] Applying rotation: ${rotationDegrees}°")
|
|
552
589
|
matrix.postRotate(rotationDegrees, centerX, centerY)
|
|
553
590
|
}
|
|
554
591
|
|
|
592
|
+
// After rotation, determine effective buffer size
|
|
555
593
|
val rotatedBufferWidth = if (rotationDegrees == 90f || rotationDegrees == 270f) {
|
|
556
594
|
bufferHeight
|
|
557
595
|
} else {
|
|
@@ -563,17 +601,26 @@ class CameraController(
|
|
|
563
601
|
bufferHeight
|
|
564
602
|
}
|
|
565
603
|
|
|
566
|
-
// Scale to
|
|
604
|
+
// Scale to fit within the view while maintaining aspect ratio (no zoom/crop)
|
|
567
605
|
val scaleX = viewWidth.toFloat() / rotatedBufferWidth.toFloat()
|
|
568
606
|
val scaleY = viewHeight.toFloat() / rotatedBufferHeight.toFloat()
|
|
569
|
-
val scale = scaleX.
|
|
607
|
+
val scale = scaleX.coerceAtMost(scaleY) // Use min to fit
|
|
570
608
|
|
|
571
609
|
Log.d(TAG, "[TRANSFORM] Rotated buffer: ${rotatedBufferWidth}x${rotatedBufferHeight}, ScaleX: $scaleX, ScaleY: $scaleY, Using: $scale")
|
|
572
610
|
|
|
573
611
|
matrix.postScale(scale, scale, centerX, centerY)
|
|
574
612
|
|
|
575
|
-
//
|
|
576
|
-
|
|
613
|
+
// Track the actual preview viewport within the view for clipping overlays.
|
|
614
|
+
val scaledWidth = rotatedBufferWidth * scale
|
|
615
|
+
val scaledHeight = rotatedBufferHeight * scale
|
|
616
|
+
val offsetX = (viewWidth - scaledWidth) / 2f
|
|
617
|
+
val offsetY = (viewHeight - scaledHeight) / 2f
|
|
618
|
+
previewViewport = android.graphics.RectF(
|
|
619
|
+
offsetX,
|
|
620
|
+
offsetY,
|
|
621
|
+
offsetX + scaledWidth,
|
|
622
|
+
offsetY + scaledHeight
|
|
623
|
+
)
|
|
577
624
|
|
|
578
625
|
textureView.setTransform(matrix)
|
|
579
626
|
Log.d(TAG, "[TRANSFORM] Transform applied successfully")
|
package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt
CHANGED
|
@@ -746,7 +746,7 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
746
746
|
if (viewWidth == 0 || viewHeight == 0 || imageWidth == 0 || imageHeight == 0) {
|
|
747
747
|
return rectangle
|
|
748
748
|
}
|
|
749
|
-
val scale =
|
|
749
|
+
val scale = min(
|
|
750
750
|
viewWidth.toDouble() / imageWidth.toDouble(),
|
|
751
751
|
viewHeight.toDouble() / imageHeight.toDouble()
|
|
752
752
|
)
|
|
@@ -589,7 +589,7 @@ class DocumentDetector {
|
|
|
589
589
|
return rectangle
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
-
val scale =
|
|
592
|
+
val scale = min(
|
|
593
593
|
viewWidth.toDouble() / imageWidth.toDouble(),
|
|
594
594
|
viewHeight.toDouble() / imageHeight.toDouble()
|
|
595
595
|
)
|
|
@@ -602,7 +602,10 @@ class DocumentDetector {
|
|
|
602
602
|
fun mapPoint(point: Point): Point {
|
|
603
603
|
val x = (point.x * scale) + offsetX
|
|
604
604
|
val y = (point.y * scale) + offsetY
|
|
605
|
-
return Point(
|
|
605
|
+
return Point(
|
|
606
|
+
x.coerceIn(0.0, viewWidth.toDouble()),
|
|
607
|
+
y.coerceIn(0.0, viewHeight.toDouble())
|
|
608
|
+
)
|
|
606
609
|
}
|
|
607
610
|
|
|
608
611
|
return Rectangle(
|
package/dist/DocScanner.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
128
|
-
|
|
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:
|
|
131
|
-
|
|
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),
|
|
@@ -418,7 +428,7 @@ const VisionCameraScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor =
|
|
|
418
428
|
const overlayIsActive = autoCapture ? isAutoCapturing : (detectedRectangle?.stableCounter ?? 0) > 0;
|
|
419
429
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container, onLayout: handleLayout },
|
|
420
430
|
CameraComponent && device && hasPermission ? (react_1.default.createElement(CameraComponent, { ref: cameraRef, style: styles.scanner, device: device, isActive: true, photo: true, torch: enableTorch ? 'on' : 'off', frameProcessor: frameProcessor, frameProcessorFps: 10 })) : (react_1.default.createElement(react_native_1.View, { style: styles.scanner })),
|
|
421
|
-
showGrid && overlayPolygon && (react_1.default.createElement(overlay_1.ScannerOverlay, { active: overlayIsActive, color: gridColor ?? overlayColor, lineWidth: gridLineWidth, polygon: overlayPolygon, clipRect: detectedRectangle?.previewViewport ?? null })),
|
|
431
|
+
showGrid && overlayPolygon && (react_1.default.createElement(overlay_1.ScannerOverlay, { active: overlayIsActive, color: gridColor ?? overlayColor, lineWidth: gridLineWidth, polygon: overlayPolygon, clipRect: react_native_1.Platform.OS === 'android' ? null : (detectedRectangle?.previewViewport ?? null) })),
|
|
422
432
|
showManualCaptureButton && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: () => captureVision('manual') })),
|
|
423
433
|
children));
|
|
424
434
|
});
|
|
@@ -630,6 +640,7 @@ const NativeScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAU
|
|
|
630
640
|
let rectangleOnScreen = normalizeRectangle(event.rectangleOnScreen ?? null);
|
|
631
641
|
const density = react_native_1.PixelRatio.get();
|
|
632
642
|
if (react_native_1.Platform.OS === 'android' &&
|
|
643
|
+
!rectangleOnScreen &&
|
|
633
644
|
rectangleCoordinates &&
|
|
634
645
|
event.imageSize &&
|
|
635
646
|
event.previewSize &&
|
|
@@ -705,7 +716,7 @@ const NativeScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAU
|
|
|
705
716
|
const detectionThreshold = autoCapture ? minStableFrames : 99999;
|
|
706
717
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
707
718
|
react_1.default.createElement(react_native_document_scanner_1.default, { ref: scannerRef, style: styles.scanner, detectionCountBeforeCapture: detectionThreshold, overlayColor: overlayColor, enableTorch: enableTorch, quality: normalizedQuality, useBase64: useBase64, manualOnly: false, detectionConfig: detectionConfig, onPictureTaken: handlePictureTaken, onError: handleError, onRectangleDetect: handleRectangleDetect }),
|
|
708
|
-
showGrid && overlayPolygon && (react_1.default.createElement(overlay_1.ScannerOverlay, { active: overlayIsActive, color: gridColor ?? overlayColor, lineWidth: gridLineWidth, polygon: overlayPolygon, clipRect: detectedRectangle?.previewViewport ?? null })),
|
|
719
|
+
showGrid && overlayPolygon && (react_1.default.createElement(overlay_1.ScannerOverlay, { active: overlayIsActive, color: gridColor ?? overlayColor, lineWidth: gridLineWidth, polygon: overlayPolygon, clipRect: react_native_1.Platform.OS === 'android' ? null : (detectedRectangle?.previewViewport ?? null) })),
|
|
709
720
|
showManualCaptureButton && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: handleManualCapture })),
|
|
710
721
|
children));
|
|
711
722
|
});
|
package/package.json
CHANGED
package/src/DocScanner.tsx
CHANGED
|
@@ -188,15 +188,30 @@ const mapRectangleToView = (
|
|
|
188
188
|
): Rectangle => {
|
|
189
189
|
const viewWidthPx = viewWidth * density;
|
|
190
190
|
const viewHeightPx = viewHeight * density;
|
|
191
|
-
const scale =
|
|
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 =
|
|
195
|
-
|
|
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:
|
|
199
|
-
|
|
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 {
|
|
@@ -601,7 +616,7 @@ const VisionCameraScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
601
616
|
color={gridColor ?? overlayColor}
|
|
602
617
|
lineWidth={gridLineWidth}
|
|
603
618
|
polygon={overlayPolygon}
|
|
604
|
-
clipRect={detectedRectangle?.previewViewport ?? null}
|
|
619
|
+
clipRect={Platform.OS === 'android' ? null : (detectedRectangle?.previewViewport ?? null)}
|
|
605
620
|
/>
|
|
606
621
|
)}
|
|
607
622
|
{showManualCaptureButton && (
|
|
@@ -881,6 +896,7 @@ const NativeScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
881
896
|
|
|
882
897
|
if (
|
|
883
898
|
Platform.OS === 'android' &&
|
|
899
|
+
!rectangleOnScreen &&
|
|
884
900
|
rectangleCoordinates &&
|
|
885
901
|
event.imageSize &&
|
|
886
902
|
event.previewSize &&
|
|
@@ -1002,7 +1018,7 @@ const NativeScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
1002
1018
|
color={gridColor ?? overlayColor}
|
|
1003
1019
|
lineWidth={gridLineWidth}
|
|
1004
1020
|
polygon={overlayPolygon}
|
|
1005
|
-
clipRect={detectedRectangle?.previewViewport ?? null}
|
|
1021
|
+
clipRect={Platform.OS === 'android' ? null : (detectedRectangle?.previewViewport ?? null)}
|
|
1006
1022
|
/>
|
|
1007
1023
|
)}
|
|
1008
1024
|
{showManualCaptureButton && (
|