react-native-rectangle-doc-scanner 3.41.0 → 3.43.1
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/SETUP.md +186 -0
- package/dist/FullDocScanner.d.ts +11 -10
- package/dist/FullDocScanner.js +75 -244
- package/package.json +6 -3
- package/src/FullDocScanner.tsx +141 -415
package/src/FullDocScanner.tsx
CHANGED
|
@@ -1,55 +1,20 @@
|
|
|
1
|
-
import React, { useCallback,
|
|
1
|
+
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
ActivityIndicator,
|
|
4
4
|
Alert,
|
|
5
|
-
Image,
|
|
6
|
-
NativeModules,
|
|
7
5
|
StyleSheet,
|
|
8
6
|
Text,
|
|
9
7
|
TouchableOpacity,
|
|
10
8
|
View,
|
|
11
9
|
} from 'react-native';
|
|
10
|
+
import { launchImageLibrary } from 'react-native-image-picker';
|
|
11
|
+
import ImageCropPicker from 'react-native-image-crop-picker';
|
|
12
12
|
import { DocScanner } from './DocScanner';
|
|
13
|
-
import {
|
|
14
|
-
import type { CapturedDocument, Point, Quad, Rectangle } from './types';
|
|
13
|
+
import type { CapturedDocument } from './types';
|
|
15
14
|
import type { DetectionConfig, DocScannerHandle, DocScannerCapture } from './DocScanner';
|
|
16
|
-
import { createFullImageRectangle, quadToRectangle, scaleRectangle } from './utils/coordinate';
|
|
17
|
-
|
|
18
|
-
type CustomCropManagerType = {
|
|
19
|
-
crop: (
|
|
20
|
-
points: {
|
|
21
|
-
topLeft: Point;
|
|
22
|
-
topRight: Point;
|
|
23
|
-
bottomRight: Point;
|
|
24
|
-
bottomLeft: Point;
|
|
25
|
-
width: number;
|
|
26
|
-
height: number;
|
|
27
|
-
},
|
|
28
|
-
imageUri: string,
|
|
29
|
-
callback: (error: unknown, result: { image: string }) => void,
|
|
30
|
-
) => void;
|
|
31
|
-
};
|
|
32
15
|
|
|
33
16
|
const stripFileUri = (value: string) => value.replace(/^file:\/\//, '');
|
|
34
17
|
|
|
35
|
-
const ensureFileUri = (value: string) => (value.startsWith('file://') ? value : `file://${value}`);
|
|
36
|
-
|
|
37
|
-
const resolveImageSize = (
|
|
38
|
-
path: string,
|
|
39
|
-
fallbackWidth: number,
|
|
40
|
-
fallbackHeight: number,
|
|
41
|
-
): Promise<{ width: number; height: number }> =>
|
|
42
|
-
new Promise((resolve) => {
|
|
43
|
-
Image.getSize(
|
|
44
|
-
ensureFileUri(path),
|
|
45
|
-
(width, height) => resolve({ width, height }),
|
|
46
|
-
() => resolve({
|
|
47
|
-
width: fallbackWidth > 0 ? fallbackWidth : 0,
|
|
48
|
-
height: fallbackHeight > 0 ? fallbackHeight : 0,
|
|
49
|
-
}),
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
18
|
const normalizeCapturedDocument = (document: DocScannerCapture): CapturedDocument => {
|
|
54
19
|
const { origin: _origin, ...rest } = document;
|
|
55
20
|
const normalizedPath = stripFileUri(document.initialPath ?? document.path);
|
|
@@ -62,20 +27,20 @@ const normalizeCapturedDocument = (document: DocScannerCapture): CapturedDocumen
|
|
|
62
27
|
};
|
|
63
28
|
|
|
64
29
|
export interface FullDocScannerResult {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
/** Base64-encoded
|
|
68
|
-
base64
|
|
30
|
+
/** File path to the cropped image */
|
|
31
|
+
path: string;
|
|
32
|
+
/** Base64-encoded image string (optional) */
|
|
33
|
+
base64?: string;
|
|
34
|
+
/** Original captured document info */
|
|
35
|
+
original?: CapturedDocument;
|
|
69
36
|
}
|
|
70
37
|
|
|
71
38
|
export interface FullDocScannerStrings {
|
|
72
39
|
captureHint?: string;
|
|
73
40
|
manualHint?: string;
|
|
74
41
|
cancel?: string;
|
|
75
|
-
confirm?: string;
|
|
76
|
-
retake?: string;
|
|
77
|
-
cropTitle?: string;
|
|
78
42
|
processing?: string;
|
|
43
|
+
galleryButton?: string;
|
|
79
44
|
}
|
|
80
45
|
|
|
81
46
|
export interface FullDocScannerProps {
|
|
@@ -86,16 +51,15 @@ export interface FullDocScannerProps {
|
|
|
86
51
|
gridColor?: string;
|
|
87
52
|
gridLineWidth?: number;
|
|
88
53
|
showGrid?: boolean;
|
|
89
|
-
overlayStrokeColor?: string;
|
|
90
|
-
handlerColor?: string;
|
|
91
54
|
strings?: FullDocScannerStrings;
|
|
92
55
|
manualCapture?: boolean;
|
|
93
56
|
minStableFrames?: number;
|
|
94
57
|
onError?: (error: Error) => void;
|
|
58
|
+
enableGallery?: boolean;
|
|
59
|
+
cropWidth?: number;
|
|
60
|
+
cropHeight?: number;
|
|
95
61
|
}
|
|
96
62
|
|
|
97
|
-
type ScreenState = 'scanner' | 'crop';
|
|
98
|
-
|
|
99
63
|
export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
100
64
|
onResult,
|
|
101
65
|
onClose,
|
|
@@ -104,97 +68,30 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
104
68
|
gridColor,
|
|
105
69
|
gridLineWidth,
|
|
106
70
|
showGrid,
|
|
107
|
-
overlayStrokeColor = '#3170f3',
|
|
108
|
-
handlerColor = '#3170f3',
|
|
109
71
|
strings,
|
|
110
72
|
manualCapture = false,
|
|
111
73
|
minStableFrames,
|
|
112
74
|
onError,
|
|
75
|
+
enableGallery = true,
|
|
76
|
+
cropWidth = 1200,
|
|
77
|
+
cropHeight = 1600,
|
|
113
78
|
}) => {
|
|
114
|
-
const [screen, setScreen] = useState<ScreenState>('scanner');
|
|
115
|
-
const [capturedDoc, setCapturedDoc] = useState<CapturedDocument | null>(null);
|
|
116
|
-
const [cropRectangle, setCropRectangle] = useState<Rectangle | null>(null);
|
|
117
|
-
const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null);
|
|
118
79
|
const [processing, setProcessing] = useState(false);
|
|
119
80
|
const resolvedGridColor = gridColor ?? overlayColor;
|
|
120
81
|
const docScannerRef = useRef<DocScannerHandle | null>(null);
|
|
121
82
|
const manualCapturePending = useRef(false);
|
|
122
|
-
const processingCaptureRef = useRef(false);
|
|
123
|
-
const cropInitializedRef = useRef(false);
|
|
124
83
|
|
|
125
84
|
const mergedStrings = useMemo(
|
|
126
85
|
() => ({
|
|
127
86
|
captureHint: strings?.captureHint,
|
|
128
87
|
manualHint: strings?.manualHint,
|
|
129
88
|
cancel: strings?.cancel,
|
|
130
|
-
confirm: strings?.confirm,
|
|
131
|
-
retake: strings?.retake,
|
|
132
|
-
cropTitle: strings?.cropTitle,
|
|
133
89
|
processing: strings?.processing,
|
|
90
|
+
galleryButton: strings?.galleryButton,
|
|
134
91
|
}),
|
|
135
92
|
[strings],
|
|
136
93
|
);
|
|
137
94
|
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
if (!capturedDoc) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
Image.getSize(
|
|
144
|
-
ensureFileUri(capturedDoc.path),
|
|
145
|
-
(width, height) => setImageSize({ width, height }),
|
|
146
|
-
() => setImageSize({ width: capturedDoc.width, height: capturedDoc.height }),
|
|
147
|
-
);
|
|
148
|
-
}, [capturedDoc]);
|
|
149
|
-
|
|
150
|
-
useEffect(() => {
|
|
151
|
-
if (!capturedDoc || !imageSize || cropInitializedRef.current) {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const baseWidth = capturedDoc.width > 0 ? capturedDoc.width : imageSize.width;
|
|
156
|
-
const baseHeight = capturedDoc.height > 0 ? capturedDoc.height : imageSize.height;
|
|
157
|
-
|
|
158
|
-
let initialRectangle: Rectangle | null = null;
|
|
159
|
-
|
|
160
|
-
if (capturedDoc.rectangle) {
|
|
161
|
-
initialRectangle = scaleRectangle(
|
|
162
|
-
capturedDoc.rectangle,
|
|
163
|
-
baseWidth,
|
|
164
|
-
baseHeight,
|
|
165
|
-
imageSize.width,
|
|
166
|
-
imageSize.height,
|
|
167
|
-
);
|
|
168
|
-
} else if (capturedDoc.quad && capturedDoc.quad.length === 4) {
|
|
169
|
-
const quadRectangle = quadToRectangle(capturedDoc.quad as Quad);
|
|
170
|
-
if (quadRectangle) {
|
|
171
|
-
initialRectangle = scaleRectangle(
|
|
172
|
-
quadRectangle,
|
|
173
|
-
baseWidth,
|
|
174
|
-
baseHeight,
|
|
175
|
-
imageSize.width,
|
|
176
|
-
imageSize.height,
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
cropInitializedRef.current = true;
|
|
182
|
-
setCropRectangle(
|
|
183
|
-
initialRectangle ?? createFullImageRectangle(imageSize.width || 1, imageSize.height || 1),
|
|
184
|
-
);
|
|
185
|
-
}, [capturedDoc, imageSize]);
|
|
186
|
-
|
|
187
|
-
const resetState = useCallback(() => {
|
|
188
|
-
setScreen('scanner');
|
|
189
|
-
setCapturedDoc(null);
|
|
190
|
-
setCropRectangle(null);
|
|
191
|
-
setImageSize(null);
|
|
192
|
-
setProcessing(false);
|
|
193
|
-
manualCapturePending.current = false;
|
|
194
|
-
processingCaptureRef.current = false;
|
|
195
|
-
cropInitializedRef.current = false;
|
|
196
|
-
}, []);
|
|
197
|
-
|
|
198
95
|
const emitError = useCallback(
|
|
199
96
|
(error: Error, fallbackMessage?: string) => {
|
|
200
97
|
console.error('[FullDocScanner] error', error);
|
|
@@ -206,144 +103,63 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
206
103
|
[onError],
|
|
207
104
|
);
|
|
208
105
|
|
|
209
|
-
const
|
|
210
|
-
async (
|
|
211
|
-
console.log('[FullDocScanner] processAutoCapture started');
|
|
212
|
-
manualCapturePending.current = false;
|
|
213
|
-
const normalizedDoc = normalizeCapturedDocument(document);
|
|
214
|
-
const cropManager = NativeModules.CustomCropManager as CustomCropManagerType | undefined;
|
|
215
|
-
|
|
216
|
-
if (!cropManager?.crop) {
|
|
217
|
-
console.error('[FullDocScanner] CustomCropManager.crop is not available');
|
|
218
|
-
emitError(new Error('CustomCropManager.crop is not available'));
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
console.log('[FullDocScanner] Setting processing to true');
|
|
223
|
-
setProcessing(true);
|
|
224
|
-
|
|
106
|
+
const openCropper = useCallback(
|
|
107
|
+
async (imagePath: string) => {
|
|
225
108
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const targetHeight = targetHeightRaw > 0 ? targetHeightRaw : baseHeight || 1;
|
|
238
|
-
|
|
239
|
-
let rectangleBase: Rectangle | null = normalizedDoc.rectangle ?? null;
|
|
240
|
-
if (!rectangleBase && normalizedDoc.quad && normalizedDoc.quad.length === 4) {
|
|
241
|
-
rectangleBase = quadToRectangle(normalizedDoc.quad as Quad);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const scaledRectangle = rectangleBase
|
|
245
|
-
? scaleRectangle(
|
|
246
|
-
rectangleBase,
|
|
247
|
-
baseWidth || targetWidth,
|
|
248
|
-
baseHeight || targetHeight,
|
|
249
|
-
targetWidth,
|
|
250
|
-
targetHeight,
|
|
251
|
-
)
|
|
252
|
-
: null;
|
|
253
|
-
|
|
254
|
-
const rectangleToUse = scaledRectangle ?? createFullImageRectangle(targetWidth, targetHeight);
|
|
255
|
-
|
|
256
|
-
console.log('[FullDocScanner] Calling CustomCropManager.crop with:', {
|
|
257
|
-
rectangle: rectangleToUse,
|
|
258
|
-
imageUri: ensureFileUri(normalizedDoc.path),
|
|
259
|
-
targetSize: { width: targetWidth, height: targetHeight },
|
|
109
|
+
setProcessing(true);
|
|
110
|
+
const croppedImage = await ImageCropPicker.openCropper({
|
|
111
|
+
path: imagePath,
|
|
112
|
+
mediaType: 'photo',
|
|
113
|
+
width: cropWidth,
|
|
114
|
+
height: cropHeight,
|
|
115
|
+
cropping: true,
|
|
116
|
+
cropperToolbarTitle: 'Crop Document',
|
|
117
|
+
freeStyleCropEnabled: true,
|
|
118
|
+
includeBase64: true,
|
|
119
|
+
compressImageQuality: 0.9,
|
|
260
120
|
});
|
|
261
121
|
|
|
262
|
-
|
|
263
|
-
cropManager.crop(
|
|
264
|
-
{
|
|
265
|
-
topLeft: rectangleToUse.topLeft,
|
|
266
|
-
topRight: rectangleToUse.topRight,
|
|
267
|
-
bottomRight: rectangleToUse.bottomRight,
|
|
268
|
-
bottomLeft: rectangleToUse.bottomLeft,
|
|
269
|
-
width: targetWidth,
|
|
270
|
-
height: targetHeight,
|
|
271
|
-
},
|
|
272
|
-
ensureFileUri(normalizedDoc.path),
|
|
273
|
-
(error: unknown, result: { image: string }) => {
|
|
274
|
-
if (error) {
|
|
275
|
-
console.error('[FullDocScanner] CustomCropManager.crop error:', error);
|
|
276
|
-
reject(error instanceof Error ? error : new Error('Crop failed'));
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
console.log('[FullDocScanner] CustomCropManager.crop success, base64 length:', result.image?.length);
|
|
280
|
-
resolve(result.image);
|
|
281
|
-
},
|
|
282
|
-
);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
const finalDoc: CapturedDocument = {
|
|
286
|
-
...normalizedDoc,
|
|
287
|
-
rectangle: rectangleToUse,
|
|
288
|
-
};
|
|
122
|
+
setProcessing(false);
|
|
289
123
|
|
|
290
|
-
console.log('[FullDocScanner] Calling onResult with base64 length:', base64?.length);
|
|
291
124
|
onResult({
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
base64,
|
|
125
|
+
path: croppedImage.path,
|
|
126
|
+
base64: croppedImage.data ?? undefined,
|
|
295
127
|
});
|
|
296
|
-
|
|
297
|
-
console.log('[FullDocScanner] Resetting state');
|
|
298
|
-
resetState();
|
|
299
128
|
} catch (error) {
|
|
300
129
|
setProcessing(false);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
130
|
+
if ((error as any)?.message !== 'User cancelled image selection') {
|
|
131
|
+
emitError(
|
|
132
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
133
|
+
'Failed to crop image.',
|
|
134
|
+
);
|
|
135
|
+
}
|
|
304
136
|
}
|
|
305
137
|
},
|
|
306
|
-
[emitError, onResult
|
|
138
|
+
[cropWidth, cropHeight, emitError, onResult],
|
|
307
139
|
);
|
|
308
140
|
|
|
309
141
|
const handleCapture = useCallback(
|
|
310
|
-
(document: DocScannerCapture) => {
|
|
142
|
+
async (document: DocScannerCapture) => {
|
|
311
143
|
console.log('[FullDocScanner] handleCapture called:', {
|
|
312
144
|
origin: document.origin,
|
|
313
145
|
path: document.path,
|
|
314
|
-
width: document.width,
|
|
315
|
-
height: document.height,
|
|
316
|
-
hasQuad: !!document.quad,
|
|
317
|
-
hasRectangle: !!document.rectangle,
|
|
318
146
|
});
|
|
319
147
|
|
|
320
|
-
if (
|
|
321
|
-
|
|
322
|
-
return;
|
|
148
|
+
if (manualCapturePending.current) {
|
|
149
|
+
manualCapturePending.current = false;
|
|
323
150
|
}
|
|
324
151
|
|
|
325
152
|
const normalizedDoc = normalizeCapturedDocument(document);
|
|
326
153
|
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
manualCapturePending.current = false;
|
|
330
|
-
processingCaptureRef.current = false;
|
|
331
|
-
cropInitializedRef.current = false;
|
|
332
|
-
setCapturedDoc(normalizedDoc);
|
|
333
|
-
setImageSize(null);
|
|
334
|
-
setCropRectangle(null);
|
|
335
|
-
setScreen('crop');
|
|
154
|
+
// Open cropper with the captured image
|
|
155
|
+
await openCropper(normalizedDoc.path);
|
|
336
156
|
},
|
|
337
|
-
[],
|
|
157
|
+
[openCropper],
|
|
338
158
|
);
|
|
339
159
|
|
|
340
|
-
const handleCropChange = useCallback((rectangle: Rectangle) => {
|
|
341
|
-
setCropRectangle(rectangle);
|
|
342
|
-
}, []);
|
|
343
|
-
|
|
344
160
|
const triggerManualCapture = useCallback(() => {
|
|
345
161
|
console.log('[FullDocScanner] triggerManualCapture called');
|
|
346
|
-
if (
|
|
162
|
+
if (processing) {
|
|
347
163
|
console.log('[FullDocScanner] Already processing, skipping manual capture');
|
|
348
164
|
return;
|
|
349
165
|
}
|
|
@@ -356,7 +172,6 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
356
172
|
manualCapturePending.current = true;
|
|
357
173
|
|
|
358
174
|
const capturePromise = docScannerRef.current?.capture();
|
|
359
|
-
console.log('[FullDocScanner] capturePromise:', !!capturePromise);
|
|
360
175
|
if (capturePromise && typeof capturePromise.then === 'function') {
|
|
361
176
|
capturePromise
|
|
362
177
|
.then(() => {
|
|
@@ -370,185 +185,108 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
370
185
|
console.warn('[FullDocScanner] No capture promise returned');
|
|
371
186
|
manualCapturePending.current = false;
|
|
372
187
|
}
|
|
373
|
-
}, []);
|
|
374
|
-
|
|
375
|
-
const performCrop = useCallback(async (): Promise<{ base64: string; rectangle: Rectangle }> => {
|
|
376
|
-
if (!capturedDoc) {
|
|
377
|
-
throw new Error('No captured document to crop');
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const size = imageSize ?? { width: capturedDoc.width, height: capturedDoc.height };
|
|
381
|
-
const cropManager = NativeModules.CustomCropManager as CustomCropManagerType | undefined;
|
|
188
|
+
}, [processing]);
|
|
382
189
|
|
|
383
|
-
|
|
384
|
-
|
|
190
|
+
const handleGalleryPick = useCallback(async () => {
|
|
191
|
+
console.log('[FullDocScanner] handleGalleryPick called');
|
|
192
|
+
if (processing) {
|
|
193
|
+
return;
|
|
385
194
|
}
|
|
386
195
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
196
|
+
try {
|
|
197
|
+
const result = await launchImageLibrary({
|
|
198
|
+
mediaType: 'photo',
|
|
199
|
+
quality: 1,
|
|
200
|
+
selectionLimit: 1,
|
|
201
|
+
});
|
|
393
202
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
baseWidth || targetWidth,
|
|
398
|
-
baseHeight || targetHeight,
|
|
399
|
-
targetWidth,
|
|
400
|
-
targetHeight,
|
|
401
|
-
);
|
|
402
|
-
} else if (capturedDoc.quad && capturedDoc.quad.length === 4) {
|
|
403
|
-
const quadRectangle = quadToRectangle(capturedDoc.quad as Quad);
|
|
404
|
-
if (quadRectangle) {
|
|
405
|
-
fallbackRectangle = scaleRectangle(
|
|
406
|
-
quadRectangle,
|
|
407
|
-
baseWidth || targetWidth,
|
|
408
|
-
baseHeight || targetHeight,
|
|
409
|
-
targetWidth,
|
|
410
|
-
targetHeight,
|
|
411
|
-
);
|
|
203
|
+
if (result.didCancel || !result.assets?.[0]?.uri) {
|
|
204
|
+
console.log('[FullDocScanner] User cancelled gallery picker');
|
|
205
|
+
return;
|
|
412
206
|
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const rectangleToUse = cropRectangle ?? fallbackRectangle ?? createFullImageRectangle(targetWidth, targetHeight);
|
|
416
207
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
{
|
|
420
|
-
topLeft: rectangleToUse.topLeft,
|
|
421
|
-
topRight: rectangleToUse.topRight,
|
|
422
|
-
bottomRight: rectangleToUse.bottomRight,
|
|
423
|
-
bottomLeft: rectangleToUse.bottomLeft,
|
|
424
|
-
width: targetWidth,
|
|
425
|
-
height: targetHeight,
|
|
426
|
-
},
|
|
427
|
-
ensureFileUri(capturedDoc.path),
|
|
428
|
-
(error: unknown, result: { image: string }) => {
|
|
429
|
-
if (error) {
|
|
430
|
-
reject(error instanceof Error ? error : new Error('Crop failed'));
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
resolve(result.image);
|
|
435
|
-
},
|
|
436
|
-
);
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
return { base64, rectangle: rectangleToUse };
|
|
440
|
-
}, [capturedDoc, cropRectangle, imageSize]);
|
|
441
|
-
|
|
442
|
-
const handleConfirm = useCallback(async () => {
|
|
443
|
-
if (!capturedDoc) {
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
208
|
+
const imageUri = result.assets[0].uri;
|
|
209
|
+
console.log('[FullDocScanner] Gallery image selected:', imageUri);
|
|
446
210
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const { base64, rectangle } = await performCrop();
|
|
450
|
-
setProcessing(false);
|
|
451
|
-
const finalDoc: CapturedDocument = {
|
|
452
|
-
...capturedDoc,
|
|
453
|
-
rectangle,
|
|
454
|
-
};
|
|
455
|
-
onResult({
|
|
456
|
-
original: finalDoc,
|
|
457
|
-
rectangle,
|
|
458
|
-
base64,
|
|
459
|
-
});
|
|
460
|
-
resetState();
|
|
211
|
+
// Open cropper with the selected image
|
|
212
|
+
await openCropper(imageUri);
|
|
461
213
|
} catch (error) {
|
|
462
|
-
|
|
463
|
-
|
|
214
|
+
emitError(
|
|
215
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
216
|
+
'Failed to pick image from gallery.',
|
|
217
|
+
);
|
|
464
218
|
}
|
|
465
|
-
}, [
|
|
466
|
-
|
|
467
|
-
const handleRetake = useCallback(() => {
|
|
468
|
-
resetState();
|
|
469
|
-
}, [resetState]);
|
|
219
|
+
}, [processing, openCropper, emitError]);
|
|
470
220
|
|
|
471
221
|
const handleClose = useCallback(() => {
|
|
472
|
-
resetState();
|
|
473
222
|
onClose?.();
|
|
474
|
-
}, [onClose
|
|
223
|
+
}, [onClose]);
|
|
475
224
|
|
|
476
225
|
return (
|
|
477
226
|
<View style={styles.container}>
|
|
478
|
-
{
|
|
479
|
-
<
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
>
|
|
492
|
-
<
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
>
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
{
|
|
503
|
-
<View style={styles.
|
|
504
|
-
|
|
505
|
-
{mergedStrings.captureHint
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
{mergedStrings.manualHint
|
|
509
|
-
|
|
510
|
-
)}
|
|
511
|
-
</View>
|
|
227
|
+
<View style={styles.flex}>
|
|
228
|
+
<DocScanner
|
|
229
|
+
ref={docScannerRef}
|
|
230
|
+
autoCapture={!manualCapture}
|
|
231
|
+
overlayColor={overlayColor}
|
|
232
|
+
showGrid={showGrid}
|
|
233
|
+
gridColor={resolvedGridColor}
|
|
234
|
+
gridLineWidth={gridLineWidth}
|
|
235
|
+
minStableFrames={minStableFrames ?? 6}
|
|
236
|
+
detectionConfig={detectionConfig}
|
|
237
|
+
onCapture={handleCapture}
|
|
238
|
+
showManualCaptureButton={false}
|
|
239
|
+
>
|
|
240
|
+
<View style={styles.overlayTop} pointerEvents="box-none">
|
|
241
|
+
<TouchableOpacity
|
|
242
|
+
style={styles.closeButton}
|
|
243
|
+
onPress={handleClose}
|
|
244
|
+
accessibilityLabel={mergedStrings.cancel}
|
|
245
|
+
accessibilityRole="button"
|
|
246
|
+
>
|
|
247
|
+
<Text style={styles.closeButtonLabel}>×</Text>
|
|
248
|
+
</TouchableOpacity>
|
|
249
|
+
</View>
|
|
250
|
+
{(mergedStrings.captureHint || mergedStrings.manualHint) && (
|
|
251
|
+
<View style={styles.instructionsContainer} pointerEvents="none">
|
|
252
|
+
<View style={styles.instructions}>
|
|
253
|
+
{mergedStrings.captureHint && (
|
|
254
|
+
<Text style={styles.captureText}>{mergedStrings.captureHint}</Text>
|
|
255
|
+
)}
|
|
256
|
+
{mergedStrings.manualHint && (
|
|
257
|
+
<Text style={styles.captureText}>{mergedStrings.manualHint}</Text>
|
|
258
|
+
)}
|
|
512
259
|
</View>
|
|
513
|
-
|
|
514
|
-
|
|
260
|
+
</View>
|
|
261
|
+
)}
|
|
262
|
+
<View style={styles.shutterContainer} pointerEvents="box-none">
|
|
263
|
+
{enableGallery && (
|
|
515
264
|
<TouchableOpacity
|
|
516
|
-
style={[styles.
|
|
517
|
-
onPress={
|
|
265
|
+
style={[styles.galleryButton, processing && styles.buttonDisabled]}
|
|
266
|
+
onPress={handleGalleryPick}
|
|
518
267
|
disabled={processing}
|
|
519
|
-
accessibilityLabel={mergedStrings.
|
|
268
|
+
accessibilityLabel={mergedStrings.galleryButton}
|
|
520
269
|
accessibilityRole="button"
|
|
521
270
|
>
|
|
522
|
-
<
|
|
271
|
+
<Text style={styles.galleryButtonText}>📁</Text>
|
|
523
272
|
</TouchableOpacity>
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
overlayColor="rgba(0,0,0,0.6)"
|
|
534
|
-
overlayStrokeColor={overlayStrokeColor}
|
|
535
|
-
handlerColor={handlerColor}
|
|
536
|
-
onCropChange={handleCropChange}
|
|
537
|
-
/>
|
|
538
|
-
<View style={styles.cropFooter}>
|
|
539
|
-
<TouchableOpacity style={[styles.actionButton, styles.secondaryButton]} onPress={handleRetake}>
|
|
540
|
-
{mergedStrings.retake && <Text style={styles.buttonText}>{mergedStrings.retake}</Text>}
|
|
541
|
-
</TouchableOpacity>
|
|
542
|
-
<TouchableOpacity style={[styles.actionButton, styles.primaryButton]} onPress={handleConfirm}>
|
|
543
|
-
{mergedStrings.confirm && <Text style={styles.buttonText}>{mergedStrings.confirm}</Text>}
|
|
273
|
+
)}
|
|
274
|
+
<TouchableOpacity
|
|
275
|
+
style={[styles.shutterButton, processing && styles.buttonDisabled]}
|
|
276
|
+
onPress={triggerManualCapture}
|
|
277
|
+
disabled={processing}
|
|
278
|
+
accessibilityLabel={mergedStrings.manualHint}
|
|
279
|
+
accessibilityRole="button"
|
|
280
|
+
>
|
|
281
|
+
<View style={styles.shutterInner} />
|
|
544
282
|
</TouchableOpacity>
|
|
545
283
|
</View>
|
|
546
|
-
</
|
|
547
|
-
|
|
284
|
+
</DocScanner>
|
|
285
|
+
</View>
|
|
548
286
|
|
|
549
287
|
{processing && (
|
|
550
288
|
<View style={styles.processingOverlay}>
|
|
551
|
-
<ActivityIndicator size="large" color={
|
|
289
|
+
<ActivityIndicator size="large" color={overlayColor} />
|
|
552
290
|
{mergedStrings.processing && (
|
|
553
291
|
<Text style={styles.processingText}>{mergedStrings.processing}</Text>
|
|
554
292
|
)}
|
|
@@ -585,7 +323,10 @@ const styles = StyleSheet.create({
|
|
|
585
323
|
bottom: 64,
|
|
586
324
|
left: 0,
|
|
587
325
|
right: 0,
|
|
326
|
+
flexDirection: 'row',
|
|
327
|
+
justifyContent: 'center',
|
|
588
328
|
alignItems: 'center',
|
|
329
|
+
gap: 24,
|
|
589
330
|
zIndex: 10,
|
|
590
331
|
},
|
|
591
332
|
closeButton: {
|
|
@@ -613,6 +354,19 @@ const styles = StyleSheet.create({
|
|
|
613
354
|
fontSize: 15,
|
|
614
355
|
textAlign: 'center',
|
|
615
356
|
},
|
|
357
|
+
galleryButton: {
|
|
358
|
+
width: 60,
|
|
359
|
+
height: 60,
|
|
360
|
+
borderRadius: 30,
|
|
361
|
+
borderWidth: 3,
|
|
362
|
+
borderColor: '#fff',
|
|
363
|
+
justifyContent: 'center',
|
|
364
|
+
alignItems: 'center',
|
|
365
|
+
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
366
|
+
},
|
|
367
|
+
galleryButtonText: {
|
|
368
|
+
fontSize: 28,
|
|
369
|
+
},
|
|
616
370
|
shutterButton: {
|
|
617
371
|
width: 80,
|
|
618
372
|
height: 80,
|
|
@@ -623,7 +377,7 @@ const styles = StyleSheet.create({
|
|
|
623
377
|
alignItems: 'center',
|
|
624
378
|
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
625
379
|
},
|
|
626
|
-
|
|
380
|
+
buttonDisabled: {
|
|
627
381
|
opacity: 0.4,
|
|
628
382
|
},
|
|
629
383
|
shutterInner: {
|
|
@@ -632,34 +386,6 @@ const styles = StyleSheet.create({
|
|
|
632
386
|
borderRadius: 30,
|
|
633
387
|
backgroundColor: '#fff',
|
|
634
388
|
},
|
|
635
|
-
cropFooter: {
|
|
636
|
-
position: 'absolute',
|
|
637
|
-
bottom: 40,
|
|
638
|
-
left: 20,
|
|
639
|
-
right: 20,
|
|
640
|
-
flexDirection: 'row',
|
|
641
|
-
justifyContent: 'space-between',
|
|
642
|
-
},
|
|
643
|
-
actionButton: {
|
|
644
|
-
flex: 1,
|
|
645
|
-
paddingVertical: 14,
|
|
646
|
-
borderRadius: 12,
|
|
647
|
-
alignItems: 'center',
|
|
648
|
-
marginHorizontal: 6,
|
|
649
|
-
},
|
|
650
|
-
secondaryButton: {
|
|
651
|
-
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
652
|
-
borderWidth: 1,
|
|
653
|
-
borderColor: 'rgba(255,255,255,0.35)',
|
|
654
|
-
},
|
|
655
|
-
primaryButton: {
|
|
656
|
-
backgroundColor: '#3170f3',
|
|
657
|
-
},
|
|
658
|
-
buttonText: {
|
|
659
|
-
color: '#fff',
|
|
660
|
-
fontSize: 16,
|
|
661
|
-
fontWeight: '600',
|
|
662
|
-
},
|
|
663
389
|
processingOverlay: {
|
|
664
390
|
...StyleSheet.absoluteFillObject,
|
|
665
391
|
backgroundColor: 'rgba(0,0,0,0.65)',
|