react-native-rectangle-doc-scanner 3.26.0 → 3.29.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/dist/CropEditor.js +11 -6
- package/dist/DocScanner.d.ts +1 -0
- package/dist/DocScanner.js +10 -5
- package/dist/FullDocScanner.js +36 -7
- package/dist/types.d.ts +1 -0
- package/dist/utils/overlay.js +25 -90
- package/package.json +1 -1
- package/src/CropEditor.tsx +15 -8
- package/src/DocScanner.tsx +14 -6
- package/src/FullDocScanner.tsx +62 -10
- package/src/types.ts +1 -0
- package/src/utils/overlay.tsx +39 -142
package/dist/CropEditor.js
CHANGED
|
@@ -83,17 +83,22 @@ const CropEditor = ({ document, overlayColor = 'rgba(0,0,0,0.5)', overlayStrokeC
|
|
|
83
83
|
}, [document]);
|
|
84
84
|
// Get initial rectangle from detected quad or use default
|
|
85
85
|
const getInitialRectangle = (0, react_1.useCallback)(() => {
|
|
86
|
-
if (!
|
|
86
|
+
if (!imageSize) {
|
|
87
87
|
return undefined;
|
|
88
88
|
}
|
|
89
|
-
const
|
|
90
|
-
|
|
89
|
+
const baseWidth = document.width > 0 ? document.width : imageSize.width;
|
|
90
|
+
const baseHeight = document.height > 0 ? document.height : imageSize.height;
|
|
91
|
+
const sourceRectangle = document.rectangle
|
|
92
|
+
? document.rectangle
|
|
93
|
+
: document.quad && document.quad.length === 4
|
|
94
|
+
? (0, coordinate_1.quadToRectangle)(document.quad)
|
|
95
|
+
: null;
|
|
96
|
+
if (!sourceRectangle) {
|
|
91
97
|
return undefined;
|
|
92
98
|
}
|
|
93
|
-
|
|
94
|
-
const scaled = (0, coordinate_1.scaleRectangle)(rect, document.width, document.height, imageSize.width, imageSize.height);
|
|
99
|
+
const scaled = (0, coordinate_1.scaleRectangle)(sourceRectangle, baseWidth, baseHeight, imageSize.width, imageSize.height);
|
|
95
100
|
return scaled;
|
|
96
|
-
}, [document.quad, document.width, document.height, imageSize]);
|
|
101
|
+
}, [document.rectangle, document.quad, document.width, document.height, imageSize]);
|
|
97
102
|
const handleImageLoad = (0, react_1.useCallback)((event) => {
|
|
98
103
|
// This is just for debugging - actual size is loaded via Image.getSize in useEffect
|
|
99
104
|
console.log('[CropEditor] Image onLoad event triggered');
|
package/dist/DocScanner.d.ts
CHANGED
package/dist/DocScanner.js
CHANGED
|
@@ -73,6 +73,7 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
73
73
|
const captureResolvers = (0, react_1.useRef)(null);
|
|
74
74
|
const [isAutoCapturing, setIsAutoCapturing] = (0, react_1.useState)(false);
|
|
75
75
|
const [detectedRectangle, setDetectedRectangle] = (0, react_1.useState)(null);
|
|
76
|
+
const lastRectangleRef = (0, react_1.useRef)(null);
|
|
76
77
|
(0, react_1.useEffect)(() => {
|
|
77
78
|
if (!autoCapture) {
|
|
78
79
|
setIsAutoCapturing(false);
|
|
@@ -87,17 +88,18 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
87
88
|
}, [quality]);
|
|
88
89
|
const handlePictureTaken = (0, react_1.useCallback)((event) => {
|
|
89
90
|
setIsAutoCapturing(false);
|
|
90
|
-
const normalizedRectangle = normalizeRectangle(event.rectangleCoordinates ?? null);
|
|
91
|
+
const normalizedRectangle = normalizeRectangle(event.rectangleCoordinates ?? null) ?? lastRectangleRef.current;
|
|
91
92
|
const quad = normalizedRectangle ? (0, coordinate_1.rectangleToQuad)(normalizedRectangle) : null;
|
|
92
93
|
const initialPath = event.initialImage ?? null;
|
|
93
94
|
const croppedPath = event.croppedImage ?? null;
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
95
|
+
const editablePath = initialPath ?? croppedPath;
|
|
96
|
+
if (editablePath) {
|
|
96
97
|
onCapture?.({
|
|
97
|
-
path:
|
|
98
|
+
path: editablePath,
|
|
98
99
|
initialPath,
|
|
99
100
|
croppedPath,
|
|
100
101
|
quad,
|
|
102
|
+
rectangle: normalizedRectangle,
|
|
101
103
|
width: event.width ?? 0,
|
|
102
104
|
height: event.height ?? 0,
|
|
103
105
|
});
|
|
@@ -157,6 +159,9 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
157
159
|
setIsAutoCapturing(false);
|
|
158
160
|
}
|
|
159
161
|
}
|
|
162
|
+
if (payload.rectangleCoordinates) {
|
|
163
|
+
lastRectangleRef.current = payload.rectangleCoordinates;
|
|
164
|
+
}
|
|
160
165
|
const isGoodRectangle = payload.lastDetectionType === 0;
|
|
161
166
|
setDetectedRectangle(isGoodRectangle && rectangleOnScreen ? payload : null);
|
|
162
167
|
onRectangleDetect?.(payload);
|
|
@@ -170,7 +175,7 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
170
175
|
}
|
|
171
176
|
},
|
|
172
177
|
}), [capture]);
|
|
173
|
-
const overlayPolygon = detectedRectangle?.rectangleOnScreen ?? null;
|
|
178
|
+
const overlayPolygon = detectedRectangle?.rectangleOnScreen ?? detectedRectangle?.rectangleCoordinates ?? null;
|
|
174
179
|
const overlayIsActive = autoCapture ? isAutoCapturing : (detectedRectangle?.stableCounter ?? 0) > 0;
|
|
175
180
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
176
181
|
react_1.default.createElement(react_native_document_scanner_1.default, { ref: scannerRef, style: styles.scanner, detectionCountBeforeCapture: minStableFrames, overlayColor: overlayColor, enableTorch: enableTorch, quality: normalizedQuality, useBase64: useBase64, manualOnly: !autoCapture, detectionConfig: detectionConfig, onPictureTaken: handlePictureTaken, onError: handleError, onRectangleDetect: handleRectangleDetect }),
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -63,6 +63,26 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
63
63
|
}
|
|
64
64
|
react_native_1.Image.getSize(ensureFileUri(capturedDoc.path), (width, height) => setImageSize({ width, height }), () => setImageSize({ width: capturedDoc.width, height: capturedDoc.height }));
|
|
65
65
|
}, [capturedDoc]);
|
|
66
|
+
(0, react_1.useEffect)(() => {
|
|
67
|
+
if (!capturedDoc || !imageSize || cropRectangle) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const baseWidth = capturedDoc.width > 0 ? capturedDoc.width : imageSize.width;
|
|
71
|
+
const baseHeight = capturedDoc.height > 0 ? capturedDoc.height : imageSize.height;
|
|
72
|
+
let initialRectangle = null;
|
|
73
|
+
if (capturedDoc.rectangle) {
|
|
74
|
+
initialRectangle = (0, coordinate_1.scaleRectangle)(capturedDoc.rectangle, baseWidth, baseHeight, imageSize.width, imageSize.height);
|
|
75
|
+
}
|
|
76
|
+
else if (capturedDoc.quad && capturedDoc.quad.length === 4) {
|
|
77
|
+
const quadRectangle = (0, coordinate_1.quadToRectangle)(capturedDoc.quad);
|
|
78
|
+
if (quadRectangle) {
|
|
79
|
+
initialRectangle = (0, coordinate_1.scaleRectangle)(quadRectangle, baseWidth, baseHeight, imageSize.width, imageSize.height);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (initialRectangle) {
|
|
83
|
+
setCropRectangle(initialRectangle);
|
|
84
|
+
}
|
|
85
|
+
}, [capturedDoc, imageSize, cropRectangle]);
|
|
66
86
|
const resetState = (0, react_1.useCallback)(() => {
|
|
67
87
|
setScreen('scanner');
|
|
68
88
|
setCapturedDoc(null);
|
|
@@ -73,6 +93,8 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
73
93
|
const handleCapture = (0, react_1.useCallback)((document) => {
|
|
74
94
|
const normalizedPath = stripFileUri(document.path);
|
|
75
95
|
const nextQuad = document.quad && document.quad.length === 4 ? document.quad : null;
|
|
96
|
+
const quadRectangle = nextQuad ? (0, coordinate_1.quadToRectangle)(nextQuad) : null;
|
|
97
|
+
const nextRectangle = document.rectangle ?? quadRectangle ?? null;
|
|
76
98
|
const normalizedInitial = document.initialPath != null ? stripFileUri(document.initialPath) : normalizedPath;
|
|
77
99
|
const normalizedCropped = document.croppedPath != null ? stripFileUri(document.croppedPath) : null;
|
|
78
100
|
setCapturedDoc({
|
|
@@ -81,8 +103,9 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
81
103
|
initialPath: normalizedInitial,
|
|
82
104
|
croppedPath: normalizedCropped,
|
|
83
105
|
quad: nextQuad,
|
|
106
|
+
rectangle: nextRectangle,
|
|
84
107
|
});
|
|
85
|
-
setCropRectangle(
|
|
108
|
+
setCropRectangle(null);
|
|
86
109
|
setScreen('crop');
|
|
87
110
|
}, []);
|
|
88
111
|
const handleCropChange = (0, react_1.useCallback)((rectangle) => {
|
|
@@ -104,13 +127,19 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
104
127
|
if (!cropManager?.crop) {
|
|
105
128
|
throw new Error('CustomCropManager.crop is not available');
|
|
106
129
|
}
|
|
107
|
-
const
|
|
108
|
-
|
|
130
|
+
const baseWidth = capturedDoc.width > 0 ? capturedDoc.width : size.width;
|
|
131
|
+
const baseHeight = capturedDoc.height > 0 ? capturedDoc.height : size.height;
|
|
132
|
+
const rectangleFromDetection = capturedDoc.rectangle
|
|
133
|
+
? (0, coordinate_1.scaleRectangle)(capturedDoc.rectangle, baseWidth, baseHeight, size.width, size.height)
|
|
109
134
|
: null;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
135
|
+
let fallbackRectangle = rectangleFromDetection;
|
|
136
|
+
if (!fallbackRectangle && capturedDoc.quad && capturedDoc.quad.length === 4) {
|
|
137
|
+
const quadRectangle = (0, coordinate_1.quadToRectangle)(capturedDoc.quad);
|
|
138
|
+
if (quadRectangle) {
|
|
139
|
+
fallbackRectangle = (0, coordinate_1.scaleRectangle)(quadRectangle, baseWidth, baseHeight, size.width, size.height);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const rectangle = cropRectangle ?? fallbackRectangle;
|
|
114
143
|
const base64 = await new Promise((resolve, reject) => {
|
|
115
144
|
cropManager.crop({
|
|
116
145
|
topLeft: rectangle?.topLeft ?? { x: 0, y: 0 },
|
package/dist/types.d.ts
CHANGED
package/dist/utils/overlay.js
CHANGED
|
@@ -44,23 +44,7 @@ try {
|
|
|
44
44
|
catch (error) {
|
|
45
45
|
SvgModule = null;
|
|
46
46
|
}
|
|
47
|
-
const SCAN_DURATION_MS = 2200;
|
|
48
47
|
const GRID_STEPS = [1 / 3, 2 / 3];
|
|
49
|
-
const calculateMetrics = (polygon) => {
|
|
50
|
-
const minX = Math.min(polygon.topLeft.x, polygon.bottomLeft.x, polygon.topRight.x, polygon.bottomRight.x);
|
|
51
|
-
const maxX = Math.max(polygon.topLeft.x, polygon.bottomLeft.x, polygon.topRight.x, polygon.bottomRight.x);
|
|
52
|
-
const minY = Math.min(polygon.topLeft.y, polygon.topRight.y, polygon.bottomLeft.y, polygon.bottomRight.y);
|
|
53
|
-
const maxY = Math.max(polygon.topLeft.y, polygon.topRight.y, polygon.bottomLeft.y, polygon.bottomRight.y);
|
|
54
|
-
return {
|
|
55
|
-
minX,
|
|
56
|
-
maxX,
|
|
57
|
-
minY,
|
|
58
|
-
maxY,
|
|
59
|
-
width: maxX - minX,
|
|
60
|
-
height: maxY - minY,
|
|
61
|
-
centerX: minX + (maxX - minX) / 2,
|
|
62
|
-
};
|
|
63
|
-
};
|
|
64
48
|
const createPointsString = (polygon) => [
|
|
65
49
|
`${polygon.topLeft.x},${polygon.topLeft.y}`,
|
|
66
50
|
`${polygon.topRight.x},${polygon.topRight.y}`,
|
|
@@ -81,100 +65,51 @@ const createGridLines = (polygon) => GRID_STEPS.flatMap((step) => {
|
|
|
81
65
|
{ x1: verticalStart.x, y1: verticalStart.y, x2: verticalEnd.x, y2: verticalEnd.y },
|
|
82
66
|
];
|
|
83
67
|
});
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
(0, react_1.useEffect)(() => {
|
|
103
|
-
if (!active || !metrics || metrics.height <= 1) {
|
|
104
|
-
scanProgress.stopAnimation();
|
|
105
|
-
scanProgress.setValue(0);
|
|
106
|
-
return undefined;
|
|
107
|
-
}
|
|
108
|
-
const loop = react_native_1.Animated.loop(react_native_1.Animated.sequence([
|
|
109
|
-
react_native_1.Animated.timing(scanProgress, {
|
|
110
|
-
toValue: 1,
|
|
111
|
-
duration: SCAN_DURATION_MS,
|
|
112
|
-
easing: react_native_1.Easing.inOut(react_native_1.Easing.quad),
|
|
113
|
-
useNativeDriver: false,
|
|
114
|
-
}),
|
|
115
|
-
react_native_1.Animated.timing(scanProgress, {
|
|
116
|
-
toValue: 0,
|
|
117
|
-
duration: SCAN_DURATION_MS,
|
|
118
|
-
easing: react_native_1.Easing.inOut(react_native_1.Easing.quad),
|
|
119
|
-
useNativeDriver: false,
|
|
120
|
-
}),
|
|
121
|
-
]));
|
|
122
|
-
loop.start();
|
|
123
|
-
return () => {
|
|
124
|
-
loop.stop();
|
|
125
|
-
};
|
|
126
|
-
}, [active, metrics, scanProgress]);
|
|
127
|
-
if (!polygon || !metrics || metrics.width <= 0 || metrics.height <= 0) {
|
|
68
|
+
const getBounds = (polygon) => {
|
|
69
|
+
const minX = Math.min(polygon.topLeft.x, polygon.bottomLeft.x, polygon.topRight.x, polygon.bottomRight.x);
|
|
70
|
+
const maxX = Math.max(polygon.topLeft.x, polygon.bottomLeft.x, polygon.topRight.x, polygon.bottomRight.x);
|
|
71
|
+
const minY = Math.min(polygon.topLeft.y, polygon.topRight.y, polygon.bottomLeft.y, polygon.bottomRight.y);
|
|
72
|
+
const maxY = Math.max(polygon.topLeft.y, polygon.topRight.y, polygon.bottomLeft.y, polygon.bottomRight.y);
|
|
73
|
+
return {
|
|
74
|
+
minX,
|
|
75
|
+
minY,
|
|
76
|
+
width: maxX - minX,
|
|
77
|
+
height: maxY - minY,
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const ScannerOverlay = ({ active: _active, // kept for compatibility; no animation currently
|
|
81
|
+
color = '#0b7ef4', lineWidth = react_native_1.StyleSheet.hairlineWidth, polygon, }) => {
|
|
82
|
+
const points = (0, react_1.useMemo)(() => (polygon ? createPointsString(polygon) : null), [polygon]);
|
|
83
|
+
const gridLines = (0, react_1.useMemo)(() => (polygon ? createGridLines(polygon) : []), [polygon]);
|
|
84
|
+
const bounds = (0, react_1.useMemo)(() => (polygon ? getBounds(polygon) : null), [polygon]);
|
|
85
|
+
if (!polygon || !points || !bounds) {
|
|
128
86
|
return null;
|
|
129
87
|
}
|
|
130
88
|
if (SvgModule) {
|
|
131
|
-
const { default: Svg, Polygon, Line
|
|
132
|
-
const AnimatedRect = react_native_1.Animated.createAnimatedComponent(Rect);
|
|
133
|
-
const gridLines = createGridLines(polygon);
|
|
134
|
-
const points = createPointsString(polygon);
|
|
89
|
+
const { default: Svg, Polygon, Line } = SvgModule;
|
|
135
90
|
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: react_native_1.StyleSheet.absoluteFill },
|
|
136
91
|
react_1.default.createElement(Svg, { style: react_native_1.StyleSheet.absoluteFill },
|
|
137
92
|
react_1.default.createElement(Polygon, { points: points, fill: color, opacity: 0.15 }),
|
|
138
93
|
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 }))),
|
|
139
|
-
react_1.default.createElement(Polygon, { points: points, stroke: color, strokeWidth: lineWidth, fill: "none" })
|
|
140
|
-
react_1.default.createElement(Defs, null,
|
|
141
|
-
react_1.default.createElement(LinearGradient, { id: "scanGradient", x1: "0", y1: "0", x2: "0", y2: "1" },
|
|
142
|
-
react_1.default.createElement(Stop, { offset: "0%", stopColor: "rgba(255,255,255,0)" }),
|
|
143
|
-
react_1.default.createElement(Stop, { offset: "50%", stopColor: color, stopOpacity: 0.8 }),
|
|
144
|
-
react_1.default.createElement(Stop, { offset: "100%", stopColor: "rgba(255,255,255,0)" }))),
|
|
145
|
-
active && scanTranslate && (react_1.default.createElement(AnimatedRect, { x: metrics.minX, width: metrics.width, height: scanBarHeight, fill: "url(#scanGradient)", y: scanTranslate })))));
|
|
94
|
+
react_1.default.createElement(Polygon, { points: points, stroke: color, strokeWidth: lineWidth, fill: "none" }))));
|
|
146
95
|
}
|
|
147
|
-
// Fallback rendering without react-native-svg
|
|
148
|
-
const relativeTranslate = scanTranslate != null ? react_native_1.Animated.subtract(scanTranslate, metrics.minY) : fallbackBase;
|
|
149
96
|
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: react_native_1.StyleSheet.absoluteFill },
|
|
150
97
|
react_1.default.createElement(react_native_1.View, { style: [
|
|
151
98
|
styles.fallbackBox,
|
|
152
99
|
{
|
|
153
|
-
left:
|
|
154
|
-
top:
|
|
155
|
-
width:
|
|
156
|
-
height:
|
|
100
|
+
left: bounds.minX,
|
|
101
|
+
top: bounds.minY,
|
|
102
|
+
width: bounds.width,
|
|
103
|
+
height: bounds.height,
|
|
157
104
|
borderColor: color,
|
|
158
105
|
borderWidth: lineWidth,
|
|
159
106
|
},
|
|
160
|
-
] }
|
|
161
|
-
styles.fallbackScanBar,
|
|
162
|
-
{
|
|
163
|
-
backgroundColor: color,
|
|
164
|
-
height: scanBarHeight,
|
|
165
|
-
transform: [{ translateY: relativeTranslate }],
|
|
166
|
-
},
|
|
167
|
-
] })))));
|
|
107
|
+
] })));
|
|
168
108
|
};
|
|
169
109
|
exports.ScannerOverlay = ScannerOverlay;
|
|
170
110
|
const styles = react_native_1.StyleSheet.create({
|
|
171
111
|
fallbackBox: {
|
|
172
112
|
position: 'absolute',
|
|
173
113
|
backgroundColor: 'rgba(11, 126, 244, 0.1)',
|
|
174
|
-
overflow: 'hidden',
|
|
175
|
-
},
|
|
176
|
-
fallbackScanBar: {
|
|
177
|
-
width: '100%',
|
|
178
|
-
opacity: 0.4,
|
|
179
114
|
},
|
|
180
115
|
});
|
package/package.json
CHANGED
package/src/CropEditor.tsx
CHANGED
|
@@ -70,26 +70,33 @@ export const CropEditor: React.FC<CropEditorProps> = ({
|
|
|
70
70
|
|
|
71
71
|
// Get initial rectangle from detected quad or use default
|
|
72
72
|
const getInitialRectangle = useCallback((): CropperRectangle | undefined => {
|
|
73
|
-
if (!
|
|
73
|
+
if (!imageSize) {
|
|
74
74
|
return undefined;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const
|
|
78
|
-
|
|
77
|
+
const baseWidth = document.width > 0 ? document.width : imageSize.width;
|
|
78
|
+
const baseHeight = document.height > 0 ? document.height : imageSize.height;
|
|
79
|
+
|
|
80
|
+
const sourceRectangle = document.rectangle
|
|
81
|
+
? document.rectangle
|
|
82
|
+
: document.quad && document.quad.length === 4
|
|
83
|
+
? quadToRectangle(document.quad)
|
|
84
|
+
: null;
|
|
85
|
+
|
|
86
|
+
if (!sourceRectangle) {
|
|
79
87
|
return undefined;
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
// Scale from original detection coordinates to image coordinates
|
|
83
90
|
const scaled = scaleRectangle(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
sourceRectangle,
|
|
92
|
+
baseWidth,
|
|
93
|
+
baseHeight,
|
|
87
94
|
imageSize.width,
|
|
88
95
|
imageSize.height
|
|
89
96
|
);
|
|
90
97
|
|
|
91
98
|
return scaled as CropperRectangle;
|
|
92
|
-
}, [document.quad, document.width, document.height, imageSize]);
|
|
99
|
+
}, [document.rectangle, document.quad, document.width, document.height, imageSize]);
|
|
93
100
|
|
|
94
101
|
const handleImageLoad = useCallback((event: any) => {
|
|
95
102
|
// This is just for debugging - actual size is loaded via Image.getSize in useEffect
|
package/src/DocScanner.tsx
CHANGED
|
@@ -33,6 +33,7 @@ export type DocScannerCapture = {
|
|
|
33
33
|
initialPath: string | null;
|
|
34
34
|
croppedPath: string | null;
|
|
35
35
|
quad: Point[] | null;
|
|
36
|
+
rectangle: Rectangle | null;
|
|
36
37
|
width: number;
|
|
37
38
|
height: number;
|
|
38
39
|
};
|
|
@@ -127,6 +128,7 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
127
128
|
} | null>(null);
|
|
128
129
|
const [isAutoCapturing, setIsAutoCapturing] = useState(false);
|
|
129
130
|
const [detectedRectangle, setDetectedRectangle] = useState<RectangleDetectEvent | null>(null);
|
|
131
|
+
const lastRectangleRef = useRef<Rectangle | null>(null);
|
|
130
132
|
|
|
131
133
|
useEffect(() => {
|
|
132
134
|
if (!autoCapture) {
|
|
@@ -146,19 +148,21 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
146
148
|
(event: PictureEvent) => {
|
|
147
149
|
setIsAutoCapturing(false);
|
|
148
150
|
|
|
149
|
-
const normalizedRectangle =
|
|
151
|
+
const normalizedRectangle =
|
|
152
|
+
normalizeRectangle(event.rectangleCoordinates ?? null) ?? lastRectangleRef.current;
|
|
150
153
|
const quad = normalizedRectangle ? rectangleToQuad(normalizedRectangle) : null;
|
|
151
154
|
|
|
152
155
|
const initialPath = event.initialImage ?? null;
|
|
153
156
|
const croppedPath = event.croppedImage ?? null;
|
|
154
|
-
const
|
|
157
|
+
const editablePath = initialPath ?? croppedPath;
|
|
155
158
|
|
|
156
|
-
if (
|
|
159
|
+
if (editablePath) {
|
|
157
160
|
onCapture?.({
|
|
158
|
-
path:
|
|
161
|
+
path: editablePath,
|
|
159
162
|
initialPath,
|
|
160
163
|
croppedPath,
|
|
161
164
|
quad,
|
|
165
|
+
rectangle: normalizedRectangle,
|
|
162
166
|
width: event.width ?? 0,
|
|
163
167
|
height: event.height ?? 0,
|
|
164
168
|
});
|
|
@@ -224,13 +228,17 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
224
228
|
};
|
|
225
229
|
|
|
226
230
|
if (autoCapture) {
|
|
227
|
-
|
|
231
|
+
if (payload.stableCounter >= Math.max(minStableFrames - 1, 0)) {
|
|
228
232
|
setIsAutoCapturing(true);
|
|
229
233
|
} else if (payload.stableCounter === 0) {
|
|
230
234
|
setIsAutoCapturing(false);
|
|
231
235
|
}
|
|
232
236
|
}
|
|
233
237
|
|
|
238
|
+
if (payload.rectangleCoordinates) {
|
|
239
|
+
lastRectangleRef.current = payload.rectangleCoordinates;
|
|
240
|
+
}
|
|
241
|
+
|
|
234
242
|
const isGoodRectangle = payload.lastDetectionType === 0;
|
|
235
243
|
setDetectedRectangle(isGoodRectangle && rectangleOnScreen ? payload : null);
|
|
236
244
|
onRectangleDetect?.(payload);
|
|
@@ -252,7 +260,7 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
252
260
|
[capture],
|
|
253
261
|
);
|
|
254
262
|
|
|
255
|
-
const overlayPolygon = detectedRectangle?.rectangleOnScreen ?? null;
|
|
263
|
+
const overlayPolygon = detectedRectangle?.rectangleOnScreen ?? detectedRectangle?.rectangleCoordinates ?? null;
|
|
256
264
|
const overlayIsActive = autoCapture ? isAutoCapturing : (detectedRectangle?.stableCounter ?? 0) > 0;
|
|
257
265
|
|
|
258
266
|
return (
|
package/src/FullDocScanner.tsx
CHANGED
|
@@ -116,6 +116,42 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
116
116
|
);
|
|
117
117
|
}, [capturedDoc]);
|
|
118
118
|
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!capturedDoc || !imageSize || cropRectangle) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const baseWidth = capturedDoc.width > 0 ? capturedDoc.width : imageSize.width;
|
|
125
|
+
const baseHeight = capturedDoc.height > 0 ? capturedDoc.height : imageSize.height;
|
|
126
|
+
|
|
127
|
+
let initialRectangle: Rectangle | null = null;
|
|
128
|
+
|
|
129
|
+
if (capturedDoc.rectangle) {
|
|
130
|
+
initialRectangle = scaleRectangle(
|
|
131
|
+
capturedDoc.rectangle,
|
|
132
|
+
baseWidth,
|
|
133
|
+
baseHeight,
|
|
134
|
+
imageSize.width,
|
|
135
|
+
imageSize.height,
|
|
136
|
+
);
|
|
137
|
+
} else if (capturedDoc.quad && capturedDoc.quad.length === 4) {
|
|
138
|
+
const quadRectangle = quadToRectangle(capturedDoc.quad as Quad);
|
|
139
|
+
if (quadRectangle) {
|
|
140
|
+
initialRectangle = scaleRectangle(
|
|
141
|
+
quadRectangle,
|
|
142
|
+
baseWidth,
|
|
143
|
+
baseHeight,
|
|
144
|
+
imageSize.width,
|
|
145
|
+
imageSize.height,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (initialRectangle) {
|
|
151
|
+
setCropRectangle(initialRectangle);
|
|
152
|
+
}
|
|
153
|
+
}, [capturedDoc, imageSize, cropRectangle]);
|
|
154
|
+
|
|
119
155
|
const resetState = useCallback(() => {
|
|
120
156
|
setScreen('scanner');
|
|
121
157
|
setCapturedDoc(null);
|
|
@@ -128,6 +164,8 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
128
164
|
(document: CapturedDocument) => {
|
|
129
165
|
const normalizedPath = stripFileUri(document.path);
|
|
130
166
|
const nextQuad = document.quad && document.quad.length === 4 ? (document.quad as Quad) : null;
|
|
167
|
+
const quadRectangle = nextQuad ? quadToRectangle(nextQuad) : null;
|
|
168
|
+
const nextRectangle = document.rectangle ?? quadRectangle ?? null;
|
|
131
169
|
const normalizedInitial =
|
|
132
170
|
document.initialPath != null ? stripFileUri(document.initialPath) : normalizedPath;
|
|
133
171
|
const normalizedCropped =
|
|
@@ -139,8 +177,9 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
139
177
|
initialPath: normalizedInitial,
|
|
140
178
|
croppedPath: normalizedCropped,
|
|
141
179
|
quad: nextQuad,
|
|
180
|
+
rectangle: nextRectangle,
|
|
142
181
|
});
|
|
143
|
-
setCropRectangle(
|
|
182
|
+
setCropRectangle(null);
|
|
144
183
|
setScreen('crop');
|
|
145
184
|
},
|
|
146
185
|
[],
|
|
@@ -173,22 +212,35 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
173
212
|
throw new Error('CustomCropManager.crop is not available');
|
|
174
213
|
}
|
|
175
214
|
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
? quadToRectangle(capturedDoc.quad as Quad)
|
|
179
|
-
: null;
|
|
215
|
+
const baseWidth = capturedDoc.width > 0 ? capturedDoc.width : size.width;
|
|
216
|
+
const baseHeight = capturedDoc.height > 0 ? capturedDoc.height : size.height;
|
|
180
217
|
|
|
181
|
-
const
|
|
218
|
+
const rectangleFromDetection = capturedDoc.rectangle
|
|
182
219
|
? scaleRectangle(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
220
|
+
capturedDoc.rectangle,
|
|
221
|
+
baseWidth,
|
|
222
|
+
baseHeight,
|
|
186
223
|
size.width,
|
|
187
224
|
size.height,
|
|
188
225
|
)
|
|
189
226
|
: null;
|
|
190
227
|
|
|
191
|
-
|
|
228
|
+
let fallbackRectangle: Rectangle | null = rectangleFromDetection;
|
|
229
|
+
|
|
230
|
+
if (!fallbackRectangle && capturedDoc.quad && capturedDoc.quad.length === 4) {
|
|
231
|
+
const quadRectangle = quadToRectangle(capturedDoc.quad as Quad);
|
|
232
|
+
if (quadRectangle) {
|
|
233
|
+
fallbackRectangle = scaleRectangle(
|
|
234
|
+
quadRectangle,
|
|
235
|
+
baseWidth,
|
|
236
|
+
baseHeight,
|
|
237
|
+
size.width,
|
|
238
|
+
size.height,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const rectangle = cropRectangle ?? fallbackRectangle;
|
|
192
244
|
|
|
193
245
|
const base64 = await new Promise<string>((resolve, reject) => {
|
|
194
246
|
cropManager.crop(
|
package/src/types.ts
CHANGED
package/src/utils/overlay.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
3
|
import type { Rectangle } from '../types';
|
|
4
4
|
|
|
5
5
|
let SvgModule: typeof import('react-native-svg') | null = null;
|
|
@@ -11,20 +11,35 @@ try {
|
|
|
11
11
|
SvgModule = null;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const SCAN_DURATION_MS = 2200;
|
|
15
14
|
const GRID_STEPS = [1 / 3, 2 / 3];
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
};
|
|
16
|
+
const createPointsString = (polygon: Rectangle): string =>
|
|
17
|
+
[
|
|
18
|
+
`${polygon.topLeft.x},${polygon.topLeft.y}`,
|
|
19
|
+
`${polygon.topRight.x},${polygon.topRight.y}`,
|
|
20
|
+
`${polygon.bottomRight.x},${polygon.bottomRight.y}`,
|
|
21
|
+
`${polygon.bottomLeft.x},${polygon.bottomLeft.y}`,
|
|
22
|
+
].join(' ');
|
|
23
|
+
|
|
24
|
+
const interpolatePoint = (a: { x: number; y: number }, b: { x: number; y: number }, t: number) => ({
|
|
25
|
+
x: a.x + (b.x - a.x) * t,
|
|
26
|
+
y: a.y + (b.y - a.y) * t,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const createGridLines = (polygon: Rectangle) =>
|
|
30
|
+
GRID_STEPS.flatMap((step) => {
|
|
31
|
+
const horizontalStart = interpolatePoint(polygon.topLeft, polygon.bottomLeft, step);
|
|
32
|
+
const horizontalEnd = interpolatePoint(polygon.topRight, polygon.bottomRight, step);
|
|
33
|
+
const verticalStart = interpolatePoint(polygon.topLeft, polygon.topRight, step);
|
|
34
|
+
const verticalEnd = interpolatePoint(polygon.bottomLeft, polygon.bottomRight, step);
|
|
26
35
|
|
|
27
|
-
|
|
36
|
+
return [
|
|
37
|
+
{ x1: horizontalStart.x, y1: horizontalStart.y, x2: horizontalEnd.x, y2: horizontalEnd.y },
|
|
38
|
+
{ x1: verticalStart.x, y1: verticalStart.y, x2: verticalEnd.x, y2: verticalEnd.y },
|
|
39
|
+
];
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const getBounds = (polygon: Rectangle) => {
|
|
28
43
|
const minX = Math.min(
|
|
29
44
|
polygon.topLeft.x,
|
|
30
45
|
polygon.bottomLeft.x,
|
|
@@ -52,41 +67,12 @@ const calculateMetrics = (polygon: Rectangle): PolygonMetrics => {
|
|
|
52
67
|
|
|
53
68
|
return {
|
|
54
69
|
minX,
|
|
55
|
-
maxX,
|
|
56
70
|
minY,
|
|
57
|
-
maxY,
|
|
58
71
|
width: maxX - minX,
|
|
59
72
|
height: maxY - minY,
|
|
60
|
-
centerX: minX + (maxX - minX) / 2,
|
|
61
73
|
};
|
|
62
74
|
};
|
|
63
75
|
|
|
64
|
-
const createPointsString = (polygon: Rectangle): string =>
|
|
65
|
-
[
|
|
66
|
-
`${polygon.topLeft.x},${polygon.topLeft.y}`,
|
|
67
|
-
`${polygon.topRight.x},${polygon.topRight.y}`,
|
|
68
|
-
`${polygon.bottomRight.x},${polygon.bottomRight.y}`,
|
|
69
|
-
`${polygon.bottomLeft.x},${polygon.bottomLeft.y}`,
|
|
70
|
-
].join(' ');
|
|
71
|
-
|
|
72
|
-
const interpolatePoint = (a: { x: number; y: number }, b: { x: number; y: number }, t: number) => ({
|
|
73
|
-
x: a.x + (b.x - a.x) * t,
|
|
74
|
-
y: a.y + (b.y - a.y) * t,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const createGridLines = (polygon: Rectangle) =>
|
|
78
|
-
GRID_STEPS.flatMap((step) => {
|
|
79
|
-
const horizontalStart = interpolatePoint(polygon.topLeft, polygon.bottomLeft, step);
|
|
80
|
-
const horizontalEnd = interpolatePoint(polygon.topRight, polygon.bottomRight, step);
|
|
81
|
-
const verticalStart = interpolatePoint(polygon.topLeft, polygon.topRight, step);
|
|
82
|
-
const verticalEnd = interpolatePoint(polygon.bottomLeft, polygon.bottomRight, step);
|
|
83
|
-
|
|
84
|
-
return [
|
|
85
|
-
{ x1: horizontalStart.x, y1: horizontalStart.y, x2: horizontalEnd.x, y2: horizontalEnd.y },
|
|
86
|
-
{ x1: verticalStart.x, y1: verticalStart.y, x2: verticalEnd.x, y2: verticalEnd.y },
|
|
87
|
-
];
|
|
88
|
-
});
|
|
89
|
-
|
|
90
76
|
export interface ScannerOverlayProps {
|
|
91
77
|
active: boolean;
|
|
92
78
|
color?: string;
|
|
@@ -95,72 +81,21 @@ export interface ScannerOverlayProps {
|
|
|
95
81
|
}
|
|
96
82
|
|
|
97
83
|
export const ScannerOverlay: React.FC<ScannerOverlayProps> = ({
|
|
98
|
-
active,
|
|
84
|
+
active: _active, // kept for compatibility; no animation currently
|
|
99
85
|
color = '#0b7ef4',
|
|
100
86
|
lineWidth = StyleSheet.hairlineWidth,
|
|
101
87
|
polygon,
|
|
102
88
|
}) => {
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
const metrics = useMemo(() => (polygon ? calculateMetrics(polygon) : null), [polygon]);
|
|
107
|
-
|
|
108
|
-
const scanBarHeight = useMemo(() => {
|
|
109
|
-
if (!metrics) return 0;
|
|
110
|
-
return Math.max(metrics.height * 0.2, 16);
|
|
111
|
-
}, [metrics]);
|
|
112
|
-
|
|
113
|
-
const scanTranslate = useMemo(() => {
|
|
114
|
-
if (!metrics || scanBarHeight === 0) {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return scanProgress.interpolate({
|
|
119
|
-
inputRange: [0, 1],
|
|
120
|
-
outputRange: [metrics.minY, Math.max(metrics.minY, metrics.maxY - scanBarHeight)],
|
|
121
|
-
});
|
|
122
|
-
}, [metrics, scanBarHeight, scanProgress]);
|
|
123
|
-
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
if (!active || !metrics || metrics.height <= 1) {
|
|
126
|
-
scanProgress.stopAnimation();
|
|
127
|
-
scanProgress.setValue(0);
|
|
128
|
-
return undefined;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const loop = Animated.loop(
|
|
132
|
-
Animated.sequence([
|
|
133
|
-
Animated.timing(scanProgress, {
|
|
134
|
-
toValue: 1,
|
|
135
|
-
duration: SCAN_DURATION_MS,
|
|
136
|
-
easing: Easing.inOut(Easing.quad),
|
|
137
|
-
useNativeDriver: false,
|
|
138
|
-
}),
|
|
139
|
-
Animated.timing(scanProgress, {
|
|
140
|
-
toValue: 0,
|
|
141
|
-
duration: SCAN_DURATION_MS,
|
|
142
|
-
easing: Easing.inOut(Easing.quad),
|
|
143
|
-
useNativeDriver: false,
|
|
144
|
-
}),
|
|
145
|
-
]),
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
loop.start();
|
|
149
|
-
return () => {
|
|
150
|
-
loop.stop();
|
|
151
|
-
};
|
|
152
|
-
}, [active, metrics, scanProgress]);
|
|
89
|
+
const points = useMemo(() => (polygon ? createPointsString(polygon) : null), [polygon]);
|
|
90
|
+
const gridLines = useMemo(() => (polygon ? createGridLines(polygon) : []), [polygon]);
|
|
91
|
+
const bounds = useMemo(() => (polygon ? getBounds(polygon) : null), [polygon]);
|
|
153
92
|
|
|
154
|
-
if (!polygon || !
|
|
93
|
+
if (!polygon || !points || !bounds) {
|
|
155
94
|
return null;
|
|
156
95
|
}
|
|
157
96
|
|
|
158
97
|
if (SvgModule) {
|
|
159
|
-
const { default: Svg, Polygon, Line
|
|
160
|
-
const AnimatedRect = Animated.createAnimatedComponent(Rect);
|
|
161
|
-
|
|
162
|
-
const gridLines = createGridLines(polygon);
|
|
163
|
-
const points = createPointsString(polygon);
|
|
98
|
+
const { default: Svg, Polygon, Line } = SvgModule;
|
|
164
99
|
|
|
165
100
|
return (
|
|
166
101
|
<View pointerEvents="none" style={StyleSheet.absoluteFill}>
|
|
@@ -179,59 +114,26 @@ export const ScannerOverlay: React.FC<ScannerOverlayProps> = ({
|
|
|
179
114
|
/>
|
|
180
115
|
))}
|
|
181
116
|
<Polygon points={points} stroke={color} strokeWidth={lineWidth} fill="none" />
|
|
182
|
-
<Defs>
|
|
183
|
-
<LinearGradient id="scanGradient" x1="0" y1="0" x2="0" y2="1">
|
|
184
|
-
<Stop offset="0%" stopColor="rgba(255,255,255,0)" />
|
|
185
|
-
<Stop offset="50%" stopColor={color} stopOpacity={0.8} />
|
|
186
|
-
<Stop offset="100%" stopColor="rgba(255,255,255,0)" />
|
|
187
|
-
</LinearGradient>
|
|
188
|
-
</Defs>
|
|
189
|
-
{active && scanTranslate && (
|
|
190
|
-
<AnimatedRect
|
|
191
|
-
x={metrics.minX}
|
|
192
|
-
width={metrics.width}
|
|
193
|
-
height={scanBarHeight}
|
|
194
|
-
fill="url(#scanGradient)"
|
|
195
|
-
y={scanTranslate}
|
|
196
|
-
/>
|
|
197
|
-
)}
|
|
198
117
|
</Svg>
|
|
199
118
|
</View>
|
|
200
119
|
);
|
|
201
120
|
}
|
|
202
121
|
|
|
203
|
-
// Fallback rendering without react-native-svg
|
|
204
|
-
const relativeTranslate =
|
|
205
|
-
scanTranslate != null ? Animated.subtract(scanTranslate, metrics.minY) : fallbackBase;
|
|
206
|
-
|
|
207
122
|
return (
|
|
208
123
|
<View pointerEvents="none" style={StyleSheet.absoluteFill}>
|
|
209
124
|
<View
|
|
210
125
|
style={[
|
|
211
126
|
styles.fallbackBox,
|
|
212
127
|
{
|
|
213
|
-
left:
|
|
214
|
-
top:
|
|
215
|
-
width:
|
|
216
|
-
height:
|
|
128
|
+
left: bounds.minX,
|
|
129
|
+
top: bounds.minY,
|
|
130
|
+
width: bounds.width,
|
|
131
|
+
height: bounds.height,
|
|
217
132
|
borderColor: color,
|
|
218
133
|
borderWidth: lineWidth,
|
|
219
134
|
},
|
|
220
135
|
]}
|
|
221
|
-
|
|
222
|
-
{active && (
|
|
223
|
-
<Animated.View
|
|
224
|
-
style={[
|
|
225
|
-
styles.fallbackScanBar,
|
|
226
|
-
{
|
|
227
|
-
backgroundColor: color,
|
|
228
|
-
height: scanBarHeight,
|
|
229
|
-
transform: [{ translateY: relativeTranslate }],
|
|
230
|
-
},
|
|
231
|
-
]}
|
|
232
|
-
/>
|
|
233
|
-
)}
|
|
234
|
-
</View>
|
|
136
|
+
/>
|
|
235
137
|
</View>
|
|
236
138
|
);
|
|
237
139
|
};
|
|
@@ -240,10 +142,5 @@ const styles = StyleSheet.create({
|
|
|
240
142
|
fallbackBox: {
|
|
241
143
|
position: 'absolute',
|
|
242
144
|
backgroundColor: 'rgba(11, 126, 244, 0.1)',
|
|
243
|
-
overflow: 'hidden',
|
|
244
|
-
},
|
|
245
|
-
fallbackScanBar: {
|
|
246
|
-
width: '100%',
|
|
247
|
-
opacity: 0.4,
|
|
248
145
|
},
|
|
249
146
|
});
|