react-native-rectangle-doc-scanner 3.27.0 → 3.31.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 +3 -0
- package/dist/DocScanner.js +45 -17
- package/dist/FullDocScanner.js +201 -38
- package/dist/types.d.ts +1 -0
- package/dist/utils/overlay.js +25 -105
- package/package.json +1 -1
- package/src/CropEditor.tsx +15 -8
- package/src/DocScanner.tsx +52 -17
- package/src/FullDocScanner.tsx +288 -55
- package/src/types.ts +1 -0
- package/src/utils/overlay.tsx +39 -158
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
|
@@ -17,8 +17,10 @@ export type DocScannerCapture = {
|
|
|
17
17
|
initialPath: string | null;
|
|
18
18
|
croppedPath: string | null;
|
|
19
19
|
quad: Point[] | null;
|
|
20
|
+
rectangle: Rectangle | null;
|
|
20
21
|
width: number;
|
|
21
22
|
height: number;
|
|
23
|
+
origin: 'auto' | 'manual';
|
|
22
24
|
};
|
|
23
25
|
export interface DetectionConfig {
|
|
24
26
|
processingWidth?: number;
|
|
@@ -42,6 +44,7 @@ interface Props {
|
|
|
42
44
|
gridLineWidth?: number;
|
|
43
45
|
detectionConfig?: DetectionConfig;
|
|
44
46
|
onRectangleDetect?: (event: RectangleDetectEvent) => void;
|
|
47
|
+
showManualCaptureButton?: boolean;
|
|
45
48
|
}
|
|
46
49
|
export type DocScannerHandle = {
|
|
47
50
|
capture: () => Promise<PictureEvent>;
|
package/dist/DocScanner.js
CHANGED
|
@@ -68,11 +68,13 @@ const normalizeRectangle = (rectangle) => {
|
|
|
68
68
|
};
|
|
69
69
|
};
|
|
70
70
|
const DEFAULT_OVERLAY_COLOR = '#0b7ef4';
|
|
71
|
-
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) => {
|
|
71
|
+
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, showManualCaptureButton = false, }, ref) => {
|
|
72
72
|
const scannerRef = (0, react_1.useRef)(null);
|
|
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);
|
|
77
|
+
const captureOriginRef = (0, react_1.useRef)('auto');
|
|
76
78
|
(0, react_1.useEffect)(() => {
|
|
77
79
|
if (!autoCapture) {
|
|
78
80
|
setIsAutoCapturing(false);
|
|
@@ -87,19 +89,23 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
87
89
|
}, [quality]);
|
|
88
90
|
const handlePictureTaken = (0, react_1.useCallback)((event) => {
|
|
89
91
|
setIsAutoCapturing(false);
|
|
90
|
-
const normalizedRectangle = normalizeRectangle(event.rectangleCoordinates ?? null);
|
|
92
|
+
const normalizedRectangle = normalizeRectangle(event.rectangleCoordinates ?? null) ?? lastRectangleRef.current;
|
|
91
93
|
const quad = normalizedRectangle ? (0, coordinate_1.rectangleToQuad)(normalizedRectangle) : null;
|
|
94
|
+
const origin = captureOriginRef.current;
|
|
95
|
+
captureOriginRef.current = 'auto';
|
|
92
96
|
const initialPath = event.initialImage ?? null;
|
|
93
97
|
const croppedPath = event.croppedImage ?? null;
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
98
|
+
const editablePath = initialPath ?? croppedPath;
|
|
99
|
+
if (editablePath) {
|
|
96
100
|
onCapture?.({
|
|
97
|
-
path:
|
|
101
|
+
path: editablePath,
|
|
98
102
|
initialPath,
|
|
99
103
|
croppedPath,
|
|
100
104
|
quad,
|
|
105
|
+
rectangle: normalizedRectangle,
|
|
101
106
|
width: event.width ?? 0,
|
|
102
107
|
height: event.height ?? 0,
|
|
108
|
+
origin,
|
|
103
109
|
});
|
|
104
110
|
}
|
|
105
111
|
setDetectedRectangle(null);
|
|
@@ -115,32 +121,50 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
115
121
|
}
|
|
116
122
|
}, []);
|
|
117
123
|
const capture = (0, react_1.useCallback)(() => {
|
|
124
|
+
captureOriginRef.current = 'manual';
|
|
118
125
|
const instance = scannerRef.current;
|
|
119
126
|
if (!instance || typeof instance.capture !== 'function') {
|
|
127
|
+
captureOriginRef.current = 'auto';
|
|
120
128
|
return Promise.reject(new Error('DocumentScanner native instance is not ready'));
|
|
121
129
|
}
|
|
122
130
|
if (captureResolvers.current) {
|
|
131
|
+
captureOriginRef.current = 'auto';
|
|
123
132
|
return Promise.reject(new Error('capture_in_progress'));
|
|
124
133
|
}
|
|
125
|
-
|
|
134
|
+
let result;
|
|
135
|
+
try {
|
|
136
|
+
result = instance.capture();
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
captureOriginRef.current = 'auto';
|
|
140
|
+
return Promise.reject(error);
|
|
141
|
+
}
|
|
126
142
|
if (result && typeof result.then === 'function') {
|
|
127
|
-
return result.
|
|
128
|
-
|
|
129
|
-
|
|
143
|
+
return result.catch((error) => {
|
|
144
|
+
captureOriginRef.current = 'auto';
|
|
145
|
+
throw error;
|
|
130
146
|
});
|
|
131
147
|
}
|
|
132
148
|
return new Promise((resolve, reject) => {
|
|
133
|
-
captureResolvers.current = {
|
|
149
|
+
captureResolvers.current = {
|
|
150
|
+
resolve: (value) => {
|
|
151
|
+
captureOriginRef.current = 'auto';
|
|
152
|
+
resolve(value);
|
|
153
|
+
},
|
|
154
|
+
reject: (reason) => {
|
|
155
|
+
captureOriginRef.current = 'auto';
|
|
156
|
+
reject(reason);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
134
159
|
});
|
|
135
|
-
}, [
|
|
160
|
+
}, []);
|
|
136
161
|
const handleManualCapture = (0, react_1.useCallback)(() => {
|
|
137
|
-
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
162
|
+
captureOriginRef.current = 'manual';
|
|
140
163
|
capture().catch((error) => {
|
|
164
|
+
captureOriginRef.current = 'auto';
|
|
141
165
|
console.warn('[DocScanner] manual capture failed', error);
|
|
142
166
|
});
|
|
143
|
-
}, [
|
|
167
|
+
}, [capture]);
|
|
144
168
|
const handleRectangleDetect = (0, react_1.useCallback)((event) => {
|
|
145
169
|
const rectangleCoordinates = normalizeRectangle(event.rectangleCoordinates ?? null);
|
|
146
170
|
const rectangleOnScreen = normalizeRectangle(event.rectangleOnScreen ?? null);
|
|
@@ -157,6 +181,9 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
157
181
|
setIsAutoCapturing(false);
|
|
158
182
|
}
|
|
159
183
|
}
|
|
184
|
+
if (payload.rectangleCoordinates) {
|
|
185
|
+
lastRectangleRef.current = payload.rectangleCoordinates;
|
|
186
|
+
}
|
|
160
187
|
const isGoodRectangle = payload.lastDetectionType === 0;
|
|
161
188
|
setDetectedRectangle(isGoodRectangle && rectangleOnScreen ? payload : null);
|
|
162
189
|
onRectangleDetect?.(payload);
|
|
@@ -168,14 +195,15 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
168
195
|
captureResolvers.current.reject(new Error('reset'));
|
|
169
196
|
captureResolvers.current = null;
|
|
170
197
|
}
|
|
198
|
+
captureOriginRef.current = 'auto';
|
|
171
199
|
},
|
|
172
200
|
}), [capture]);
|
|
173
|
-
const overlayPolygon = detectedRectangle?.rectangleOnScreen ?? null;
|
|
201
|
+
const overlayPolygon = detectedRectangle?.rectangleOnScreen ?? detectedRectangle?.rectangleCoordinates ?? null;
|
|
174
202
|
const overlayIsActive = autoCapture ? isAutoCapturing : (detectedRectangle?.stableCounter ?? 0) > 0;
|
|
175
203
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
176
204
|
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 }),
|
|
177
205
|
showGrid && overlayPolygon && (react_1.default.createElement(overlay_1.ScannerOverlay, { active: overlayIsActive, color: gridColor ?? overlayColor, lineWidth: gridLineWidth, polygon: overlayPolygon })),
|
|
178
|
-
!autoCapture && react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: handleManualCapture }),
|
|
206
|
+
(showManualCaptureButton || !autoCapture) && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: handleManualCapture })),
|
|
179
207
|
children));
|
|
180
208
|
});
|
|
181
209
|
const styles = react_native_1.StyleSheet.create({
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -41,6 +41,27 @@ const CropEditor_1 = require("./CropEditor");
|
|
|
41
41
|
const coordinate_1 = require("./utils/coordinate");
|
|
42
42
|
const stripFileUri = (value) => value.replace(/^file:\/\//, '');
|
|
43
43
|
const ensureFileUri = (value) => (value.startsWith('file://') ? value : `file://${value}`);
|
|
44
|
+
const createFullImageRectangle = (width, height) => ({
|
|
45
|
+
topLeft: { x: 0, y: 0 },
|
|
46
|
+
topRight: { x: width, y: 0 },
|
|
47
|
+
bottomRight: { x: width, y: height },
|
|
48
|
+
bottomLeft: { x: 0, y: height },
|
|
49
|
+
});
|
|
50
|
+
const resolveImageSize = (path, fallbackWidth, fallbackHeight) => new Promise((resolve) => {
|
|
51
|
+
react_native_1.Image.getSize(ensureFileUri(path), (width, height) => resolve({ width, height }), () => resolve({
|
|
52
|
+
width: fallbackWidth > 0 ? fallbackWidth : 0,
|
|
53
|
+
height: fallbackHeight > 0 ? fallbackHeight : 0,
|
|
54
|
+
}));
|
|
55
|
+
});
|
|
56
|
+
const normalizeCapturedDocument = (document) => {
|
|
57
|
+
const normalizedPath = stripFileUri(document.initialPath ?? document.path);
|
|
58
|
+
return {
|
|
59
|
+
...document,
|
|
60
|
+
path: normalizedPath,
|
|
61
|
+
initialPath: document.initialPath ? stripFileUri(document.initialPath) : normalizedPath,
|
|
62
|
+
croppedPath: document.croppedPath ? stripFileUri(document.croppedPath) : null,
|
|
63
|
+
};
|
|
64
|
+
};
|
|
44
65
|
const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3170f3', gridColor, gridLineWidth, showGrid, overlayStrokeColor = '#3170f3', handlerColor = '#3170f3', strings, manualCapture = false, minStableFrames, onError, }) => {
|
|
45
66
|
const [screen, setScreen] = (0, react_1.useState)('scanner');
|
|
46
67
|
const [capturedDoc, setCapturedDoc] = (0, react_1.useState)(null);
|
|
@@ -48,6 +69,10 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
48
69
|
const [imageSize, setImageSize] = (0, react_1.useState)(null);
|
|
49
70
|
const [processing, setProcessing] = (0, react_1.useState)(false);
|
|
50
71
|
const resolvedGridColor = gridColor ?? overlayColor;
|
|
72
|
+
const docScannerRef = (0, react_1.useRef)(null);
|
|
73
|
+
const manualCapturePending = (0, react_1.useRef)(false);
|
|
74
|
+
const processingCaptureRef = (0, react_1.useRef)(false);
|
|
75
|
+
const cropInitializedRef = (0, react_1.useRef)(false);
|
|
51
76
|
const mergedStrings = (0, react_1.useMemo)(() => ({
|
|
52
77
|
captureHint: strings?.captureHint ?? 'Align the document within the frame.',
|
|
53
78
|
manualHint: strings?.manualHint ?? 'Tap the button below to capture.',
|
|
@@ -63,30 +88,36 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
63
88
|
}
|
|
64
89
|
react_native_1.Image.getSize(ensureFileUri(capturedDoc.path), (width, height) => setImageSize({ width, height }), () => setImageSize({ width: capturedDoc.width, height: capturedDoc.height }));
|
|
65
90
|
}, [capturedDoc]);
|
|
91
|
+
(0, react_1.useEffect)(() => {
|
|
92
|
+
if (!capturedDoc || !imageSize || cropInitializedRef.current) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const baseWidth = capturedDoc.width > 0 ? capturedDoc.width : imageSize.width;
|
|
96
|
+
const baseHeight = capturedDoc.height > 0 ? capturedDoc.height : imageSize.height;
|
|
97
|
+
let initialRectangle = null;
|
|
98
|
+
if (capturedDoc.rectangle) {
|
|
99
|
+
initialRectangle = (0, coordinate_1.scaleRectangle)(capturedDoc.rectangle, baseWidth, baseHeight, imageSize.width, imageSize.height);
|
|
100
|
+
}
|
|
101
|
+
else if (capturedDoc.quad && capturedDoc.quad.length === 4) {
|
|
102
|
+
const quadRectangle = (0, coordinate_1.quadToRectangle)(capturedDoc.quad);
|
|
103
|
+
if (quadRectangle) {
|
|
104
|
+
initialRectangle = (0, coordinate_1.scaleRectangle)(quadRectangle, baseWidth, baseHeight, imageSize.width, imageSize.height);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (initialRectangle) {
|
|
108
|
+
cropInitializedRef.current = true;
|
|
109
|
+
setCropRectangle(initialRectangle);
|
|
110
|
+
}
|
|
111
|
+
}, [capturedDoc, imageSize]);
|
|
66
112
|
const resetState = (0, react_1.useCallback)(() => {
|
|
67
113
|
setScreen('scanner');
|
|
68
114
|
setCapturedDoc(null);
|
|
69
115
|
setCropRectangle(null);
|
|
70
116
|
setImageSize(null);
|
|
71
117
|
setProcessing(false);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const nextQuad = document.quad && document.quad.length === 4 ? document.quad : null;
|
|
76
|
-
const normalizedInitial = document.initialPath != null ? stripFileUri(document.initialPath) : normalizedPath;
|
|
77
|
-
const normalizedCropped = document.croppedPath != null ? stripFileUri(document.croppedPath) : null;
|
|
78
|
-
setCapturedDoc({
|
|
79
|
-
...document,
|
|
80
|
-
path: normalizedPath,
|
|
81
|
-
initialPath: normalizedInitial,
|
|
82
|
-
croppedPath: normalizedCropped,
|
|
83
|
-
quad: nextQuad,
|
|
84
|
-
});
|
|
85
|
-
setCropRectangle(nextQuad ? (0, coordinate_1.quadToRectangle)(nextQuad) : null);
|
|
86
|
-
setScreen('crop');
|
|
87
|
-
}, []);
|
|
88
|
-
const handleCropChange = (0, react_1.useCallback)((rectangle) => {
|
|
89
|
-
setCropRectangle(rectangle);
|
|
118
|
+
manualCapturePending.current = false;
|
|
119
|
+
processingCaptureRef.current = false;
|
|
120
|
+
cropInitializedRef.current = false;
|
|
90
121
|
}, []);
|
|
91
122
|
const emitError = (0, react_1.useCallback)((error, fallbackMessage) => {
|
|
92
123
|
console.error('[FullDocScanner] error', error);
|
|
@@ -95,6 +126,104 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
95
126
|
react_native_1.Alert.alert('Document Scanner', fallbackMessage);
|
|
96
127
|
}
|
|
97
128
|
}, [onError]);
|
|
129
|
+
const processAutoCapture = (0, react_1.useCallback)(async (document) => {
|
|
130
|
+
manualCapturePending.current = false;
|
|
131
|
+
const normalizedDoc = normalizeCapturedDocument(document);
|
|
132
|
+
const cropManager = react_native_1.NativeModules.CustomCropManager;
|
|
133
|
+
if (!cropManager?.crop) {
|
|
134
|
+
emitError(new Error('CustomCropManager.crop is not available'));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
setProcessing(true);
|
|
138
|
+
try {
|
|
139
|
+
const size = await resolveImageSize(normalizedDoc.path, normalizedDoc.width, normalizedDoc.height);
|
|
140
|
+
const targetWidthRaw = size.width > 0 ? size.width : normalizedDoc.width;
|
|
141
|
+
const targetHeightRaw = size.height > 0 ? size.height : normalizedDoc.height;
|
|
142
|
+
const baseWidth = normalizedDoc.width > 0 ? normalizedDoc.width : targetWidthRaw;
|
|
143
|
+
const baseHeight = normalizedDoc.height > 0 ? normalizedDoc.height : targetHeightRaw;
|
|
144
|
+
const targetWidth = targetWidthRaw > 0 ? targetWidthRaw : baseWidth || 1;
|
|
145
|
+
const targetHeight = targetHeightRaw > 0 ? targetHeightRaw : baseHeight || 1;
|
|
146
|
+
let rectangleBase = normalizedDoc.rectangle ?? null;
|
|
147
|
+
if (!rectangleBase && normalizedDoc.quad && normalizedDoc.quad.length === 4) {
|
|
148
|
+
rectangleBase = (0, coordinate_1.quadToRectangle)(normalizedDoc.quad);
|
|
149
|
+
}
|
|
150
|
+
const scaledRectangle = rectangleBase
|
|
151
|
+
? (0, coordinate_1.scaleRectangle)(rectangleBase, baseWidth || targetWidth, baseHeight || targetHeight, targetWidth, targetHeight)
|
|
152
|
+
: null;
|
|
153
|
+
const rectangleToUse = scaledRectangle ?? createFullImageRectangle(targetWidth, targetHeight);
|
|
154
|
+
const base64 = await new Promise((resolve, reject) => {
|
|
155
|
+
cropManager.crop({
|
|
156
|
+
topLeft: rectangleToUse.topLeft,
|
|
157
|
+
topRight: rectangleToUse.topRight,
|
|
158
|
+
bottomRight: rectangleToUse.bottomRight,
|
|
159
|
+
bottomLeft: rectangleToUse.bottomLeft,
|
|
160
|
+
width: targetWidth,
|
|
161
|
+
height: targetHeight,
|
|
162
|
+
}, ensureFileUri(normalizedDoc.path), (error, result) => {
|
|
163
|
+
if (error) {
|
|
164
|
+
reject(error instanceof Error ? error : new Error('Crop failed'));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
resolve(result.image);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
const finalDoc = {
|
|
171
|
+
...normalizedDoc,
|
|
172
|
+
rectangle: rectangleToUse,
|
|
173
|
+
};
|
|
174
|
+
onResult({
|
|
175
|
+
original: finalDoc,
|
|
176
|
+
rectangle: rectangleToUse,
|
|
177
|
+
base64,
|
|
178
|
+
});
|
|
179
|
+
resetState();
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
setProcessing(false);
|
|
183
|
+
emitError(error instanceof Error ? error : new Error(String(error)), 'Failed to process document.');
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
processingCaptureRef.current = false;
|
|
187
|
+
}
|
|
188
|
+
}, [emitError, onResult, resetState]);
|
|
189
|
+
const handleCapture = (0, react_1.useCallback)((document) => {
|
|
190
|
+
if (processingCaptureRef.current) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const isManualCapture = manualCapture || manualCapturePending.current || document.origin === 'manual';
|
|
194
|
+
const normalizedDoc = normalizeCapturedDocument(document);
|
|
195
|
+
if (isManualCapture) {
|
|
196
|
+
manualCapturePending.current = false;
|
|
197
|
+
processingCaptureRef.current = false;
|
|
198
|
+
cropInitializedRef.current = false;
|
|
199
|
+
setCapturedDoc(normalizedDoc);
|
|
200
|
+
setImageSize(null);
|
|
201
|
+
setCropRectangle(null);
|
|
202
|
+
setScreen('crop');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
processingCaptureRef.current = true;
|
|
206
|
+
processAutoCapture(document);
|
|
207
|
+
}, [manualCapture, processAutoCapture]);
|
|
208
|
+
const handleCropChange = (0, react_1.useCallback)((rectangle) => {
|
|
209
|
+
setCropRectangle(rectangle);
|
|
210
|
+
}, []);
|
|
211
|
+
const triggerManualCapture = (0, react_1.useCallback)(() => {
|
|
212
|
+
if (processingCaptureRef.current) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
manualCapturePending.current = true;
|
|
216
|
+
const capturePromise = docScannerRef.current?.capture();
|
|
217
|
+
if (capturePromise && typeof capturePromise.catch === 'function') {
|
|
218
|
+
capturePromise.catch((error) => {
|
|
219
|
+
manualCapturePending.current = false;
|
|
220
|
+
console.warn('[FullDocScanner] manual capture failed', error);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
else if (!capturePromise) {
|
|
224
|
+
manualCapturePending.current = false;
|
|
225
|
+
}
|
|
226
|
+
}, []);
|
|
98
227
|
const performCrop = (0, react_1.useCallback)(async () => {
|
|
99
228
|
if (!capturedDoc) {
|
|
100
229
|
throw new Error('No captured document to crop');
|
|
@@ -104,21 +233,29 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
104
233
|
if (!cropManager?.crop) {
|
|
105
234
|
throw new Error('CustomCropManager.crop is not available');
|
|
106
235
|
}
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
236
|
+
const baseWidth = capturedDoc.width > 0 ? capturedDoc.width : size.width;
|
|
237
|
+
const baseHeight = capturedDoc.height > 0 ? capturedDoc.height : size.height;
|
|
238
|
+
const targetWidth = size.width > 0 ? size.width : baseWidth || 1;
|
|
239
|
+
const targetHeight = size.height > 0 ? size.height : baseHeight || 1;
|
|
240
|
+
let fallbackRectangle = null;
|
|
241
|
+
if (capturedDoc.rectangle) {
|
|
242
|
+
fallbackRectangle = (0, coordinate_1.scaleRectangle)(capturedDoc.rectangle, baseWidth || targetWidth, baseHeight || targetHeight, targetWidth, targetHeight);
|
|
243
|
+
}
|
|
244
|
+
else if (capturedDoc.quad && capturedDoc.quad.length === 4) {
|
|
245
|
+
const quadRectangle = (0, coordinate_1.quadToRectangle)(capturedDoc.quad);
|
|
246
|
+
if (quadRectangle) {
|
|
247
|
+
fallbackRectangle = (0, coordinate_1.scaleRectangle)(quadRectangle, baseWidth || targetWidth, baseHeight || targetHeight, targetWidth, targetHeight);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const rectangleToUse = cropRectangle ?? fallbackRectangle ?? createFullImageRectangle(targetWidth, targetHeight);
|
|
114
251
|
const base64 = await new Promise((resolve, reject) => {
|
|
115
252
|
cropManager.crop({
|
|
116
|
-
topLeft:
|
|
117
|
-
topRight:
|
|
118
|
-
bottomRight:
|
|
119
|
-
bottomLeft:
|
|
120
|
-
width:
|
|
121
|
-
height:
|
|
253
|
+
topLeft: rectangleToUse.topLeft,
|
|
254
|
+
topRight: rectangleToUse.topRight,
|
|
255
|
+
bottomRight: rectangleToUse.bottomRight,
|
|
256
|
+
bottomLeft: rectangleToUse.bottomLeft,
|
|
257
|
+
width: targetWidth,
|
|
258
|
+
height: targetHeight,
|
|
122
259
|
}, ensureFileUri(capturedDoc.path), (error, result) => {
|
|
123
260
|
if (error) {
|
|
124
261
|
reject(error instanceof Error ? error : new Error('Crop failed'));
|
|
@@ -127,7 +264,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
127
264
|
resolve(result.image);
|
|
128
265
|
});
|
|
129
266
|
});
|
|
130
|
-
return base64;
|
|
267
|
+
return { base64, rectangle: rectangleToUse };
|
|
131
268
|
}, [capturedDoc, cropRectangle, imageSize]);
|
|
132
269
|
const handleConfirm = (0, react_1.useCallback)(async () => {
|
|
133
270
|
if (!capturedDoc) {
|
|
@@ -135,11 +272,15 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
135
272
|
}
|
|
136
273
|
try {
|
|
137
274
|
setProcessing(true);
|
|
138
|
-
const base64 = await performCrop();
|
|
275
|
+
const { base64, rectangle } = await performCrop();
|
|
139
276
|
setProcessing(false);
|
|
277
|
+
const finalDoc = {
|
|
278
|
+
...capturedDoc,
|
|
279
|
+
rectangle,
|
|
280
|
+
};
|
|
140
281
|
onResult({
|
|
141
|
-
original:
|
|
142
|
-
rectangle
|
|
282
|
+
original: finalDoc,
|
|
283
|
+
rectangle,
|
|
143
284
|
base64,
|
|
144
285
|
});
|
|
145
286
|
resetState();
|
|
@@ -148,7 +289,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
148
289
|
setProcessing(false);
|
|
149
290
|
emitError(error instanceof Error ? error : new Error(String(error)), 'Failed to process document.');
|
|
150
291
|
}
|
|
151
|
-
}, [capturedDoc,
|
|
292
|
+
}, [capturedDoc, emitError, onResult, performCrop, resetState]);
|
|
152
293
|
const handleRetake = (0, react_1.useCallback)(() => {
|
|
153
294
|
resetState();
|
|
154
295
|
}, [resetState]);
|
|
@@ -158,13 +299,15 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
158
299
|
}, [onClose, resetState]);
|
|
159
300
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
160
301
|
screen === 'scanner' && (react_1.default.createElement(react_native_1.View, { style: styles.flex },
|
|
161
|
-
react_1.default.createElement(DocScanner_1.DocScanner, { autoCapture: !manualCapture, overlayColor: overlayColor, showGrid: showGrid, gridColor: resolvedGridColor, gridLineWidth: gridLineWidth, minStableFrames: minStableFrames ?? 6, detectionConfig: detectionConfig, onCapture: handleCapture },
|
|
302
|
+
react_1.default.createElement(DocScanner_1.DocScanner, { ref: docScannerRef, autoCapture: !manualCapture, overlayColor: overlayColor, showGrid: showGrid, gridColor: resolvedGridColor, gridLineWidth: gridLineWidth, minStableFrames: minStableFrames ?? 6, detectionConfig: detectionConfig, onCapture: handleCapture },
|
|
162
303
|
react_1.default.createElement(react_native_1.View, { style: styles.overlay, pointerEvents: "box-none" },
|
|
163
304
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.closeButton, onPress: handleClose, accessibilityLabel: mergedStrings.cancel, accessibilityRole: "button" },
|
|
164
305
|
react_1.default.createElement(react_native_1.Text, { style: styles.closeButtonLabel }, "\u00D7")),
|
|
165
306
|
react_1.default.createElement(react_native_1.View, { style: styles.instructions, pointerEvents: "none" },
|
|
166
307
|
react_1.default.createElement(react_native_1.Text, { style: styles.captureText }, mergedStrings.captureHint),
|
|
167
|
-
|
|
308
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.captureText }, mergedStrings.manualHint)),
|
|
309
|
+
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.shutterButton, processing && styles.shutterButtonDisabled], onPress: triggerManualCapture, disabled: processing, accessibilityLabel: mergedStrings.manualHint, accessibilityRole: "button" },
|
|
310
|
+
react_1.default.createElement(react_native_1.View, { style: styles.shutterInner })))))),
|
|
168
311
|
screen === 'crop' && capturedDoc && (react_1.default.createElement(react_native_1.View, { style: styles.flex },
|
|
169
312
|
react_1.default.createElement(CropEditor_1.CropEditor, { document: capturedDoc, overlayColor: "rgba(0,0,0,0.6)", overlayStrokeColor: overlayStrokeColor, handlerColor: handlerColor, onCropChange: handleCropChange }),
|
|
170
313
|
react_1.default.createElement(react_native_1.View, { style: styles.cropFooter },
|
|
@@ -219,6 +362,26 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
219
362
|
fontSize: 15,
|
|
220
363
|
textAlign: 'center',
|
|
221
364
|
},
|
|
365
|
+
shutterButton: {
|
|
366
|
+
alignSelf: 'center',
|
|
367
|
+
width: 80,
|
|
368
|
+
height: 80,
|
|
369
|
+
borderRadius: 40,
|
|
370
|
+
borderWidth: 4,
|
|
371
|
+
borderColor: '#fff',
|
|
372
|
+
justifyContent: 'center',
|
|
373
|
+
alignItems: 'center',
|
|
374
|
+
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
375
|
+
},
|
|
376
|
+
shutterButtonDisabled: {
|
|
377
|
+
opacity: 0.4,
|
|
378
|
+
},
|
|
379
|
+
shutterInner: {
|
|
380
|
+
width: 60,
|
|
381
|
+
height: 60,
|
|
382
|
+
borderRadius: 30,
|
|
383
|
+
backgroundColor: '#fff',
|
|
384
|
+
},
|
|
222
385
|
cropFooter: {
|
|
223
386
|
position: 'absolute',
|
|
224
387
|
bottom: 40,
|