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.
@@ -17,6 +17,7 @@ export interface FullDocScannerStrings {
17
17
  galleryButton?: string;
18
18
  retake?: string;
19
19
  confirm?: string;
20
+ cropTitle?: string;
20
21
  }
21
22
  export interface FullDocScannerProps {
22
23
  onResult: (result: FullDocScannerResult) => void;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.90.0",
3
+ "version": "3.91.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -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%',