react-native-rectangle-doc-scanner 3.89.0 → 3.91.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.d.ts +1 -0
- package/dist/FullDocScanner.js +82 -3
- package/package.json +1 -1
- package/src/FullDocScanner.tsx +105 -4
package/dist/FullDocScanner.d.ts
CHANGED
package/dist/FullDocScanner.js
CHANGED
|
@@ -110,6 +110,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
110
110
|
const [rectangleDetected, setRectangleDetected] = (0, react_1.useState)(false);
|
|
111
111
|
const [rectangleHint, setRectangleHint] = (0, react_1.useState)(false);
|
|
112
112
|
const [flashEnabled, setFlashEnabled] = (0, react_1.useState)(false);
|
|
113
|
+
const [isRotating, setIsRotating] = (0, react_1.useState)(false);
|
|
113
114
|
const resolvedGridColor = gridColor ?? overlayColor;
|
|
114
115
|
const docScannerRef = (0, react_1.useRef)(null);
|
|
115
116
|
const captureModeRef = (0, react_1.useRef)(null);
|
|
@@ -124,6 +125,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
124
125
|
galleryButton: strings?.galleryButton,
|
|
125
126
|
retake: strings?.retake ?? 'Retake',
|
|
126
127
|
confirm: strings?.confirm ?? 'Confirm',
|
|
128
|
+
cropTitle: strings?.cropTitle ?? 'Crop Document',
|
|
127
129
|
}), [strings]);
|
|
128
130
|
const emitError = (0, react_1.useCallback)((error, fallbackMessage) => {
|
|
129
131
|
console.error('[FullDocScanner] error', error);
|
|
@@ -153,7 +155,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
153
155
|
width: cropWidth,
|
|
154
156
|
height: cropHeight,
|
|
155
157
|
cropping: true,
|
|
156
|
-
cropperToolbarTitle: 'Crop Document',
|
|
158
|
+
cropperToolbarTitle: mergedStrings.cropTitle || 'Crop Document',
|
|
157
159
|
freeStyleCropEnabled: true,
|
|
158
160
|
includeBase64: true,
|
|
159
161
|
compressImageQuality: 0.9,
|
|
@@ -301,14 +303,18 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
301
303
|
hasAssets: !!result.assets,
|
|
302
304
|
assetsLength: result.assets?.length,
|
|
303
305
|
});
|
|
304
|
-
setIsGalleryOpen(false);
|
|
305
306
|
if (result.didCancel || !result.assets?.[0]?.uri) {
|
|
306
307
|
console.log('[FullDocScanner] User cancelled gallery picker or no image selected');
|
|
308
|
+
setIsGalleryOpen(false);
|
|
307
309
|
return;
|
|
308
310
|
}
|
|
309
311
|
const imageUri = result.assets[0].uri;
|
|
310
312
|
console.log('[FullDocScanner] Gallery image selected:', imageUri);
|
|
311
|
-
//
|
|
313
|
+
// Set gallery closed state immediately but wait for modal to dismiss
|
|
314
|
+
setIsGalleryOpen(false);
|
|
315
|
+
// Wait for the image picker modal to fully dismiss before opening cropper
|
|
316
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
317
|
+
// Now open cropper after picker is dismissed
|
|
312
318
|
await openCropper(imageUri, { waitForPickerDismissal: false });
|
|
313
319
|
}
|
|
314
320
|
catch (error) {
|
|
@@ -323,6 +329,43 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
323
329
|
const handleFlashToggle = (0, react_1.useCallback)(() => {
|
|
324
330
|
setFlashEnabled(prev => !prev);
|
|
325
331
|
}, []);
|
|
332
|
+
const handleRotateImage = (0, react_1.useCallback)(async (degrees) => {
|
|
333
|
+
if (isRotating || !croppedImageData)
|
|
334
|
+
return;
|
|
335
|
+
setIsRotating(true);
|
|
336
|
+
try {
|
|
337
|
+
console.log('[FullDocScanner] Rotating image by', degrees, 'degrees');
|
|
338
|
+
const rotatedImage = await react_native_image_crop_picker_1.default.openCropper({
|
|
339
|
+
path: croppedImageData.path,
|
|
340
|
+
mediaType: 'photo',
|
|
341
|
+
cropping: true,
|
|
342
|
+
freeStyleCropEnabled: true,
|
|
343
|
+
includeBase64: true,
|
|
344
|
+
compressImageQuality: 0.9,
|
|
345
|
+
cropperToolbarTitle: degrees === 90 ? '↻' : '↺',
|
|
346
|
+
cropperChooseText: '완료',
|
|
347
|
+
cropperCancelText: '취소',
|
|
348
|
+
cropperRotateButtonsHidden: false,
|
|
349
|
+
});
|
|
350
|
+
console.log('[FullDocScanner] Image rotated successfully');
|
|
351
|
+
setCroppedImageData({
|
|
352
|
+
path: rotatedImage.path,
|
|
353
|
+
base64: rotatedImage.data ?? undefined,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
console.error('[FullDocScanner] Image rotation error:', error);
|
|
358
|
+
if (error && typeof error === 'object' && 'message' in error) {
|
|
359
|
+
const errorMessage = error.message;
|
|
360
|
+
if (!errorMessage.includes('cancel') && !errorMessage.includes('User cancelled')) {
|
|
361
|
+
emitError(error instanceof Error ? error : new Error(String(error)), 'Failed to rotate image.');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
setIsRotating(false);
|
|
367
|
+
}
|
|
368
|
+
}, [isRotating, croppedImageData, emitError]);
|
|
326
369
|
const handleConfirm = (0, react_1.useCallback)(() => {
|
|
327
370
|
if (croppedImageData) {
|
|
328
371
|
onResult({
|
|
@@ -416,6 +459,13 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
416
459
|
croppedImageData ? (
|
|
417
460
|
// check_DP: Show confirmation screen
|
|
418
461
|
react_1.default.createElement(react_native_1.View, { style: styles.confirmationContainer },
|
|
462
|
+
react_1.default.createElement(react_native_1.View, { style: styles.rotateButtonsTop },
|
|
463
|
+
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.rotateButtonTop, isRotating && styles.buttonDisabled], onPress: () => handleRotateImage(-90), disabled: isRotating, accessibilityLabel: "\uC67C\uCABD\uC73C\uB85C 90\uB3C4 \uD68C\uC804", accessibilityRole: "button" },
|
|
464
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.rotateIconText }, "\u21BA"),
|
|
465
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC88C\uB85C 90\u00B0")),
|
|
466
|
+
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.rotateButtonTop, isRotating && styles.buttonDisabled], onPress: () => handleRotateImage(90), disabled: isRotating, accessibilityLabel: "\uC624\uB978\uCABD\uC73C\uB85C 90\uB3C4 \uD68C\uC804", accessibilityRole: "button" },
|
|
467
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.rotateIconText }, "\u21BB"),
|
|
468
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC6B0\uB85C 90\u00B0"))),
|
|
419
469
|
react_1.default.createElement(react_native_1.Image, { source: { uri: croppedImageData.path }, style: styles.previewImage, resizeMode: "contain" }),
|
|
420
470
|
react_1.default.createElement(react_native_1.View, { style: styles.confirmationButtons },
|
|
421
471
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.retakeButton], onPress: handleRetake, accessibilityLabel: mergedStrings.retake, accessibilityRole: "button" },
|
|
@@ -567,6 +617,35 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
567
617
|
justifyContent: 'center',
|
|
568
618
|
alignItems: 'center',
|
|
569
619
|
},
|
|
620
|
+
rotateButtonsTop: {
|
|
621
|
+
position: 'absolute',
|
|
622
|
+
top: 60,
|
|
623
|
+
left: 20,
|
|
624
|
+
flexDirection: 'row',
|
|
625
|
+
gap: 12,
|
|
626
|
+
zIndex: 10,
|
|
627
|
+
},
|
|
628
|
+
rotateButtonTop: {
|
|
629
|
+
flexDirection: 'row',
|
|
630
|
+
alignItems: 'center',
|
|
631
|
+
backgroundColor: 'rgba(50,50,50,0.8)',
|
|
632
|
+
paddingVertical: 10,
|
|
633
|
+
paddingHorizontal: 16,
|
|
634
|
+
borderRadius: 24,
|
|
635
|
+
borderWidth: 1,
|
|
636
|
+
borderColor: 'rgba(255,255,255,0.3)',
|
|
637
|
+
gap: 6,
|
|
638
|
+
},
|
|
639
|
+
rotateIconText: {
|
|
640
|
+
fontSize: 24,
|
|
641
|
+
color: '#fff',
|
|
642
|
+
fontWeight: 'bold',
|
|
643
|
+
},
|
|
644
|
+
rotateButtonLabel: {
|
|
645
|
+
fontSize: 14,
|
|
646
|
+
color: '#fff',
|
|
647
|
+
fontWeight: '500',
|
|
648
|
+
},
|
|
570
649
|
previewImage: {
|
|
571
650
|
width: '100%',
|
|
572
651
|
height: '80%',
|
package/package.json
CHANGED
package/src/FullDocScanner.tsx
CHANGED
|
@@ -112,6 +112,7 @@ export interface FullDocScannerStrings {
|
|
|
112
112
|
galleryButton?: string;
|
|
113
113
|
retake?: string;
|
|
114
114
|
confirm?: string;
|
|
115
|
+
cropTitle?: string;
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
export interface FullDocScannerProps {
|
|
@@ -151,6 +152,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
151
152
|
const [rectangleDetected, setRectangleDetected] = useState(false);
|
|
152
153
|
const [rectangleHint, setRectangleHint] = useState(false);
|
|
153
154
|
const [flashEnabled, setFlashEnabled] = useState(false);
|
|
155
|
+
const [isRotating, setIsRotating] = useState(false);
|
|
154
156
|
const resolvedGridColor = gridColor ?? overlayColor;
|
|
155
157
|
const docScannerRef = useRef<DocScannerHandle | null>(null);
|
|
156
158
|
const captureModeRef = useRef<'grid' | 'no-grid' | null>(null);
|
|
@@ -167,6 +169,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
167
169
|
galleryButton: strings?.galleryButton,
|
|
168
170
|
retake: strings?.retake ?? 'Retake',
|
|
169
171
|
confirm: strings?.confirm ?? 'Confirm',
|
|
172
|
+
cropTitle: strings?.cropTitle ?? 'Crop Document',
|
|
170
173
|
}),
|
|
171
174
|
[strings],
|
|
172
175
|
);
|
|
@@ -209,7 +212,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
209
212
|
width: cropWidth,
|
|
210
213
|
height: cropHeight,
|
|
211
214
|
cropping: true,
|
|
212
|
-
cropperToolbarTitle: 'Crop Document',
|
|
215
|
+
cropperToolbarTitle: mergedStrings.cropTitle || 'Crop Document',
|
|
213
216
|
freeStyleCropEnabled: true,
|
|
214
217
|
includeBase64: true,
|
|
215
218
|
compressImageQuality: 0.9,
|
|
@@ -402,17 +405,22 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
402
405
|
assetsLength: result.assets?.length,
|
|
403
406
|
});
|
|
404
407
|
|
|
405
|
-
setIsGalleryOpen(false);
|
|
406
|
-
|
|
407
408
|
if (result.didCancel || !result.assets?.[0]?.uri) {
|
|
408
409
|
console.log('[FullDocScanner] User cancelled gallery picker or no image selected');
|
|
410
|
+
setIsGalleryOpen(false);
|
|
409
411
|
return;
|
|
410
412
|
}
|
|
411
413
|
|
|
412
414
|
const imageUri = result.assets[0].uri;
|
|
413
415
|
console.log('[FullDocScanner] Gallery image selected:', imageUri);
|
|
414
416
|
|
|
415
|
-
//
|
|
417
|
+
// Set gallery closed state immediately but wait for modal to dismiss
|
|
418
|
+
setIsGalleryOpen(false);
|
|
419
|
+
|
|
420
|
+
// Wait for the image picker modal to fully dismiss before opening cropper
|
|
421
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
422
|
+
|
|
423
|
+
// Now open cropper after picker is dismissed
|
|
416
424
|
await openCropper(imageUri, { waitForPickerDismissal: false });
|
|
417
425
|
} catch (error) {
|
|
418
426
|
console.error('[FullDocScanner] Gallery pick error:', error);
|
|
@@ -432,6 +440,47 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
432
440
|
setFlashEnabled(prev => !prev);
|
|
433
441
|
}, []);
|
|
434
442
|
|
|
443
|
+
const handleRotateImage = useCallback(async (degrees: -90 | 90) => {
|
|
444
|
+
if (isRotating || !croppedImageData) return;
|
|
445
|
+
|
|
446
|
+
setIsRotating(true);
|
|
447
|
+
try {
|
|
448
|
+
console.log('[FullDocScanner] Rotating image by', degrees, 'degrees');
|
|
449
|
+
|
|
450
|
+
const rotatedImage = await ImageCropPicker.openCropper({
|
|
451
|
+
path: croppedImageData.path,
|
|
452
|
+
mediaType: 'photo',
|
|
453
|
+
cropping: true,
|
|
454
|
+
freeStyleCropEnabled: true,
|
|
455
|
+
includeBase64: true,
|
|
456
|
+
compressImageQuality: 0.9,
|
|
457
|
+
cropperToolbarTitle: degrees === 90 ? '↻' : '↺',
|
|
458
|
+
cropperChooseText: '완료',
|
|
459
|
+
cropperCancelText: '취소',
|
|
460
|
+
cropperRotateButtonsHidden: false,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
console.log('[FullDocScanner] Image rotated successfully');
|
|
464
|
+
setCroppedImageData({
|
|
465
|
+
path: rotatedImage.path,
|
|
466
|
+
base64: rotatedImage.data ?? undefined,
|
|
467
|
+
});
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.error('[FullDocScanner] Image rotation error:', error);
|
|
470
|
+
if (error && typeof error === 'object' && 'message' in error) {
|
|
471
|
+
const errorMessage = (error as Error).message;
|
|
472
|
+
if (!errorMessage.includes('cancel') && !errorMessage.includes('User cancelled')) {
|
|
473
|
+
emitError(
|
|
474
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
475
|
+
'Failed to rotate image.',
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
} finally {
|
|
480
|
+
setIsRotating(false);
|
|
481
|
+
}
|
|
482
|
+
}, [isRotating, croppedImageData, emitError]);
|
|
483
|
+
|
|
435
484
|
const handleConfirm = useCallback(() => {
|
|
436
485
|
if (croppedImageData) {
|
|
437
486
|
onResult({
|
|
@@ -537,6 +586,29 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
537
586
|
{croppedImageData ? (
|
|
538
587
|
// check_DP: Show confirmation screen
|
|
539
588
|
<View style={styles.confirmationContainer}>
|
|
589
|
+
{/* 상단 회전 버튼들 */}
|
|
590
|
+
<View style={styles.rotateButtonsTop}>
|
|
591
|
+
<TouchableOpacity
|
|
592
|
+
style={[styles.rotateButtonTop, isRotating && styles.buttonDisabled]}
|
|
593
|
+
onPress={() => handleRotateImage(-90)}
|
|
594
|
+
disabled={isRotating}
|
|
595
|
+
accessibilityLabel="왼쪽으로 90도 회전"
|
|
596
|
+
accessibilityRole="button"
|
|
597
|
+
>
|
|
598
|
+
<Text style={styles.rotateIconText}>↺</Text>
|
|
599
|
+
<Text style={styles.rotateButtonLabel}>좌로 90°</Text>
|
|
600
|
+
</TouchableOpacity>
|
|
601
|
+
<TouchableOpacity
|
|
602
|
+
style={[styles.rotateButtonTop, isRotating && styles.buttonDisabled]}
|
|
603
|
+
onPress={() => handleRotateImage(90)}
|
|
604
|
+
disabled={isRotating}
|
|
605
|
+
accessibilityLabel="오른쪽으로 90도 회전"
|
|
606
|
+
accessibilityRole="button"
|
|
607
|
+
>
|
|
608
|
+
<Text style={styles.rotateIconText}>↻</Text>
|
|
609
|
+
<Text style={styles.rotateButtonLabel}>우로 90°</Text>
|
|
610
|
+
</TouchableOpacity>
|
|
611
|
+
</View>
|
|
540
612
|
<Image
|
|
541
613
|
source={{ uri: croppedImageData.path }}
|
|
542
614
|
style={styles.previewImage}
|
|
@@ -774,6 +846,35 @@ const styles = StyleSheet.create({
|
|
|
774
846
|
justifyContent: 'center',
|
|
775
847
|
alignItems: 'center',
|
|
776
848
|
},
|
|
849
|
+
rotateButtonsTop: {
|
|
850
|
+
position: 'absolute',
|
|
851
|
+
top: 60,
|
|
852
|
+
left: 20,
|
|
853
|
+
flexDirection: 'row',
|
|
854
|
+
gap: 12,
|
|
855
|
+
zIndex: 10,
|
|
856
|
+
},
|
|
857
|
+
rotateButtonTop: {
|
|
858
|
+
flexDirection: 'row',
|
|
859
|
+
alignItems: 'center',
|
|
860
|
+
backgroundColor: 'rgba(50,50,50,0.8)',
|
|
861
|
+
paddingVertical: 10,
|
|
862
|
+
paddingHorizontal: 16,
|
|
863
|
+
borderRadius: 24,
|
|
864
|
+
borderWidth: 1,
|
|
865
|
+
borderColor: 'rgba(255,255,255,0.3)',
|
|
866
|
+
gap: 6,
|
|
867
|
+
},
|
|
868
|
+
rotateIconText: {
|
|
869
|
+
fontSize: 24,
|
|
870
|
+
color: '#fff',
|
|
871
|
+
fontWeight: 'bold',
|
|
872
|
+
},
|
|
873
|
+
rotateButtonLabel: {
|
|
874
|
+
fontSize: 14,
|
|
875
|
+
color: '#fff',
|
|
876
|
+
fontWeight: '500',
|
|
877
|
+
},
|
|
777
878
|
previewImage: {
|
|
778
879
|
width: '100%',
|
|
779
880
|
height: '80%',
|