react-native-rectangle-doc-scanner 3.15.0 → 3.18.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/DocScanner.d.ts +5 -0
- package/dist/DocScanner.js +22 -2
- package/dist/utils/overlay.d.ts +6 -13
- package/dist/utils/overlay.js +92 -136
- package/package.json +1 -1
- package/src/DocScanner.tsx +46 -0
- package/src/external.d.ts +9 -0
- package/src/utils/overlay.tsx +143 -0
- package/vendor/react-native-document-scanner/ios/IPDFCameraViewController.m +9 -3
package/dist/DocScanner.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ type PictureEvent = {
|
|
|
5
5
|
width?: number;
|
|
6
6
|
height?: number;
|
|
7
7
|
};
|
|
8
|
+
export type RectangleDetectEvent = {
|
|
9
|
+
stableCounter: number;
|
|
10
|
+
lastDetectionType: number;
|
|
11
|
+
};
|
|
8
12
|
export interface DetectionConfig {
|
|
9
13
|
processingWidth?: number;
|
|
10
14
|
cannyLowThreshold?: number;
|
|
@@ -31,6 +35,7 @@ interface Props {
|
|
|
31
35
|
gridColor?: string;
|
|
32
36
|
gridLineWidth?: number;
|
|
33
37
|
detectionConfig?: DetectionConfig;
|
|
38
|
+
onRectangleDetect?: (event: RectangleDetectEvent) => void;
|
|
34
39
|
}
|
|
35
40
|
type DocScannerHandle = {
|
|
36
41
|
capture: () => Promise<PictureEvent>;
|
package/dist/DocScanner.js
CHANGED
|
@@ -40,10 +40,17 @@ exports.DocScanner = void 0;
|
|
|
40
40
|
const react_1 = __importStar(require("react"));
|
|
41
41
|
const react_native_1 = require("react-native");
|
|
42
42
|
const react_native_document_scanner_1 = __importDefault(require("react-native-document-scanner"));
|
|
43
|
+
const overlay_1 = require("./utils/overlay");
|
|
43
44
|
const DEFAULT_OVERLAY_COLOR = '#0b7ef4';
|
|
44
|
-
exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAULT_OVERLAY_COLOR, autoCapture = true, minStableFrames = 8, enableTorch = false, quality = 90, useBase64 = false, children, showGrid = true, }, ref) => {
|
|
45
|
+
exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAULT_OVERLAY_COLOR, autoCapture = true, minStableFrames = 8, enableTorch = false, quality = 90, useBase64 = false, children, showGrid = true, gridColor, gridLineWidth, detectionConfig, onRectangleDetect, }, ref) => {
|
|
45
46
|
const scannerRef = (0, react_1.useRef)(null);
|
|
46
47
|
const captureResolvers = (0, react_1.useRef)(null);
|
|
48
|
+
const [isAutoCapturing, setIsAutoCapturing] = (0, react_1.useState)(false);
|
|
49
|
+
(0, react_1.useEffect)(() => {
|
|
50
|
+
if (!autoCapture) {
|
|
51
|
+
setIsAutoCapturing(false);
|
|
52
|
+
}
|
|
53
|
+
}, [autoCapture]);
|
|
47
54
|
const normalizedQuality = (0, react_1.useMemo)(() => {
|
|
48
55
|
if (react_native_1.Platform.OS === 'ios') {
|
|
49
56
|
// iOS expects 0-1
|
|
@@ -52,6 +59,7 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
52
59
|
return Math.min(100, Math.max(0, quality));
|
|
53
60
|
}, [quality]);
|
|
54
61
|
const handlePictureTaken = (0, react_1.useCallback)((event) => {
|
|
62
|
+
setIsAutoCapturing(false);
|
|
55
63
|
const path = event.croppedImage ?? event.initialImage;
|
|
56
64
|
if (path) {
|
|
57
65
|
onCapture?.({
|
|
@@ -99,6 +107,17 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
99
107
|
console.warn('[DocScanner] manual capture failed', error);
|
|
100
108
|
});
|
|
101
109
|
}, [autoCapture, capture]);
|
|
110
|
+
const handleRectangleDetect = (0, react_1.useCallback)((event) => {
|
|
111
|
+
if (autoCapture) {
|
|
112
|
+
if (event.stableCounter >= Math.max(minStableFrames - 1, 0)) {
|
|
113
|
+
setIsAutoCapturing(true);
|
|
114
|
+
}
|
|
115
|
+
else if (event.stableCounter === 0) {
|
|
116
|
+
setIsAutoCapturing(false);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
onRectangleDetect?.(event);
|
|
120
|
+
}, [autoCapture, minStableFrames, onRectangleDetect]);
|
|
102
121
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
103
122
|
capture,
|
|
104
123
|
reset: () => {
|
|
@@ -109,7 +128,8 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
109
128
|
},
|
|
110
129
|
}), [capture]);
|
|
111
130
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
112
|
-
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, onPictureTaken: handlePictureTaken, onError: handleError }),
|
|
131
|
+
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 }),
|
|
132
|
+
showGrid && (react_1.default.createElement(overlay_1.ScannerOverlay, { active: autoCapture && isAutoCapturing, color: gridColor ?? overlayColor, lineWidth: gridLineWidth })),
|
|
113
133
|
!autoCapture && react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: handleManualCapture }),
|
|
114
134
|
children));
|
|
115
135
|
});
|
package/dist/utils/overlay.d.ts
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
export interface ScannerOverlayProps {
|
|
3
|
+
/** 활성화 시 스캔 바가 움직이며 자동 촬영 중임을 표시합니다. */
|
|
4
|
+
active: boolean;
|
|
5
5
|
color?: string;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} | null;
|
|
10
|
-
showGrid?: boolean;
|
|
11
|
-
gridColor?: string;
|
|
12
|
-
gridLineWidth?: number;
|
|
13
|
-
};
|
|
14
|
-
export declare const Overlay: React.FC<OverlayProps>;
|
|
15
|
-
export {};
|
|
6
|
+
lineWidth?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const ScannerOverlay: React.FC<ScannerOverlayProps>;
|
package/dist/utils/overlay.js
CHANGED
|
@@ -33,150 +33,106 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
36
|
+
exports.ScannerOverlay = void 0;
|
|
37
37
|
const react_1 = __importStar(require("react"));
|
|
38
38
|
const react_native_1 = require("react-native");
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const g = parseInt(normalize.slice(2, 4), 16);
|
|
55
|
-
const b = parseInt(normalize.slice(4, 6), 16);
|
|
56
|
-
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
57
|
-
};
|
|
58
|
-
const buildPath = (points) => {
|
|
59
|
-
const path = react_native_skia_1.Skia.Path.Make();
|
|
60
|
-
path.moveTo(points[0].x, points[0].y);
|
|
61
|
-
points.slice(1).forEach((p) => path.lineTo(p.x, p.y));
|
|
62
|
-
path.close();
|
|
63
|
-
return path;
|
|
64
|
-
};
|
|
65
|
-
const orderQuad = (points) => {
|
|
66
|
-
if (points.length !== 4) {
|
|
67
|
-
return points;
|
|
68
|
-
}
|
|
69
|
-
const center = points.reduce((acc, point) => ({ x: acc.x + point.x / 4, y: acc.y + point.y / 4 }), { x: 0, y: 0 });
|
|
70
|
-
const sorted = [...points].sort((a, b) => {
|
|
71
|
-
const angleA = Math.atan2(a.y - center.y, a.x - center.x);
|
|
72
|
-
const angleB = Math.atan2(b.y - center.y, b.x - center.x);
|
|
73
|
-
return angleA - angleB;
|
|
74
|
-
});
|
|
75
|
-
// Ensure the first point is the top-left (smallest y, then smallest x)
|
|
76
|
-
let startIndex = 0;
|
|
77
|
-
for (let i = 1; i < sorted.length; i += 1) {
|
|
78
|
-
const current = sorted[i];
|
|
79
|
-
const candidate = sorted[startIndex];
|
|
80
|
-
if (current.y < candidate.y || (current.y === candidate.y && current.x < candidate.x)) {
|
|
81
|
-
startIndex = i;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return [
|
|
85
|
-
sorted[startIndex % 4],
|
|
86
|
-
sorted[(startIndex + 1) % 4],
|
|
87
|
-
sorted[(startIndex + 2) % 4],
|
|
88
|
-
sorted[(startIndex + 3) % 4],
|
|
89
|
-
];
|
|
90
|
-
};
|
|
91
|
-
const Overlay = ({ quad, color = '#e7a649', frameSize, showGrid = true, gridColor = 'rgba(231, 166, 73, 0.35)', gridLineWidth = 2, }) => {
|
|
92
|
-
const { width: screenWidth, height: screenHeight } = (0, react_native_1.useWindowDimensions)();
|
|
93
|
-
const fillColor = (0, react_1.useMemo)(() => withAlpha(color, 0.2), [color]);
|
|
94
|
-
const { outlinePath, gridPaths } = (0, react_1.useMemo)(() => {
|
|
95
|
-
let transformedQuad = null;
|
|
96
|
-
let sourceQuad = null;
|
|
97
|
-
let sourceFrameSize = frameSize;
|
|
98
|
-
if (quad && frameSize) {
|
|
99
|
-
sourceQuad = quad;
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
// No detection yet – skip drawing
|
|
103
|
-
return { outlinePath: null, gridPaths: [] };
|
|
104
|
-
}
|
|
105
|
-
if (sourceQuad && sourceFrameSize) {
|
|
106
|
-
if (__DEV__) {
|
|
107
|
-
console.log('[Overlay] drawing quad:', sourceQuad);
|
|
108
|
-
console.log('[Overlay] color:', color);
|
|
109
|
-
console.log('[Overlay] screen dimensions:', screenWidth, 'x', screenHeight);
|
|
110
|
-
console.log('[Overlay] frame dimensions:', sourceFrameSize.width, 'x', sourceFrameSize.height);
|
|
111
|
-
}
|
|
112
|
-
const isFrameLandscape = sourceFrameSize.width > sourceFrameSize.height;
|
|
113
|
-
const isScreenPortrait = screenHeight > screenWidth;
|
|
114
|
-
const needsRotation = isFrameLandscape && isScreenPortrait;
|
|
115
|
-
if (needsRotation) {
|
|
116
|
-
const scaleX = screenWidth / sourceFrameSize.height;
|
|
117
|
-
const scaleY = screenHeight / sourceFrameSize.width;
|
|
118
|
-
transformedQuad = sourceQuad.map((p) => ({
|
|
119
|
-
x: p.y * scaleX,
|
|
120
|
-
y: (sourceFrameSize.width - p.x) * scaleY,
|
|
121
|
-
}));
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
const scaleX = screenWidth / sourceFrameSize.width;
|
|
125
|
-
const scaleY = screenHeight / sourceFrameSize.height;
|
|
126
|
-
transformedQuad = sourceQuad.map((p) => ({
|
|
127
|
-
x: p.x * scaleX,
|
|
128
|
-
y: p.y * scaleY,
|
|
129
|
-
}));
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (!transformedQuad) {
|
|
133
|
-
return { outlinePath: null, gridPaths: [] };
|
|
134
|
-
}
|
|
135
|
-
const normalizedQuad = orderQuad(transformedQuad);
|
|
136
|
-
const skPath = buildPath(normalizedQuad);
|
|
137
|
-
const grid = [];
|
|
138
|
-
if (showGrid) {
|
|
139
|
-
const [topLeft, topRight, bottomRight, bottomLeft] = normalizedQuad;
|
|
140
|
-
const steps = [1 / 3, 2 / 3];
|
|
141
|
-
steps.forEach((t) => {
|
|
142
|
-
const start = lerp(topLeft, topRight, t);
|
|
143
|
-
const end = lerp(bottomLeft, bottomRight, t);
|
|
144
|
-
const verticalPath = react_native_skia_1.Skia.Path.Make();
|
|
145
|
-
verticalPath.moveTo(start.x, start.y);
|
|
146
|
-
verticalPath.lineTo(end.x, end.y);
|
|
147
|
-
grid.push(verticalPath);
|
|
148
|
-
});
|
|
149
|
-
steps.forEach((t) => {
|
|
150
|
-
const start = lerp(topLeft, bottomLeft, t);
|
|
151
|
-
const end = lerp(topRight, bottomRight, t);
|
|
152
|
-
const horizontalPath = react_native_skia_1.Skia.Path.Make();
|
|
153
|
-
horizontalPath.moveTo(start.x, start.y);
|
|
154
|
-
horizontalPath.lineTo(end.x, end.y);
|
|
155
|
-
grid.push(horizontalPath);
|
|
156
|
-
});
|
|
39
|
+
const BAR_THICKNESS = 4;
|
|
40
|
+
const ScannerOverlay = ({ active, color = 'rgba(255,255,255,0.8)', lineWidth = react_native_1.StyleSheet.hairlineWidth, }) => {
|
|
41
|
+
const animatedValue = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
|
|
42
|
+
const loopRef = (0, react_1.useRef)(null);
|
|
43
|
+
const [frameHeight, setFrameHeight] = (0, react_1.useState)(0);
|
|
44
|
+
const borderStyle = (0, react_1.useMemo)(() => ({
|
|
45
|
+
borderColor: color,
|
|
46
|
+
borderWidth: lineWidth,
|
|
47
|
+
}), [color, lineWidth]);
|
|
48
|
+
(0, react_1.useEffect)(() => {
|
|
49
|
+
loopRef.current?.stop();
|
|
50
|
+
if (!active || frameHeight <= 0) {
|
|
51
|
+
animatedValue.stopAnimation();
|
|
52
|
+
animatedValue.setValue(0);
|
|
53
|
+
return;
|
|
157
54
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
55
|
+
const loop = react_native_1.Animated.loop(react_native_1.Animated.sequence([
|
|
56
|
+
react_native_1.Animated.timing(animatedValue, {
|
|
57
|
+
toValue: 1,
|
|
58
|
+
duration: 1600,
|
|
59
|
+
easing: react_native_1.Easing.inOut(react_native_1.Easing.quad),
|
|
60
|
+
useNativeDriver: true,
|
|
61
|
+
}),
|
|
62
|
+
react_native_1.Animated.timing(animatedValue, {
|
|
63
|
+
toValue: 0,
|
|
64
|
+
duration: 1600,
|
|
65
|
+
easing: react_native_1.Easing.inOut(react_native_1.Easing.quad),
|
|
66
|
+
useNativeDriver: true,
|
|
67
|
+
}),
|
|
68
|
+
]));
|
|
69
|
+
loopRef.current = loop;
|
|
70
|
+
loop.start();
|
|
71
|
+
return () => {
|
|
72
|
+
loop.stop();
|
|
73
|
+
};
|
|
74
|
+
}, [active, animatedValue, frameHeight]);
|
|
75
|
+
const handleLayout = (event) => {
|
|
76
|
+
const { height } = event.nativeEvent.layout;
|
|
77
|
+
setFrameHeight(height);
|
|
78
|
+
};
|
|
79
|
+
const translateY = frameHeight <= BAR_THICKNESS
|
|
80
|
+
? 0
|
|
81
|
+
: animatedValue.interpolate({
|
|
82
|
+
inputRange: [0, 1],
|
|
83
|
+
outputRange: [0, frameHeight - BAR_THICKNESS],
|
|
84
|
+
});
|
|
85
|
+
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: react_native_1.StyleSheet.absoluteFill },
|
|
86
|
+
react_1.default.createElement(react_native_1.View, { onLayout: handleLayout, style: [styles.frame, borderStyle] },
|
|
87
|
+
react_1.default.createElement(react_native_1.View, { style: [
|
|
88
|
+
styles.horizontalLine,
|
|
89
|
+
{ top: '33%', borderBottomColor: color, borderBottomWidth: lineWidth },
|
|
90
|
+
] }),
|
|
91
|
+
react_1.default.createElement(react_native_1.View, { style: [
|
|
92
|
+
styles.horizontalLine,
|
|
93
|
+
{ top: '66%', borderBottomColor: color, borderBottomWidth: lineWidth },
|
|
94
|
+
] }),
|
|
95
|
+
react_1.default.createElement(react_native_1.View, { style: [
|
|
96
|
+
styles.verticalLine,
|
|
97
|
+
{ left: '33%', borderRightColor: color, borderRightWidth: lineWidth },
|
|
98
|
+
] }),
|
|
99
|
+
react_1.default.createElement(react_native_1.View, { style: [
|
|
100
|
+
styles.verticalLine,
|
|
101
|
+
{ left: '66%', borderRightColor: color, borderRightWidth: lineWidth },
|
|
102
|
+
] }),
|
|
103
|
+
react_1.default.createElement(react_native_1.Animated.View, { style: [
|
|
104
|
+
styles.scanBar,
|
|
105
|
+
{
|
|
106
|
+
opacity: active ? 1 : 0,
|
|
107
|
+
backgroundColor: color,
|
|
108
|
+
transform: [{ translateY }],
|
|
109
|
+
},
|
|
110
|
+
] }))));
|
|
172
111
|
};
|
|
173
|
-
exports.
|
|
112
|
+
exports.ScannerOverlay = ScannerOverlay;
|
|
174
113
|
const styles = react_native_1.StyleSheet.create({
|
|
175
|
-
|
|
114
|
+
frame: {
|
|
115
|
+
flex: 1,
|
|
116
|
+
borderRadius: 12,
|
|
117
|
+
overflow: 'hidden',
|
|
118
|
+
backgroundColor: 'transparent',
|
|
119
|
+
},
|
|
120
|
+
horizontalLine: {
|
|
121
|
+
position: 'absolute',
|
|
122
|
+
right: 0,
|
|
123
|
+
left: 0,
|
|
124
|
+
height: 0,
|
|
125
|
+
},
|
|
126
|
+
verticalLine: {
|
|
176
127
|
position: 'absolute',
|
|
177
128
|
top: 0,
|
|
129
|
+
bottom: 0,
|
|
130
|
+
width: 0,
|
|
131
|
+
},
|
|
132
|
+
scanBar: {
|
|
133
|
+
position: 'absolute',
|
|
178
134
|
left: 0,
|
|
179
135
|
right: 0,
|
|
180
|
-
|
|
136
|
+
height: BAR_THICKNESS,
|
|
181
137
|
},
|
|
182
138
|
});
|
package/package.json
CHANGED
package/src/DocScanner.tsx
CHANGED
|
@@ -2,12 +2,15 @@ import React, {
|
|
|
2
2
|
ReactNode,
|
|
3
3
|
forwardRef,
|
|
4
4
|
useCallback,
|
|
5
|
+
useEffect,
|
|
5
6
|
useImperativeHandle,
|
|
6
7
|
useMemo,
|
|
7
8
|
useRef,
|
|
9
|
+
useState,
|
|
8
10
|
} from 'react';
|
|
9
11
|
import { Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
|
|
10
12
|
import DocumentScanner from 'react-native-document-scanner';
|
|
13
|
+
import { ScannerOverlay } from './utils/overlay';
|
|
11
14
|
|
|
12
15
|
type PictureEvent = {
|
|
13
16
|
croppedImage?: string | null;
|
|
@@ -16,6 +19,11 @@ type PictureEvent = {
|
|
|
16
19
|
height?: number;
|
|
17
20
|
};
|
|
18
21
|
|
|
22
|
+
export type RectangleDetectEvent = {
|
|
23
|
+
stableCounter: number;
|
|
24
|
+
lastDetectionType: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
19
27
|
export interface DetectionConfig {
|
|
20
28
|
processingWidth?: number;
|
|
21
29
|
cannyLowThreshold?: number;
|
|
@@ -38,6 +46,7 @@ interface Props {
|
|
|
38
46
|
gridColor?: string;
|
|
39
47
|
gridLineWidth?: number;
|
|
40
48
|
detectionConfig?: DetectionConfig;
|
|
49
|
+
onRectangleDetect?: (event: RectangleDetectEvent) => void;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
type DocScannerHandle = {
|
|
@@ -59,6 +68,10 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
59
68
|
useBase64 = false,
|
|
60
69
|
children,
|
|
61
70
|
showGrid = true,
|
|
71
|
+
gridColor,
|
|
72
|
+
gridLineWidth,
|
|
73
|
+
detectionConfig,
|
|
74
|
+
onRectangleDetect,
|
|
62
75
|
},
|
|
63
76
|
ref,
|
|
64
77
|
) => {
|
|
@@ -67,6 +80,13 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
67
80
|
resolve: (value: PictureEvent) => void;
|
|
68
81
|
reject: (reason?: unknown) => void;
|
|
69
82
|
} | null>(null);
|
|
83
|
+
const [isAutoCapturing, setIsAutoCapturing] = useState(false);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!autoCapture) {
|
|
87
|
+
setIsAutoCapturing(false);
|
|
88
|
+
}
|
|
89
|
+
}, [autoCapture]);
|
|
70
90
|
|
|
71
91
|
const normalizedQuality = useMemo(() => {
|
|
72
92
|
if (Platform.OS === 'ios') {
|
|
@@ -78,6 +98,8 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
78
98
|
|
|
79
99
|
const handlePictureTaken = useCallback(
|
|
80
100
|
(event: PictureEvent) => {
|
|
101
|
+
setIsAutoCapturing(false);
|
|
102
|
+
|
|
81
103
|
const path = event.croppedImage ?? event.initialImage;
|
|
82
104
|
if (path) {
|
|
83
105
|
onCapture?.({
|
|
@@ -134,6 +156,21 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
134
156
|
});
|
|
135
157
|
}, [autoCapture, capture]);
|
|
136
158
|
|
|
159
|
+
const handleRectangleDetect = useCallback(
|
|
160
|
+
(event: RectangleDetectEvent) => {
|
|
161
|
+
if (autoCapture) {
|
|
162
|
+
if (event.stableCounter >= Math.max(minStableFrames - 1, 0)) {
|
|
163
|
+
setIsAutoCapturing(true);
|
|
164
|
+
} else if (event.stableCounter === 0) {
|
|
165
|
+
setIsAutoCapturing(false);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
onRectangleDetect?.(event);
|
|
170
|
+
},
|
|
171
|
+
[autoCapture, minStableFrames, onRectangleDetect],
|
|
172
|
+
);
|
|
173
|
+
|
|
137
174
|
useImperativeHandle(
|
|
138
175
|
ref,
|
|
139
176
|
() => ({
|
|
@@ -159,9 +196,18 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
159
196
|
quality={normalizedQuality}
|
|
160
197
|
useBase64={useBase64}
|
|
161
198
|
manualOnly={!autoCapture}
|
|
199
|
+
detectionConfig={detectionConfig}
|
|
162
200
|
onPictureTaken={handlePictureTaken}
|
|
163
201
|
onError={handleError}
|
|
202
|
+
onRectangleDetect={handleRectangleDetect}
|
|
164
203
|
/>
|
|
204
|
+
{showGrid && (
|
|
205
|
+
<ScannerOverlay
|
|
206
|
+
active={autoCapture && isAutoCapturing}
|
|
207
|
+
color={gridColor ?? overlayColor}
|
|
208
|
+
lineWidth={gridLineWidth}
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
165
211
|
{!autoCapture && <TouchableOpacity style={styles.button} onPress={handleManualCapture} />}
|
|
166
212
|
{children}
|
|
167
213
|
</View>
|
package/src/external.d.ts
CHANGED
|
@@ -77,8 +77,17 @@ declare module 'react-native-document-scanner' {
|
|
|
77
77
|
useBase64?: boolean;
|
|
78
78
|
quality?: number;
|
|
79
79
|
manualOnly?: boolean;
|
|
80
|
+
detectionConfig?: {
|
|
81
|
+
processingWidth?: number;
|
|
82
|
+
cannyLowThreshold?: number;
|
|
83
|
+
cannyHighThreshold?: number;
|
|
84
|
+
snapDistance?: number;
|
|
85
|
+
maxAnchorMisses?: number;
|
|
86
|
+
maxCenterDelta?: number;
|
|
87
|
+
};
|
|
80
88
|
onPictureTaken?: (event: DocumentScannerResult) => void;
|
|
81
89
|
onError?: (error: Error) => void;
|
|
90
|
+
onRectangleDetect?: (event: { stableCounter: number; lastDetectionType: number }) => void;
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
export default class DocumentScanner extends Component<DocumentScannerProps> {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { Animated, Easing, LayoutChangeEvent, StyleSheet, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface ScannerOverlayProps {
|
|
5
|
+
/** 활성화 시 스캔 바가 움직이며 자동 촬영 중임을 표시합니다. */
|
|
6
|
+
active: boolean;
|
|
7
|
+
color?: string;
|
|
8
|
+
lineWidth?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const BAR_THICKNESS = 4;
|
|
12
|
+
|
|
13
|
+
export const ScannerOverlay: React.FC<ScannerOverlayProps> = ({
|
|
14
|
+
active,
|
|
15
|
+
color = 'rgba(255,255,255,0.8)',
|
|
16
|
+
lineWidth = StyleSheet.hairlineWidth,
|
|
17
|
+
}) => {
|
|
18
|
+
const animatedValue = useRef(new Animated.Value(0)).current;
|
|
19
|
+
const loopRef = useRef<Animated.CompositeAnimation | null>(null);
|
|
20
|
+
const [frameHeight, setFrameHeight] = useState(0);
|
|
21
|
+
|
|
22
|
+
const borderStyle = useMemo(
|
|
23
|
+
() => ({
|
|
24
|
+
borderColor: color,
|
|
25
|
+
borderWidth: lineWidth,
|
|
26
|
+
}),
|
|
27
|
+
[color, lineWidth],
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
loopRef.current?.stop();
|
|
32
|
+
if (!active || frameHeight <= 0) {
|
|
33
|
+
animatedValue.stopAnimation();
|
|
34
|
+
animatedValue.setValue(0);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const loop = Animated.loop(
|
|
39
|
+
Animated.sequence([
|
|
40
|
+
Animated.timing(animatedValue, {
|
|
41
|
+
toValue: 1,
|
|
42
|
+
duration: 1600,
|
|
43
|
+
easing: Easing.inOut(Easing.quad),
|
|
44
|
+
useNativeDriver: true,
|
|
45
|
+
}),
|
|
46
|
+
Animated.timing(animatedValue, {
|
|
47
|
+
toValue: 0,
|
|
48
|
+
duration: 1600,
|
|
49
|
+
easing: Easing.inOut(Easing.quad),
|
|
50
|
+
useNativeDriver: true,
|
|
51
|
+
}),
|
|
52
|
+
]),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
loopRef.current = loop;
|
|
56
|
+
loop.start();
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
loop.stop();
|
|
60
|
+
};
|
|
61
|
+
}, [active, animatedValue, frameHeight]);
|
|
62
|
+
|
|
63
|
+
const handleLayout = (event: LayoutChangeEvent) => {
|
|
64
|
+
const { height } = event.nativeEvent.layout;
|
|
65
|
+
setFrameHeight(height);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const translateY =
|
|
69
|
+
frameHeight <= BAR_THICKNESS
|
|
70
|
+
? 0
|
|
71
|
+
: animatedValue.interpolate({
|
|
72
|
+
inputRange: [0, 1],
|
|
73
|
+
outputRange: [0, frameHeight - BAR_THICKNESS],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<View pointerEvents="none" style={StyleSheet.absoluteFill}>
|
|
78
|
+
<View onLayout={handleLayout} style={[styles.frame, borderStyle]}>
|
|
79
|
+
<View
|
|
80
|
+
style={[
|
|
81
|
+
styles.horizontalLine,
|
|
82
|
+
{ top: '33%', borderBottomColor: color, borderBottomWidth: lineWidth },
|
|
83
|
+
]}
|
|
84
|
+
/>
|
|
85
|
+
<View
|
|
86
|
+
style={[
|
|
87
|
+
styles.horizontalLine,
|
|
88
|
+
{ top: '66%', borderBottomColor: color, borderBottomWidth: lineWidth },
|
|
89
|
+
]}
|
|
90
|
+
/>
|
|
91
|
+
<View
|
|
92
|
+
style={[
|
|
93
|
+
styles.verticalLine,
|
|
94
|
+
{ left: '33%', borderRightColor: color, borderRightWidth: lineWidth },
|
|
95
|
+
]}
|
|
96
|
+
/>
|
|
97
|
+
<View
|
|
98
|
+
style={[
|
|
99
|
+
styles.verticalLine,
|
|
100
|
+
{ left: '66%', borderRightColor: color, borderRightWidth: lineWidth },
|
|
101
|
+
]}
|
|
102
|
+
/>
|
|
103
|
+
<Animated.View
|
|
104
|
+
style={[
|
|
105
|
+
styles.scanBar,
|
|
106
|
+
{
|
|
107
|
+
opacity: active ? 1 : 0,
|
|
108
|
+
backgroundColor: color,
|
|
109
|
+
transform: [{ translateY }],
|
|
110
|
+
},
|
|
111
|
+
]}
|
|
112
|
+
/>
|
|
113
|
+
</View>
|
|
114
|
+
</View>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const styles = StyleSheet.create({
|
|
119
|
+
frame: {
|
|
120
|
+
flex: 1,
|
|
121
|
+
borderRadius: 12,
|
|
122
|
+
overflow: 'hidden',
|
|
123
|
+
backgroundColor: 'transparent',
|
|
124
|
+
},
|
|
125
|
+
horizontalLine: {
|
|
126
|
+
position: 'absolute',
|
|
127
|
+
right: 0,
|
|
128
|
+
left: 0,
|
|
129
|
+
height: 0,
|
|
130
|
+
},
|
|
131
|
+
verticalLine: {
|
|
132
|
+
position: 'absolute',
|
|
133
|
+
top: 0,
|
|
134
|
+
bottom: 0,
|
|
135
|
+
width: 0,
|
|
136
|
+
},
|
|
137
|
+
scanBar: {
|
|
138
|
+
position: 'absolute',
|
|
139
|
+
left: 0,
|
|
140
|
+
right: 0,
|
|
141
|
+
height: BAR_THICKNESS,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
@@ -220,15 +220,15 @@
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
if (self.context && _coreImageContext)
|
|
223
|
+
if (self.context && _coreImageContext && _glkView)
|
|
224
224
|
{
|
|
225
225
|
// Calculate the rect to draw the image with aspect fill
|
|
226
|
-
CGRect
|
|
226
|
+
CGRect viewBounds = _glkView.bounds;
|
|
227
227
|
CGRect imageExtent = image.extent;
|
|
228
228
|
|
|
229
229
|
// Calculate aspect ratios
|
|
230
230
|
CGFloat imageAspect = imageExtent.size.width / imageExtent.size.height;
|
|
231
|
-
CGFloat viewAspect =
|
|
231
|
+
CGFloat viewAspect = viewBounds.size.width / viewBounds.size.height;
|
|
232
232
|
|
|
233
233
|
CGRect fromRect = imageExtent;
|
|
234
234
|
|
|
@@ -245,6 +245,12 @@
|
|
|
245
245
|
fromRect = CGRectMake(0, yOffset, imageExtent.size.width, newHeight);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// GLKView renderbuffer expects pixel dimensions, not point-based bounds
|
|
249
|
+
CGRect drawRect = CGRectMake(0,
|
|
250
|
+
0,
|
|
251
|
+
(CGFloat)_glkView.drawableWidth,
|
|
252
|
+
(CGFloat)_glkView.drawableHeight);
|
|
253
|
+
|
|
248
254
|
[_coreImageContext drawImage:image inRect:drawRect fromRect:fromRect];
|
|
249
255
|
[self.context presentRenderbuffer:GL_RENDERBUFFER];
|
|
250
256
|
|