react-native-rectangle-doc-scanner 10.27.0 → 10.28.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.
|
@@ -33,6 +33,7 @@ class CameraController(
|
|
|
33
33
|
private var preview: Preview? = null
|
|
34
34
|
private var imageAnalyzer: ImageAnalysis? = null
|
|
35
35
|
private var imageCapture: ImageCapture? = null
|
|
36
|
+
private var previewViewport: android.graphics.RectF? = null
|
|
36
37
|
|
|
37
38
|
private val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
38
39
|
|
|
@@ -358,8 +359,8 @@ class CameraController(
|
|
|
358
359
|
if (mlBox != null) {
|
|
359
360
|
val frameWidth = if (rotation == 90 || rotation == 270) height else width
|
|
360
361
|
val frameHeight = if (rotation == 90 || rotation == 270) width else height
|
|
361
|
-
val padX = (mlBox.width() * 0.
|
|
362
|
-
val padY = (mlBox.height() * 0.
|
|
362
|
+
val padX = (mlBox.width() * 0.25f).toInt().coerceAtLeast(32)
|
|
363
|
+
val padY = (mlBox.height() * 0.25f).toInt().coerceAtLeast(32)
|
|
363
364
|
val roi = android.graphics.Rect(
|
|
364
365
|
(mlBox.left - padX).coerceAtLeast(0),
|
|
365
366
|
(mlBox.top - padY).coerceAtLeast(0),
|
|
@@ -466,13 +467,10 @@ class CameraController(
|
|
|
466
467
|
}
|
|
467
468
|
|
|
468
469
|
fun getPreviewViewport(): android.graphics.RectF? {
|
|
469
|
-
|
|
470
|
-
val
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (width <= 0 || height <= 0) return null
|
|
474
|
-
|
|
475
|
-
return android.graphics.RectF(0f, 0f, width, height)
|
|
470
|
+
val viewWidth = textureView.width.toFloat()
|
|
471
|
+
val viewHeight = textureView.height.toFloat()
|
|
472
|
+
if (viewWidth <= 0 || viewHeight <= 0) return null
|
|
473
|
+
return previewViewport ?: android.graphics.RectF(0f, 0f, viewWidth, viewHeight)
|
|
476
474
|
}
|
|
477
475
|
|
|
478
476
|
private fun updateTextureViewTransform(bufferWidth: Int, bufferHeight: Int) {
|
|
@@ -537,6 +535,18 @@ class CameraController(
|
|
|
537
535
|
|
|
538
536
|
matrix.postScale(scale, scale, centerX, centerY)
|
|
539
537
|
|
|
538
|
+
// Track the actual preview viewport within the view for clipping overlays.
|
|
539
|
+
val scaledWidth = rotatedBufferWidth * scale
|
|
540
|
+
val scaledHeight = rotatedBufferHeight * scale
|
|
541
|
+
val offsetX = (viewWidth - scaledWidth) / 2f
|
|
542
|
+
val offsetY = (viewHeight - scaledHeight) / 2f
|
|
543
|
+
previewViewport = android.graphics.RectF(
|
|
544
|
+
offsetX,
|
|
545
|
+
offsetY,
|
|
546
|
+
offsetX + scaledWidth,
|
|
547
|
+
offsetY + scaledHeight
|
|
548
|
+
)
|
|
549
|
+
|
|
540
550
|
textureView.setTransform(matrix)
|
|
541
551
|
Log.d(TAG, "[TRANSFORM] Transform applied successfully")
|
|
542
552
|
}
|
|
@@ -207,8 +207,8 @@ class DocumentDetector {
|
|
|
207
207
|
|
|
208
208
|
val median = computeMedian(blurredMat)
|
|
209
209
|
val sigma = 0.33
|
|
210
|
-
val cannyLow = max(
|
|
211
|
-
val cannyHigh = max(
|
|
210
|
+
val cannyLow = max(25.0, (1.0 - sigma) * median)
|
|
211
|
+
val cannyHigh = max(80.0, (1.0 + sigma) * median)
|
|
212
212
|
|
|
213
213
|
// Apply Canny edge detection with adaptive thresholds for better corner detection.
|
|
214
214
|
Imgproc.Canny(blurredMat, cannyMat, cannyLow, cannyHigh)
|
|
@@ -256,7 +256,7 @@ class DocumentDetector {
|
|
|
256
256
|
|
|
257
257
|
var largestRectangle: Rectangle? = null
|
|
258
258
|
var bestScore = 0.0
|
|
259
|
-
val minArea = max(
|
|
259
|
+
val minArea = max(450.0, (srcMat.rows() * srcMat.cols()) * 0.0007)
|
|
260
260
|
|
|
261
261
|
for (contour in contours) {
|
|
262
262
|
val contourArea = Imgproc.contourArea(contour)
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -129,6 +129,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
129
129
|
const [isGalleryOpen, setIsGalleryOpen] = (0, react_1.useState)(false);
|
|
130
130
|
const [rectangleDetected, setRectangleDetected] = (0, react_1.useState)(false);
|
|
131
131
|
const [rectangleHint, setRectangleHint] = (0, react_1.useState)(false);
|
|
132
|
+
const [captureReady, setCaptureReady] = (0, react_1.useState)(false);
|
|
132
133
|
const [flashEnabled, setFlashEnabled] = (0, react_1.useState)(false);
|
|
133
134
|
const [rotationDegrees, setRotationDegrees] = (0, react_1.useState)(0);
|
|
134
135
|
const [capturedPhotos, setCapturedPhotos] = (0, react_1.useState)([]);
|
|
@@ -140,6 +141,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
140
141
|
const captureInProgressRef = (0, react_1.useRef)(false);
|
|
141
142
|
const rectangleCaptureTimeoutRef = (0, react_1.useRef)(null);
|
|
142
143
|
const rectangleHintTimeoutRef = (0, react_1.useRef)(null);
|
|
144
|
+
const captureReadyTimeoutRef = (0, react_1.useRef)(null);
|
|
143
145
|
const isBusinessMode = type === 'business';
|
|
144
146
|
const resetScannerView = (0, react_1.useCallback)((options) => {
|
|
145
147
|
setProcessing(false);
|
|
@@ -147,6 +149,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
147
149
|
setRotationDegrees(0);
|
|
148
150
|
setRectangleDetected(false);
|
|
149
151
|
setRectangleHint(false);
|
|
152
|
+
setCaptureReady(false);
|
|
150
153
|
captureModeRef.current = null;
|
|
151
154
|
captureInProgressRef.current = false;
|
|
152
155
|
if (rectangleCaptureTimeoutRef.current) {
|
|
@@ -157,6 +160,10 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
157
160
|
clearTimeout(rectangleHintTimeoutRef.current);
|
|
158
161
|
rectangleHintTimeoutRef.current = null;
|
|
159
162
|
}
|
|
163
|
+
if (captureReadyTimeoutRef.current) {
|
|
164
|
+
clearTimeout(captureReadyTimeoutRef.current);
|
|
165
|
+
captureReadyTimeoutRef.current = null;
|
|
166
|
+
}
|
|
160
167
|
if (docScannerRef.current?.reset) {
|
|
161
168
|
docScannerRef.current.reset();
|
|
162
169
|
}
|
|
@@ -360,9 +367,14 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
360
367
|
hasRef: hasScanner,
|
|
361
368
|
rectangleDetected,
|
|
362
369
|
rectangleHint,
|
|
370
|
+
captureReady,
|
|
363
371
|
currentCaptureMode: captureModeRef.current,
|
|
364
372
|
captureInProgress: captureInProgressRef.current,
|
|
365
373
|
});
|
|
374
|
+
if (react_native_1.Platform.OS === 'android' && !captureReady) {
|
|
375
|
+
console.log('[FullDocScanner] Capture not ready yet, skipping');
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
366
378
|
if (processing) {
|
|
367
379
|
console.log('[FullDocScanner] Already processing, skipping manual capture');
|
|
368
380
|
return;
|
|
@@ -408,7 +420,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
408
420
|
emitError(error, 'Failed to capture image. Please try again.');
|
|
409
421
|
}
|
|
410
422
|
});
|
|
411
|
-
}, [processing, rectangleDetected, rectangleHint, emitError]);
|
|
423
|
+
}, [processing, rectangleDetected, rectangleHint, captureReady, emitError]);
|
|
412
424
|
const handleGalleryPick = (0, react_1.useCallback)(async () => {
|
|
413
425
|
console.log('[FullDocScanner] handleGalleryPick called');
|
|
414
426
|
if (processing || isGalleryOpen) {
|
|
@@ -527,7 +539,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
527
539
|
const stableCounter = event.stableCounter ?? 0;
|
|
528
540
|
const rectangleCoordinates = event.rectangleOnScreen ?? event.rectangleCoordinates;
|
|
529
541
|
const hasRectangle = Boolean(rectangleCoordinates);
|
|
530
|
-
const
|
|
542
|
+
const isStableForCapture = hasRectangle && (react_native_1.Platform.OS === 'android' || (event.lastDetectionType === 0 && stableCounter >= 1));
|
|
531
543
|
const scheduleClear = (ref, clearFn) => {
|
|
532
544
|
if (ref.current) {
|
|
533
545
|
clearTimeout(ref.current);
|
|
@@ -540,6 +552,14 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
540
552
|
if (hasRectangle) {
|
|
541
553
|
scheduleClear(rectangleHintTimeoutRef, () => setRectangleHint(false));
|
|
542
554
|
setRectangleHint(true);
|
|
555
|
+
if (react_native_1.Platform.OS === 'android') {
|
|
556
|
+
if (!captureReadyTimeoutRef.current) {
|
|
557
|
+
captureReadyTimeoutRef.current = setTimeout(() => {
|
|
558
|
+
setCaptureReady(true);
|
|
559
|
+
captureReadyTimeoutRef.current = null;
|
|
560
|
+
}, 1000);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
543
563
|
}
|
|
544
564
|
else {
|
|
545
565
|
if (rectangleHintTimeoutRef.current) {
|
|
@@ -547,8 +567,15 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
547
567
|
rectangleHintTimeoutRef.current = null;
|
|
548
568
|
}
|
|
549
569
|
setRectangleHint(false);
|
|
570
|
+
if (react_native_1.Platform.OS === 'android') {
|
|
571
|
+
if (captureReadyTimeoutRef.current) {
|
|
572
|
+
clearTimeout(captureReadyTimeoutRef.current);
|
|
573
|
+
captureReadyTimeoutRef.current = null;
|
|
574
|
+
}
|
|
575
|
+
setCaptureReady(false);
|
|
576
|
+
}
|
|
550
577
|
}
|
|
551
|
-
if (
|
|
578
|
+
if (isStableForCapture) {
|
|
552
579
|
scheduleClear(rectangleCaptureTimeoutRef, () => {
|
|
553
580
|
setRectangleDetected(false);
|
|
554
581
|
});
|
|
@@ -570,6 +597,9 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
570
597
|
if (rectangleHintTimeoutRef.current) {
|
|
571
598
|
clearTimeout(rectangleHintTimeoutRef.current);
|
|
572
599
|
}
|
|
600
|
+
if (captureReadyTimeoutRef.current) {
|
|
601
|
+
clearTimeout(captureReadyTimeoutRef.current);
|
|
602
|
+
}
|
|
573
603
|
}, []);
|
|
574
604
|
const activePreviewImage = croppedImageData ? getActivePreviewImage(croppedImageData) : null;
|
|
575
605
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
@@ -642,10 +672,14 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
642
672
|
enableGallery && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.iconButton, processing && styles.buttonDisabled], onPress: handleGalleryPick, disabled: processing, accessibilityLabel: mergedStrings.galleryButton, accessibilityRole: "button" },
|
|
643
673
|
react_1.default.createElement(react_native_1.View, { style: styles.iconContainer },
|
|
644
674
|
react_1.default.createElement(react_native_1.Text, { style: styles.iconText }, "\uD83D\uDDBC\uFE0F")))),
|
|
645
|
-
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
|
|
675
|
+
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
|
|
676
|
+
styles.shutterButton,
|
|
677
|
+
processing && styles.buttonDisabled,
|
|
678
|
+
react_native_1.Platform.OS === 'android' && !captureReady && styles.buttonDisabled,
|
|
679
|
+
], onPress: triggerManualCapture, disabled: processing || (react_native_1.Platform.OS === 'android' && !captureReady), accessibilityLabel: mergedStrings.manualHint, accessibilityRole: "button" },
|
|
646
680
|
react_1.default.createElement(react_native_1.View, { style: [
|
|
647
681
|
styles.shutterInner,
|
|
648
|
-
rectangleHint && { backgroundColor: overlayColor }
|
|
682
|
+
(react_native_1.Platform.OS === 'android' ? captureReady : rectangleHint) && { backgroundColor: overlayColor }
|
|
649
683
|
] })),
|
|
650
684
|
react_1.default.createElement(react_native_1.View, { style: styles.rightButtonsPlaceholder }))))),
|
|
651
685
|
processing && (react_1.default.createElement(react_native_1.View, { style: styles.processingOverlay },
|
package/package.json
CHANGED
package/src/FullDocScanner.tsx
CHANGED
|
@@ -192,6 +192,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
192
192
|
const [isGalleryOpen, setIsGalleryOpen] = useState(false);
|
|
193
193
|
const [rectangleDetected, setRectangleDetected] = useState(false);
|
|
194
194
|
const [rectangleHint, setRectangleHint] = useState(false);
|
|
195
|
+
const [captureReady, setCaptureReady] = useState(false);
|
|
195
196
|
const [flashEnabled, setFlashEnabled] = useState(false);
|
|
196
197
|
const [rotationDegrees, setRotationDegrees] = useState(0);
|
|
197
198
|
const [capturedPhotos, setCapturedPhotos] = useState<FullDocScannerResult[]>([]);
|
|
@@ -203,6 +204,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
203
204
|
const captureInProgressRef = useRef(false);
|
|
204
205
|
const rectangleCaptureTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
205
206
|
const rectangleHintTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
207
|
+
const captureReadyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
206
208
|
|
|
207
209
|
const isBusinessMode = type === 'business';
|
|
208
210
|
|
|
@@ -213,6 +215,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
213
215
|
setRotationDegrees(0);
|
|
214
216
|
setRectangleDetected(false);
|
|
215
217
|
setRectangleHint(false);
|
|
218
|
+
setCaptureReady(false);
|
|
216
219
|
captureModeRef.current = null;
|
|
217
220
|
captureInProgressRef.current = false;
|
|
218
221
|
|
|
@@ -225,6 +228,10 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
225
228
|
clearTimeout(rectangleHintTimeoutRef.current);
|
|
226
229
|
rectangleHintTimeoutRef.current = null;
|
|
227
230
|
}
|
|
231
|
+
if (captureReadyTimeoutRef.current) {
|
|
232
|
+
clearTimeout(captureReadyTimeoutRef.current);
|
|
233
|
+
captureReadyTimeoutRef.current = null;
|
|
234
|
+
}
|
|
228
235
|
|
|
229
236
|
if (docScannerRef.current?.reset) {
|
|
230
237
|
docScannerRef.current.reset();
|
|
@@ -505,10 +512,16 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
505
512
|
hasRef: hasScanner,
|
|
506
513
|
rectangleDetected,
|
|
507
514
|
rectangleHint,
|
|
515
|
+
captureReady,
|
|
508
516
|
currentCaptureMode: captureModeRef.current,
|
|
509
517
|
captureInProgress: captureInProgressRef.current,
|
|
510
518
|
});
|
|
511
519
|
|
|
520
|
+
if (Platform.OS === 'android' && !captureReady) {
|
|
521
|
+
console.log('[FullDocScanner] Capture not ready yet, skipping');
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
512
525
|
if (processing) {
|
|
513
526
|
console.log('[FullDocScanner] Already processing, skipping manual capture');
|
|
514
527
|
return;
|
|
@@ -567,7 +580,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
567
580
|
);
|
|
568
581
|
}
|
|
569
582
|
});
|
|
570
|
-
}, [processing, rectangleDetected, rectangleHint, emitError]);
|
|
583
|
+
}, [processing, rectangleDetected, rectangleHint, captureReady, emitError]);
|
|
571
584
|
|
|
572
585
|
const handleGalleryPick = useCallback(async () => {
|
|
573
586
|
console.log('[FullDocScanner] handleGalleryPick called');
|
|
@@ -719,7 +732,8 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
719
732
|
const stableCounter = event.stableCounter ?? 0;
|
|
720
733
|
const rectangleCoordinates = event.rectangleOnScreen ?? event.rectangleCoordinates;
|
|
721
734
|
const hasRectangle = Boolean(rectangleCoordinates);
|
|
722
|
-
const
|
|
735
|
+
const isStableForCapture =
|
|
736
|
+
hasRectangle && (Platform.OS === 'android' || (event.lastDetectionType === 0 && stableCounter >= 1));
|
|
723
737
|
|
|
724
738
|
const scheduleClear = (
|
|
725
739
|
ref: React.MutableRefObject<ReturnType<typeof setTimeout> | null>,
|
|
@@ -737,15 +751,30 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
737
751
|
if (hasRectangle) {
|
|
738
752
|
scheduleClear(rectangleHintTimeoutRef, () => setRectangleHint(false));
|
|
739
753
|
setRectangleHint(true);
|
|
754
|
+
if (Platform.OS === 'android') {
|
|
755
|
+
if (!captureReadyTimeoutRef.current) {
|
|
756
|
+
captureReadyTimeoutRef.current = setTimeout(() => {
|
|
757
|
+
setCaptureReady(true);
|
|
758
|
+
captureReadyTimeoutRef.current = null;
|
|
759
|
+
}, 1000);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
740
762
|
} else {
|
|
741
763
|
if (rectangleHintTimeoutRef.current) {
|
|
742
764
|
clearTimeout(rectangleHintTimeoutRef.current);
|
|
743
765
|
rectangleHintTimeoutRef.current = null;
|
|
744
766
|
}
|
|
745
767
|
setRectangleHint(false);
|
|
768
|
+
if (Platform.OS === 'android') {
|
|
769
|
+
if (captureReadyTimeoutRef.current) {
|
|
770
|
+
clearTimeout(captureReadyTimeoutRef.current);
|
|
771
|
+
captureReadyTimeoutRef.current = null;
|
|
772
|
+
}
|
|
773
|
+
setCaptureReady(false);
|
|
774
|
+
}
|
|
746
775
|
}
|
|
747
776
|
|
|
748
|
-
if (
|
|
777
|
+
if (isStableForCapture) {
|
|
749
778
|
scheduleClear(rectangleCaptureTimeoutRef, () => {
|
|
750
779
|
setRectangleDetected(false);
|
|
751
780
|
});
|
|
@@ -768,6 +797,9 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
768
797
|
if (rectangleHintTimeoutRef.current) {
|
|
769
798
|
clearTimeout(rectangleHintTimeoutRef.current);
|
|
770
799
|
}
|
|
800
|
+
if (captureReadyTimeoutRef.current) {
|
|
801
|
+
clearTimeout(captureReadyTimeoutRef.current);
|
|
802
|
+
}
|
|
771
803
|
},
|
|
772
804
|
[],
|
|
773
805
|
);
|
|
@@ -1000,15 +1032,19 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
1000
1032
|
</TouchableOpacity>
|
|
1001
1033
|
)}
|
|
1002
1034
|
<TouchableOpacity
|
|
1003
|
-
style={[
|
|
1035
|
+
style={[
|
|
1036
|
+
styles.shutterButton,
|
|
1037
|
+
processing && styles.buttonDisabled,
|
|
1038
|
+
Platform.OS === 'android' && !captureReady && styles.buttonDisabled,
|
|
1039
|
+
]}
|
|
1004
1040
|
onPress={triggerManualCapture}
|
|
1005
|
-
disabled={processing}
|
|
1041
|
+
disabled={processing || (Platform.OS === 'android' && !captureReady)}
|
|
1006
1042
|
accessibilityLabel={mergedStrings.manualHint}
|
|
1007
1043
|
accessibilityRole="button"
|
|
1008
1044
|
>
|
|
1009
1045
|
<View style={[
|
|
1010
1046
|
styles.shutterInner,
|
|
1011
|
-
rectangleHint && { backgroundColor: overlayColor }
|
|
1047
|
+
(Platform.OS === 'android' ? captureReady : rectangleHint) && { backgroundColor: overlayColor }
|
|
1012
1048
|
]} />
|
|
1013
1049
|
</TouchableOpacity>
|
|
1014
1050
|
<View style={styles.rightButtonsPlaceholder} />
|