react-native-rectangle-doc-scanner 3.77.0 → 3.78.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/FullDocScanner.js
CHANGED
|
@@ -43,6 +43,56 @@ const react_native_image_picker_1 = require("react-native-image-picker");
|
|
|
43
43
|
const react_native_image_crop_picker_1 = __importDefault(require("react-native-image-crop-picker"));
|
|
44
44
|
const DocScanner_1 = require("./DocScanner");
|
|
45
45
|
const stripFileUri = (value) => value.replace(/^file:\/\//, '');
|
|
46
|
+
const CROPPER_TIMEOUT_MS = 8000;
|
|
47
|
+
const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
|
|
48
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
49
|
+
const runAfterInteractions = () => new Promise((resolve) => react_native_1.InteractionManager.runAfterInteractions(() => resolve()));
|
|
50
|
+
// Allow native pickers to finish their dismissal animations before presenting the cropper.
|
|
51
|
+
const waitForModalDismissal = async () => {
|
|
52
|
+
await delay(50);
|
|
53
|
+
await runAfterInteractions();
|
|
54
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
55
|
+
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
56
|
+
}
|
|
57
|
+
await delay(180);
|
|
58
|
+
await runAfterInteractions();
|
|
59
|
+
};
|
|
60
|
+
// Guard the native cropper promise so we can recover if it never resolves.
|
|
61
|
+
async function withTimeout(factory) {
|
|
62
|
+
let timeoutId;
|
|
63
|
+
let finished = false;
|
|
64
|
+
const promise = factory()
|
|
65
|
+
.then((value) => {
|
|
66
|
+
finished = true;
|
|
67
|
+
return value;
|
|
68
|
+
})
|
|
69
|
+
.catch((error) => {
|
|
70
|
+
finished = true;
|
|
71
|
+
throw error;
|
|
72
|
+
});
|
|
73
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
74
|
+
timeoutId = setTimeout(() => {
|
|
75
|
+
if (!finished) {
|
|
76
|
+
const timeoutError = new Error(CROPPER_TIMEOUT_CODE);
|
|
77
|
+
timeoutError.code = CROPPER_TIMEOUT_CODE;
|
|
78
|
+
reject(timeoutError);
|
|
79
|
+
}
|
|
80
|
+
}, CROPPER_TIMEOUT_MS);
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
if (timeoutId) {
|
|
87
|
+
clearTimeout(timeoutId);
|
|
88
|
+
}
|
|
89
|
+
if (!finished) {
|
|
90
|
+
promise.catch(() => {
|
|
91
|
+
console.warn('[FullDocScanner] Cropper promise settled after timeout');
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
46
96
|
const normalizeCapturedDocument = (document) => {
|
|
47
97
|
const { origin: _origin, ...rest } = document;
|
|
48
98
|
const normalizedPath = stripFileUri(document.initialPath ?? document.path);
|
|
@@ -81,7 +131,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
81
131
|
react_native_1.Alert.alert('Document Scanner', fallbackMessage);
|
|
82
132
|
}
|
|
83
133
|
}, [onError]);
|
|
84
|
-
const openCropper = (0, react_1.useCallback)(async (imagePath) => {
|
|
134
|
+
const openCropper = (0, react_1.useCallback)(async (imagePath, options) => {
|
|
85
135
|
try {
|
|
86
136
|
console.log('[FullDocScanner] openCropper called with path:', imagePath);
|
|
87
137
|
setProcessing(true);
|
|
@@ -92,7 +142,11 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
92
142
|
cleanPath = cleanPath.replace('file://', '');
|
|
93
143
|
}
|
|
94
144
|
console.log('[FullDocScanner] Clean path for cropper:', cleanPath);
|
|
95
|
-
const
|
|
145
|
+
const shouldWaitForPickerDismissal = options?.waitForPickerDismissal ?? true;
|
|
146
|
+
if (shouldWaitForPickerDismissal) {
|
|
147
|
+
await waitForModalDismissal();
|
|
148
|
+
}
|
|
149
|
+
const croppedImage = await withTimeout(() => react_native_image_crop_picker_1.default.openCropper({
|
|
96
150
|
path: cleanPath,
|
|
97
151
|
mediaType: 'photo',
|
|
98
152
|
width: cropWidth,
|
|
@@ -102,7 +156,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
102
156
|
freeStyleCropEnabled: true,
|
|
103
157
|
includeBase64: true,
|
|
104
158
|
compressImageQuality: 0.9,
|
|
105
|
-
});
|
|
159
|
+
}));
|
|
106
160
|
console.log('[FullDocScanner] Cropper returned:', {
|
|
107
161
|
path: croppedImage.path,
|
|
108
162
|
hasBase64: !!croppedImage.data,
|
|
@@ -117,8 +171,14 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
117
171
|
catch (error) {
|
|
118
172
|
console.error('[FullDocScanner] openCropper error:', error);
|
|
119
173
|
setProcessing(false);
|
|
174
|
+
const errorCode = error?.code;
|
|
120
175
|
const errorMessage = error?.message || String(error);
|
|
121
|
-
if (
|
|
176
|
+
if (errorCode === CROPPER_TIMEOUT_CODE || errorMessage === CROPPER_TIMEOUT_CODE) {
|
|
177
|
+
console.error('[FullDocScanner] Cropper timed out waiting for presentation');
|
|
178
|
+
emitError(error instanceof Error ? error : new Error('Cropper timed out'), 'Failed to open crop editor. Please try again.');
|
|
179
|
+
}
|
|
180
|
+
else if (errorCode === 'E_PICKER_CANCELLED' ||
|
|
181
|
+
errorMessage === 'User cancelled image selection' ||
|
|
122
182
|
errorMessage.includes('cancelled') ||
|
|
123
183
|
errorMessage.includes('cancel')) {
|
|
124
184
|
console.log('[FullDocScanner] User cancelled cropper');
|
|
@@ -148,7 +208,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
148
208
|
const normalizedDoc = normalizeCapturedDocument(document);
|
|
149
209
|
if (captureMode === 'no-grid') {
|
|
150
210
|
console.log('[FullDocScanner] No grid at capture button press: opening cropper for manual selection');
|
|
151
|
-
await openCropper(normalizedDoc.path);
|
|
211
|
+
await openCropper(normalizedDoc.path, { waitForPickerDismissal: false });
|
|
152
212
|
return;
|
|
153
213
|
}
|
|
154
214
|
if (normalizedDoc.croppedPath) {
|
|
@@ -159,7 +219,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
159
219
|
return;
|
|
160
220
|
}
|
|
161
221
|
console.log('[FullDocScanner] Fallback to manual crop (no croppedPath available)');
|
|
162
|
-
await openCropper(normalizedDoc.path);
|
|
222
|
+
await openCropper(normalizedDoc.path, { waitForPickerDismissal: false });
|
|
163
223
|
}, [openCropper]);
|
|
164
224
|
const triggerManualCapture = (0, react_1.useCallback)(() => {
|
|
165
225
|
const scannerInstance = docScannerRef.current;
|
|
@@ -244,9 +304,6 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
244
304
|
}
|
|
245
305
|
const imageUri = result.assets[0].uri;
|
|
246
306
|
console.log('[FullDocScanner] Gallery image selected:', imageUri);
|
|
247
|
-
// Defer cropper presentation until picker dismissal finishes to avoid hierarchy errors
|
|
248
|
-
await new Promise((resolve) => react_native_1.InteractionManager.runAfterInteractions(() => resolve()));
|
|
249
|
-
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
250
307
|
await openCropper(imageUri);
|
|
251
308
|
}
|
|
252
309
|
catch (error) {
|
package/package.json
CHANGED
package/src/FullDocScanner.tsx
CHANGED
|
@@ -22,6 +22,68 @@ import type {
|
|
|
22
22
|
|
|
23
23
|
const stripFileUri = (value: string) => value.replace(/^file:\/\//, '');
|
|
24
24
|
|
|
25
|
+
const CROPPER_TIMEOUT_MS = 8000;
|
|
26
|
+
const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
|
|
27
|
+
|
|
28
|
+
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
29
|
+
|
|
30
|
+
const runAfterInteractions = () =>
|
|
31
|
+
new Promise<void>((resolve) => InteractionManager.runAfterInteractions(() => resolve()));
|
|
32
|
+
|
|
33
|
+
// Allow native pickers to finish their dismissal animations before presenting the cropper.
|
|
34
|
+
const waitForModalDismissal = async () => {
|
|
35
|
+
await delay(50);
|
|
36
|
+
await runAfterInteractions();
|
|
37
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
38
|
+
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
39
|
+
}
|
|
40
|
+
await delay(180);
|
|
41
|
+
await runAfterInteractions();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Guard the native cropper promise so we can recover if it never resolves.
|
|
45
|
+
async function withTimeout<T>(factory: () => Promise<T>): Promise<T> {
|
|
46
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
47
|
+
let finished = false;
|
|
48
|
+
|
|
49
|
+
const promise = factory()
|
|
50
|
+
.then((value) => {
|
|
51
|
+
finished = true;
|
|
52
|
+
return value;
|
|
53
|
+
})
|
|
54
|
+
.catch((error) => {
|
|
55
|
+
finished = true;
|
|
56
|
+
throw error;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const timeoutPromise = new Promise<T>((_, reject) => {
|
|
60
|
+
timeoutId = setTimeout(() => {
|
|
61
|
+
if (!finished) {
|
|
62
|
+
const timeoutError = new Error(CROPPER_TIMEOUT_CODE);
|
|
63
|
+
(timeoutError as any).code = CROPPER_TIMEOUT_CODE;
|
|
64
|
+
reject(timeoutError);
|
|
65
|
+
}
|
|
66
|
+
}, CROPPER_TIMEOUT_MS);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
71
|
+
} finally {
|
|
72
|
+
if (timeoutId) {
|
|
73
|
+
clearTimeout(timeoutId);
|
|
74
|
+
}
|
|
75
|
+
if (!finished) {
|
|
76
|
+
promise.catch(() => {
|
|
77
|
+
console.warn('[FullDocScanner] Cropper promise settled after timeout');
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type OpenCropperOptions = {
|
|
84
|
+
waitForPickerDismissal?: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
25
87
|
const normalizeCapturedDocument = (document: DocScannerCapture): CapturedDocument => {
|
|
26
88
|
const { origin: _origin, ...rest } = document;
|
|
27
89
|
const normalizedPath = stripFileUri(document.initialPath ?? document.path);
|
|
@@ -120,7 +182,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
120
182
|
);
|
|
121
183
|
|
|
122
184
|
const openCropper = useCallback(
|
|
123
|
-
async (imagePath: string) => {
|
|
185
|
+
async (imagePath: string, options?: OpenCropperOptions) => {
|
|
124
186
|
try {
|
|
125
187
|
console.log('[FullDocScanner] openCropper called with path:', imagePath);
|
|
126
188
|
setProcessing(true);
|
|
@@ -133,17 +195,25 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
133
195
|
}
|
|
134
196
|
console.log('[FullDocScanner] Clean path for cropper:', cleanPath);
|
|
135
197
|
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
198
|
+
const shouldWaitForPickerDismissal = options?.waitForPickerDismissal ?? true;
|
|
199
|
+
|
|
200
|
+
if (shouldWaitForPickerDismissal) {
|
|
201
|
+
await waitForModalDismissal();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const croppedImage = await withTimeout(() =>
|
|
205
|
+
ImageCropPicker.openCropper({
|
|
206
|
+
path: cleanPath,
|
|
207
|
+
mediaType: 'photo',
|
|
208
|
+
width: cropWidth,
|
|
209
|
+
height: cropHeight,
|
|
210
|
+
cropping: true,
|
|
211
|
+
cropperToolbarTitle: 'Crop Document',
|
|
212
|
+
freeStyleCropEnabled: true,
|
|
213
|
+
includeBase64: true,
|
|
214
|
+
compressImageQuality: 0.9,
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
147
217
|
|
|
148
218
|
console.log('[FullDocScanner] Cropper returned:', {
|
|
149
219
|
path: croppedImage.path,
|
|
@@ -161,11 +231,21 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
161
231
|
console.error('[FullDocScanner] openCropper error:', error);
|
|
162
232
|
setProcessing(false);
|
|
163
233
|
|
|
234
|
+
const errorCode = (error as any)?.code;
|
|
164
235
|
const errorMessage = (error as any)?.message || String(error);
|
|
165
236
|
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
237
|
+
if (errorCode === CROPPER_TIMEOUT_CODE || errorMessage === CROPPER_TIMEOUT_CODE) {
|
|
238
|
+
console.error('[FullDocScanner] Cropper timed out waiting for presentation');
|
|
239
|
+
emitError(
|
|
240
|
+
error instanceof Error ? error : new Error('Cropper timed out'),
|
|
241
|
+
'Failed to open crop editor. Please try again.',
|
|
242
|
+
);
|
|
243
|
+
} else if (
|
|
244
|
+
errorCode === 'E_PICKER_CANCELLED' ||
|
|
245
|
+
errorMessage === 'User cancelled image selection' ||
|
|
246
|
+
errorMessage.includes('cancelled') ||
|
|
247
|
+
errorMessage.includes('cancel')
|
|
248
|
+
) {
|
|
169
249
|
console.log('[FullDocScanner] User cancelled cropper');
|
|
170
250
|
} else {
|
|
171
251
|
emitError(
|
|
@@ -204,7 +284,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
204
284
|
|
|
205
285
|
if (captureMode === 'no-grid') {
|
|
206
286
|
console.log('[FullDocScanner] No grid at capture button press: opening cropper for manual selection');
|
|
207
|
-
await openCropper(normalizedDoc.path);
|
|
287
|
+
await openCropper(normalizedDoc.path, { waitForPickerDismissal: false });
|
|
208
288
|
return;
|
|
209
289
|
}
|
|
210
290
|
|
|
@@ -217,7 +297,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
217
297
|
}
|
|
218
298
|
|
|
219
299
|
console.log('[FullDocScanner] Fallback to manual crop (no croppedPath available)');
|
|
220
|
-
await openCropper(normalizedDoc.path);
|
|
300
|
+
await openCropper(normalizedDoc.path, { waitForPickerDismissal: false });
|
|
221
301
|
},
|
|
222
302
|
[openCropper],
|
|
223
303
|
);
|
|
@@ -327,12 +407,6 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
327
407
|
const imageUri = result.assets[0].uri;
|
|
328
408
|
console.log('[FullDocScanner] Gallery image selected:', imageUri);
|
|
329
409
|
|
|
330
|
-
// Defer cropper presentation until picker dismissal finishes to avoid hierarchy errors
|
|
331
|
-
await new Promise<void>((resolve) =>
|
|
332
|
-
InteractionManager.runAfterInteractions(() => resolve()),
|
|
333
|
-
);
|
|
334
|
-
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
335
|
-
|
|
336
410
|
await openCropper(imageUri);
|
|
337
411
|
} catch (error) {
|
|
338
412
|
console.error('[FullDocScanner] Gallery pick error:', error);
|
|
@@ -35,14 +35,18 @@ RCT_EXPORT_VIEW_PROPERTY(brightness, float)
|
|
|
35
35
|
RCT_EXPORT_VIEW_PROPERTY(contrast, float)
|
|
36
36
|
|
|
37
37
|
// Main capture method - accept reactTag when available (falls back to cached view)
|
|
38
|
-
RCT_EXPORT_METHOD(capture:(
|
|
38
|
+
RCT_EXPORT_METHOD(capture:(NSNumber * _Nullable)reactTag
|
|
39
39
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
40
40
|
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
41
41
|
NSLog(@"[RNPdfScannerManager] capture called with reactTag: %@", reactTag);
|
|
42
42
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
43
43
|
DocumentScannerView *targetView = nil;
|
|
44
44
|
|
|
45
|
-
if ([reactTag isKindOfClass:[
|
|
45
|
+
if ([reactTag isKindOfClass:[NSNull class]]) {
|
|
46
|
+
reactTag = nil;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (reactTag) {
|
|
46
50
|
NSNumber *resolvedTag = (NSNumber *)reactTag;
|
|
47
51
|
UIView *view = [self.bridge.uiManager viewForReactTag:resolvedTag];
|
|
48
52
|
if ([view isKindOfClass:[DocumentScannerView class]]) {
|
|
@@ -53,8 +57,6 @@ RCT_EXPORT_METHOD(capture:(nullable id)reactTag
|
|
|
53
57
|
} else {
|
|
54
58
|
NSLog(@"[RNPdfScannerManager] No view found for tag %@", resolvedTag);
|
|
55
59
|
}
|
|
56
|
-
} else if (reactTag) {
|
|
57
|
-
NSLog(@"[RNPdfScannerManager] Unexpected reactTag type %@ - ignoring reactTag", NSStringFromClass([reactTag class]));
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
if (!targetView && self->_scannerView) {
|