react-native-rectangle-doc-scanner 7.54.0 → 7.57.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 +36 -5
- package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt +9 -0
- package/dist/DocScanner.d.ts +6 -0
- package/dist/DocScanner.js +2 -2
- package/dist/utils/overlay.d.ts +6 -0
- package/dist/utils/overlay.js +30 -7
- package/package.json +1 -1
- package/src/DocScanner.tsx +3 -0
- package/src/external.d.ts +1 -0
- package/src/utils/overlay.tsx +36 -7
|
@@ -70,6 +70,7 @@ class CameraController(
|
|
|
70
70
|
private var latestTransform: Matrix? = null
|
|
71
71
|
private var latestBufferWidth = 0
|
|
72
72
|
private var latestBufferHeight = 0
|
|
73
|
+
private var latestTransformRotation = 0
|
|
73
74
|
private val objectDetector = ObjectDetection.getClient(
|
|
74
75
|
ObjectDetectorOptions.Builder()
|
|
75
76
|
.setDetectorMode(ObjectDetectorOptions.STREAM_MODE)
|
|
@@ -216,7 +217,7 @@ class CameraController(
|
|
|
216
217
|
if (rectangle == null || imageWidth <= 0 || imageHeight <= 0) return null
|
|
217
218
|
if (latestBufferWidth <= 0 || latestBufferHeight <= 0) return null
|
|
218
219
|
|
|
219
|
-
val rotationDegrees =
|
|
220
|
+
val rotationDegrees = latestTransformRotation
|
|
220
221
|
val inverseRotation = (360 - rotationDegrees) % 360
|
|
221
222
|
|
|
222
223
|
fun rotatePoint(point: Point): Point {
|
|
@@ -276,6 +277,30 @@ class CameraController(
|
|
|
276
277
|
)
|
|
277
278
|
}
|
|
278
279
|
|
|
280
|
+
fun getPreviewViewport(): RectF? {
|
|
281
|
+
val transform = latestTransform ?: return null
|
|
282
|
+
if (latestBufferWidth <= 0 || latestBufferHeight <= 0) return null
|
|
283
|
+
val rotation = latestTransformRotation
|
|
284
|
+
val isSwapped = rotation == 90 || rotation == 270
|
|
285
|
+
val bufferWidth = if (isSwapped) latestBufferHeight.toFloat() else latestBufferWidth.toFloat()
|
|
286
|
+
val bufferHeight = if (isSwapped) latestBufferWidth.toFloat() else latestBufferHeight.toFloat()
|
|
287
|
+
|
|
288
|
+
val pts = floatArrayOf(
|
|
289
|
+
0f, 0f,
|
|
290
|
+
bufferWidth, 0f,
|
|
291
|
+
0f, bufferHeight,
|
|
292
|
+
bufferWidth, bufferHeight
|
|
293
|
+
)
|
|
294
|
+
transform.mapPoints(pts)
|
|
295
|
+
|
|
296
|
+
val minX = min(min(pts[0], pts[2]), min(pts[4], pts[6]))
|
|
297
|
+
val maxX = max(max(pts[0], pts[2]), max(pts[4], pts[6]))
|
|
298
|
+
val minY = min(min(pts[1], pts[3]), min(pts[5], pts[7]))
|
|
299
|
+
val maxY = max(max(pts[1], pts[3]), max(pts[5], pts[7]))
|
|
300
|
+
|
|
301
|
+
return RectF(minX, minY, maxX, maxY)
|
|
302
|
+
}
|
|
303
|
+
|
|
279
304
|
private fun openCamera() {
|
|
280
305
|
if (cameraDevice != null) {
|
|
281
306
|
return
|
|
@@ -636,9 +661,14 @@ class CameraController(
|
|
|
636
661
|
if (viewWidth == 0f || viewHeight == 0f) return
|
|
637
662
|
|
|
638
663
|
val rotationDegrees = computeRotationDegrees()
|
|
664
|
+
val transformRotation = if (useFrontCamera) {
|
|
665
|
+
rotationDegrees
|
|
666
|
+
} else {
|
|
667
|
+
(360 - rotationDegrees) % 360
|
|
668
|
+
}
|
|
639
669
|
Log.d(
|
|
640
670
|
TAG,
|
|
641
|
-
"[TRANSFORM] rotation=$
|
|
671
|
+
"[TRANSFORM] rotation=$transformRotation view=${viewWidth}x${viewHeight} preview=${preview.width}x${preview.height}"
|
|
642
672
|
)
|
|
643
673
|
|
|
644
674
|
val matrix = Matrix()
|
|
@@ -646,7 +676,7 @@ class CameraController(
|
|
|
646
676
|
val centerX = viewRect.centerX()
|
|
647
677
|
val centerY = viewRect.centerY()
|
|
648
678
|
|
|
649
|
-
val isSwapped =
|
|
679
|
+
val isSwapped = transformRotation == 90 || transformRotation == 270
|
|
650
680
|
val bufferWidth = if (isSwapped) preview.height.toFloat() else preview.width.toFloat()
|
|
651
681
|
val bufferHeight = if (isSwapped) preview.width.toFloat() else preview.height.toFloat()
|
|
652
682
|
val bufferRect = RectF(0f, 0f, bufferWidth, bufferHeight)
|
|
@@ -655,14 +685,15 @@ class CameraController(
|
|
|
655
685
|
matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL)
|
|
656
686
|
val scale = max(viewWidth / bufferWidth, viewHeight / bufferHeight)
|
|
657
687
|
matrix.postScale(scale, scale, centerX, centerY)
|
|
658
|
-
if (
|
|
659
|
-
matrix.postRotate(
|
|
688
|
+
if (transformRotation != 0) {
|
|
689
|
+
matrix.postRotate(transformRotation.toFloat(), centerX, centerY)
|
|
660
690
|
}
|
|
661
691
|
|
|
662
692
|
previewView.setTransform(matrix)
|
|
663
693
|
latestTransform = Matrix(matrix)
|
|
664
694
|
latestBufferWidth = preview.width
|
|
665
695
|
latestBufferHeight = preview.height
|
|
696
|
+
latestTransformRotation = transformRotation
|
|
666
697
|
|
|
667
698
|
val pts = floatArrayOf(
|
|
668
699
|
0f, 0f,
|
package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt
CHANGED
|
@@ -470,6 +470,7 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
470
470
|
imageHeight: Int
|
|
471
471
|
) {
|
|
472
472
|
val density = resources.displayMetrics.density.takeIf { it > 0f } ?: 1f
|
|
473
|
+
val previewViewport = cameraController?.getPreviewViewport()
|
|
473
474
|
val event = Arguments.createMap().apply {
|
|
474
475
|
putInt("stableCounter", stableCounter)
|
|
475
476
|
putInt("lastDetectionType", quality.ordinal)
|
|
@@ -480,6 +481,14 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
480
481
|
putMap("bottomLeft", mapPointToDp(getMap("bottomLeft"), density))
|
|
481
482
|
putMap("bottomRight", mapPointToDp(getMap("bottomRight"), density))
|
|
482
483
|
})
|
|
484
|
+
previewViewport?.let {
|
|
485
|
+
putMap("previewViewport", Arguments.createMap().apply {
|
|
486
|
+
putDouble("left", it.left / density)
|
|
487
|
+
putDouble("top", it.top / density)
|
|
488
|
+
putDouble("width", it.width() / density)
|
|
489
|
+
putDouble("height", it.height() / density)
|
|
490
|
+
})
|
|
491
|
+
}
|
|
483
492
|
putMap("previewSize", Arguments.createMap().apply {
|
|
484
493
|
putInt("width", (width / density).toInt())
|
|
485
494
|
putInt("height", (height / density).toInt())
|
package/dist/DocScanner.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ type PictureEvent = {
|
|
|
12
12
|
export type RectangleDetectEvent = Omit<RectangleEventPayload, 'rectangleCoordinates' | 'rectangleOnScreen'> & {
|
|
13
13
|
rectangleCoordinates?: Rectangle | null;
|
|
14
14
|
rectangleOnScreen?: Rectangle | null;
|
|
15
|
+
previewViewport?: {
|
|
16
|
+
left: number;
|
|
17
|
+
top: number;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
};
|
|
15
21
|
};
|
|
16
22
|
export type DocScannerCapture = {
|
|
17
23
|
path: string;
|
package/dist/DocScanner.js
CHANGED
|
@@ -418,7 +418,7 @@ const VisionCameraScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor =
|
|
|
418
418
|
const overlayIsActive = autoCapture ? isAutoCapturing : (detectedRectangle?.stableCounter ?? 0) > 0;
|
|
419
419
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container, onLayout: handleLayout },
|
|
420
420
|
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 })),
|
|
421
|
+
showGrid && overlayPolygon && (react_1.default.createElement(overlay_1.ScannerOverlay, { active: overlayIsActive, color: gridColor ?? overlayColor, lineWidth: gridLineWidth, polygon: overlayPolygon, clipRect: detectedRectangle?.previewViewport ?? null })),
|
|
422
422
|
showManualCaptureButton && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: () => captureVision('manual') })),
|
|
423
423
|
children));
|
|
424
424
|
});
|
|
@@ -705,7 +705,7 @@ const NativeScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAU
|
|
|
705
705
|
const detectionThreshold = autoCapture ? minStableFrames : 99999;
|
|
706
706
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
707
707
|
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 })),
|
|
708
|
+
showGrid && overlayPolygon && (react_1.default.createElement(overlay_1.ScannerOverlay, { active: overlayIsActive, color: gridColor ?? overlayColor, lineWidth: gridLineWidth, polygon: overlayPolygon, clipRect: detectedRectangle?.previewViewport ?? null })),
|
|
709
709
|
showManualCaptureButton && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: handleManualCapture })),
|
|
710
710
|
children));
|
|
711
711
|
});
|
package/dist/utils/overlay.d.ts
CHANGED
|
@@ -5,5 +5,11 @@ export interface ScannerOverlayProps {
|
|
|
5
5
|
color?: string;
|
|
6
6
|
lineWidth?: number;
|
|
7
7
|
polygon?: Rectangle | null;
|
|
8
|
+
clipRect?: {
|
|
9
|
+
left: number;
|
|
10
|
+
top: number;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
} | null;
|
|
8
14
|
}
|
|
9
15
|
export declare const ScannerOverlay: React.FC<ScannerOverlayProps>;
|
package/dist/utils/overlay.js
CHANGED
|
@@ -78,22 +78,45 @@ const getBounds = (polygon) => {
|
|
|
78
78
|
};
|
|
79
79
|
};
|
|
80
80
|
const ScannerOverlay = ({ active: _active, // kept for compatibility; no animation currently
|
|
81
|
-
color = '#0b7ef4', lineWidth = react_native_1.StyleSheet.hairlineWidth, polygon, }) => {
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
color = '#0b7ef4', lineWidth = react_native_1.StyleSheet.hairlineWidth, polygon, clipRect, }) => {
|
|
82
|
+
const offset = (0, react_1.useMemo)(() => (clipRect ? { x: -clipRect.left, y: -clipRect.top } : { x: 0, y: 0 }), [clipRect]);
|
|
83
|
+
const shiftedPolygon = (0, react_1.useMemo)(() => {
|
|
84
|
+
if (!polygon)
|
|
85
|
+
return null;
|
|
86
|
+
return {
|
|
87
|
+
topLeft: { x: polygon.topLeft.x + offset.x, y: polygon.topLeft.y + offset.y },
|
|
88
|
+
topRight: { x: polygon.topRight.x + offset.x, y: polygon.topRight.y + offset.y },
|
|
89
|
+
bottomRight: { x: polygon.bottomRight.x + offset.x, y: polygon.bottomRight.y + offset.y },
|
|
90
|
+
bottomLeft: { x: polygon.bottomLeft.x + offset.x, y: polygon.bottomLeft.y + offset.y },
|
|
91
|
+
};
|
|
92
|
+
}, [polygon, offset]);
|
|
93
|
+
const points = (0, react_1.useMemo)(() => (shiftedPolygon ? createPointsString(shiftedPolygon) : null), [shiftedPolygon]);
|
|
94
|
+
const gridLines = (0, react_1.useMemo)(() => (shiftedPolygon ? createGridLines(shiftedPolygon) : []), [shiftedPolygon]);
|
|
95
|
+
const bounds = (0, react_1.useMemo)(() => (shiftedPolygon ? getBounds(shiftedPolygon) : null), [shiftedPolygon]);
|
|
96
|
+
if (!shiftedPolygon || !points || !bounds) {
|
|
86
97
|
return null;
|
|
87
98
|
}
|
|
99
|
+
const containerStyle = clipRect
|
|
100
|
+
? [
|
|
101
|
+
react_native_1.StyleSheet.absoluteFill,
|
|
102
|
+
{
|
|
103
|
+
left: clipRect.left,
|
|
104
|
+
top: clipRect.top,
|
|
105
|
+
width: clipRect.width,
|
|
106
|
+
height: clipRect.height,
|
|
107
|
+
overflow: 'hidden',
|
|
108
|
+
},
|
|
109
|
+
]
|
|
110
|
+
: react_native_1.StyleSheet.absoluteFill;
|
|
88
111
|
if (SvgModule) {
|
|
89
112
|
const { default: Svg, Polygon, Line } = SvgModule;
|
|
90
|
-
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style:
|
|
113
|
+
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: containerStyle },
|
|
91
114
|
react_1.default.createElement(Svg, { style: react_native_1.StyleSheet.absoluteFill },
|
|
92
115
|
react_1.default.createElement(Polygon, { points: points, fill: color, opacity: 0.15 }),
|
|
93
116
|
gridLines.map((line, index) => (react_1.default.createElement(Line, { key: `grid-${index}`, x1: line.x1, y1: line.y1, x2: line.x2, y2: line.y2, stroke: color, strokeWidth: lineWidth, opacity: 0.5 }))),
|
|
94
117
|
react_1.default.createElement(Polygon, { points: points, stroke: color, strokeWidth: lineWidth, fill: "none" }))));
|
|
95
118
|
}
|
|
96
|
-
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style:
|
|
119
|
+
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: containerStyle },
|
|
97
120
|
react_1.default.createElement(react_native_1.View, { style: [
|
|
98
121
|
styles.fallbackBox,
|
|
99
122
|
{
|
package/package.json
CHANGED
package/src/DocScanner.tsx
CHANGED
|
@@ -35,6 +35,7 @@ type PictureEvent = {
|
|
|
35
35
|
export type RectangleDetectEvent = Omit<RectangleEventPayload, 'rectangleCoordinates' | 'rectangleOnScreen'> & {
|
|
36
36
|
rectangleCoordinates?: Rectangle | null;
|
|
37
37
|
rectangleOnScreen?: Rectangle | null;
|
|
38
|
+
previewViewport?: { left: number; top: number; width: number; height: number };
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
export type DocScannerCapture = {
|
|
@@ -600,6 +601,7 @@ const VisionCameraScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
600
601
|
color={gridColor ?? overlayColor}
|
|
601
602
|
lineWidth={gridLineWidth}
|
|
602
603
|
polygon={overlayPolygon}
|
|
604
|
+
clipRect={detectedRectangle?.previewViewport ?? null}
|
|
603
605
|
/>
|
|
604
606
|
)}
|
|
605
607
|
{showManualCaptureButton && (
|
|
@@ -1000,6 +1002,7 @@ const NativeScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
1000
1002
|
color={gridColor ?? overlayColor}
|
|
1001
1003
|
lineWidth={gridLineWidth}
|
|
1002
1004
|
polygon={overlayPolygon}
|
|
1005
|
+
clipRect={detectedRectangle?.previewViewport ?? null}
|
|
1003
1006
|
/>
|
|
1004
1007
|
)}
|
|
1005
1008
|
{showManualCaptureButton && (
|
package/src/external.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ declare module 'react-native-document-scanner' {
|
|
|
46
46
|
lastDetectionType: number;
|
|
47
47
|
rectangleCoordinates?: Rectangle | null;
|
|
48
48
|
rectangleOnScreen?: Rectangle | null;
|
|
49
|
+
previewViewport?: { left: number; top: number; width: number; height: number };
|
|
49
50
|
previewSize?: { width: number; height: number };
|
|
50
51
|
imageSize?: { width: number; height: number };
|
|
51
52
|
};
|
package/src/utils/overlay.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
2
|
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
4
|
import type { Rectangle } from '../types';
|
|
4
5
|
|
|
5
6
|
let SvgModule: typeof import('react-native-svg') | null = null;
|
|
@@ -78,6 +79,7 @@ export interface ScannerOverlayProps {
|
|
|
78
79
|
color?: string;
|
|
79
80
|
lineWidth?: number;
|
|
80
81
|
polygon?: Rectangle | null;
|
|
82
|
+
clipRect?: { left: number; top: number; width: number; height: number } | null;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
export const ScannerOverlay: React.FC<ScannerOverlayProps> = ({
|
|
@@ -85,20 +87,47 @@ export const ScannerOverlay: React.FC<ScannerOverlayProps> = ({
|
|
|
85
87
|
color = '#0b7ef4',
|
|
86
88
|
lineWidth = StyleSheet.hairlineWidth,
|
|
87
89
|
polygon,
|
|
90
|
+
clipRect,
|
|
88
91
|
}) => {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
const offset = useMemo(
|
|
93
|
+
() => (clipRect ? { x: -clipRect.left, y: -clipRect.top } : { x: 0, y: 0 }),
|
|
94
|
+
[clipRect],
|
|
95
|
+
);
|
|
96
|
+
const shiftedPolygon = useMemo(() => {
|
|
97
|
+
if (!polygon) return null;
|
|
98
|
+
return {
|
|
99
|
+
topLeft: { x: polygon.topLeft.x + offset.x, y: polygon.topLeft.y + offset.y },
|
|
100
|
+
topRight: { x: polygon.topRight.x + offset.x, y: polygon.topRight.y + offset.y },
|
|
101
|
+
bottomRight: { x: polygon.bottomRight.x + offset.x, y: polygon.bottomRight.y + offset.y },
|
|
102
|
+
bottomLeft: { x: polygon.bottomLeft.x + offset.x, y: polygon.bottomLeft.y + offset.y },
|
|
103
|
+
};
|
|
104
|
+
}, [polygon, offset]);
|
|
105
|
+
const points = useMemo(() => (shiftedPolygon ? createPointsString(shiftedPolygon) : null), [shiftedPolygon]);
|
|
106
|
+
const gridLines = useMemo(() => (shiftedPolygon ? createGridLines(shiftedPolygon) : []), [shiftedPolygon]);
|
|
107
|
+
const bounds = useMemo(() => (shiftedPolygon ? getBounds(shiftedPolygon) : null), [shiftedPolygon]);
|
|
108
|
+
|
|
109
|
+
if (!shiftedPolygon || !points || !bounds) {
|
|
94
110
|
return null;
|
|
95
111
|
}
|
|
96
112
|
|
|
113
|
+
const containerStyle: StyleProp<ViewStyle> = clipRect
|
|
114
|
+
? [
|
|
115
|
+
StyleSheet.absoluteFill as ViewStyle,
|
|
116
|
+
{
|
|
117
|
+
left: clipRect.left,
|
|
118
|
+
top: clipRect.top,
|
|
119
|
+
width: clipRect.width,
|
|
120
|
+
height: clipRect.height,
|
|
121
|
+
overflow: 'hidden',
|
|
122
|
+
},
|
|
123
|
+
]
|
|
124
|
+
: (StyleSheet.absoluteFill as ViewStyle);
|
|
125
|
+
|
|
97
126
|
if (SvgModule) {
|
|
98
127
|
const { default: Svg, Polygon, Line } = SvgModule;
|
|
99
128
|
|
|
100
129
|
return (
|
|
101
|
-
<View pointerEvents="none" style={
|
|
130
|
+
<View pointerEvents="none" style={containerStyle}>
|
|
102
131
|
<Svg style={StyleSheet.absoluteFill}>
|
|
103
132
|
<Polygon points={points} fill={color} opacity={0.15} />
|
|
104
133
|
{gridLines.map((line, index) => (
|
|
@@ -120,7 +149,7 @@ export const ScannerOverlay: React.FC<ScannerOverlayProps> = ({
|
|
|
120
149
|
}
|
|
121
150
|
|
|
122
151
|
return (
|
|
123
|
-
<View pointerEvents="none" style={
|
|
152
|
+
<View pointerEvents="none" style={containerStyle}>
|
|
124
153
|
<View
|
|
125
154
|
style={[
|
|
126
155
|
styles.fallbackBox,
|