react-native-rectangle-doc-scanner 3.90.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 +76 -1
- package/package.json +1 -1
- package/src/FullDocScanner.tsx +97 -1
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,
|
|
@@ -327,6 +329,43 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
327
329
|
const handleFlashToggle = (0, react_1.useCallback)(() => {
|
|
328
330
|
setFlashEnabled(prev => !prev);
|
|
329
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]);
|
|
330
369
|
const handleConfirm = (0, react_1.useCallback)(() => {
|
|
331
370
|
if (croppedImageData) {
|
|
332
371
|
onResult({
|
|
@@ -420,6 +459,13 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
420
459
|
croppedImageData ? (
|
|
421
460
|
// check_DP: Show confirmation screen
|
|
422
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"))),
|
|
423
469
|
react_1.default.createElement(react_native_1.Image, { source: { uri: croppedImageData.path }, style: styles.previewImage, resizeMode: "contain" }),
|
|
424
470
|
react_1.default.createElement(react_native_1.View, { style: styles.confirmationButtons },
|
|
425
471
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.retakeButton], onPress: handleRetake, accessibilityLabel: mergedStrings.retake, accessibilityRole: "button" },
|
|
@@ -571,6 +617,35 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
571
617
|
justifyContent: 'center',
|
|
572
618
|
alignItems: 'center',
|
|
573
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
|
+
},
|
|
574
649
|
previewImage: {
|
|
575
650
|
width: '100%',
|
|
576
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,
|
|
@@ -437,6 +440,47 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
437
440
|
setFlashEnabled(prev => !prev);
|
|
438
441
|
}, []);
|
|
439
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
|
+
|
|
440
484
|
const handleConfirm = useCallback(() => {
|
|
441
485
|
if (croppedImageData) {
|
|
442
486
|
onResult({
|
|
@@ -542,6 +586,29 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
542
586
|
{croppedImageData ? (
|
|
543
587
|
// check_DP: Show confirmation screen
|
|
544
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>
|
|
545
612
|
<Image
|
|
546
613
|
source={{ uri: croppedImageData.path }}
|
|
547
614
|
style={styles.previewImage}
|
|
@@ -779,6 +846,35 @@ const styles = StyleSheet.create({
|
|
|
779
846
|
justifyContent: 'center',
|
|
780
847
|
alignItems: 'center',
|
|
781
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
|
+
},
|
|
782
878
|
previewImage: {
|
|
783
879
|
width: '100%',
|
|
784
880
|
height: '80%',
|