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.
@@ -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,
@@ -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
- // Skip waitForModalDismissal for gallery - go directly to cropper
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.89.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,
@@ -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
- // Skip waitForModalDismissal for gallery - go directly to cropper
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%',