react-native-rectangle-doc-scanner 3.90.0 → 3.92.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({
@@ -383,30 +422,18 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
383
422
  }
384
423
  if (captureReady) {
385
424
  scheduleClear(rectangleCaptureTimeoutRef, () => {
386
- console.log('[FullDocScanner] Rectangle timeout - clearing detection');
387
425
  setRectangleDetected(false);
388
426
  });
389
427
  setRectangleDetected(true);
390
428
  }
391
- else if (!hasRectangle) {
429
+ else {
430
+ // 그리드가 없거나 품질이 좋지 않으면 즉시 상태 해제
392
431
  if (rectangleCaptureTimeoutRef.current) {
393
432
  clearTimeout(rectangleCaptureTimeoutRef.current);
394
433
  rectangleCaptureTimeoutRef.current = null;
395
434
  }
396
435
  setRectangleDetected(false);
397
436
  }
398
- else if (rectangleDetected) {
399
- scheduleClear(rectangleCaptureTimeoutRef, () => {
400
- console.log('[FullDocScanner] Rectangle timeout - clearing detection');
401
- setRectangleDetected(false);
402
- });
403
- }
404
- console.log('[FullDocScanner] Rectangle detection update', {
405
- lastDetectionType: event.lastDetectionType,
406
- stableCounter,
407
- hasRectangle,
408
- captureReady,
409
- });
410
437
  }, [rectangleDetected]);
411
438
  (0, react_1.useEffect)(() => () => {
412
439
  if (rectangleCaptureTimeoutRef.current) {
@@ -420,6 +447,13 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
420
447
  croppedImageData ? (
421
448
  // check_DP: Show confirmation screen
422
449
  react_1.default.createElement(react_native_1.View, { style: styles.confirmationContainer },
450
+ react_1.default.createElement(react_native_1.View, { style: styles.rotateButtonsTop },
451
+ 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" },
452
+ react_1.default.createElement(react_native_1.Text, { style: styles.rotateIconText }, "\u21BA"),
453
+ react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC88C\uB85C 90\u00B0")),
454
+ 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" },
455
+ react_1.default.createElement(react_native_1.Text, { style: styles.rotateIconText }, "\u21BB"),
456
+ react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC6B0\uB85C 90\u00B0"))),
423
457
  react_1.default.createElement(react_native_1.Image, { source: { uri: croppedImageData.path }, style: styles.previewImage, resizeMode: "contain" }),
424
458
  react_1.default.createElement(react_native_1.View, { style: styles.confirmationButtons },
425
459
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.retakeButton], onPress: handleRetake, accessibilityLabel: mergedStrings.retake, accessibilityRole: "button" },
@@ -571,6 +605,35 @@ const styles = react_native_1.StyleSheet.create({
571
605
  justifyContent: 'center',
572
606
  alignItems: 'center',
573
607
  },
608
+ rotateButtonsTop: {
609
+ position: 'absolute',
610
+ top: 60,
611
+ left: 20,
612
+ flexDirection: 'row',
613
+ gap: 12,
614
+ zIndex: 10,
615
+ },
616
+ rotateButtonTop: {
617
+ flexDirection: 'row',
618
+ alignItems: 'center',
619
+ backgroundColor: 'rgba(50,50,50,0.8)',
620
+ paddingVertical: 10,
621
+ paddingHorizontal: 16,
622
+ borderRadius: 24,
623
+ borderWidth: 1,
624
+ borderColor: 'rgba(255,255,255,0.3)',
625
+ gap: 6,
626
+ },
627
+ rotateIconText: {
628
+ fontSize: 24,
629
+ color: '#fff',
630
+ fontWeight: 'bold',
631
+ },
632
+ rotateButtonLabel: {
633
+ fontSize: 14,
634
+ color: '#fff',
635
+ fontWeight: '500',
636
+ },
574
637
  previewImage: {
575
638
  width: '100%',
576
639
  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.92.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({
@@ -500,29 +544,17 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
500
544
 
501
545
  if (captureReady) {
502
546
  scheduleClear(rectangleCaptureTimeoutRef, () => {
503
- console.log('[FullDocScanner] Rectangle timeout - clearing detection');
504
547
  setRectangleDetected(false);
505
548
  });
506
549
  setRectangleDetected(true);
507
- } else if (!hasRectangle) {
550
+ } else {
551
+ // 그리드가 없거나 품질이 좋지 않으면 즉시 상태 해제
508
552
  if (rectangleCaptureTimeoutRef.current) {
509
553
  clearTimeout(rectangleCaptureTimeoutRef.current);
510
554
  rectangleCaptureTimeoutRef.current = null;
511
555
  }
512
556
  setRectangleDetected(false);
513
- } else if (rectangleDetected) {
514
- scheduleClear(rectangleCaptureTimeoutRef, () => {
515
- console.log('[FullDocScanner] Rectangle timeout - clearing detection');
516
- setRectangleDetected(false);
517
- });
518
557
  }
519
-
520
- console.log('[FullDocScanner] Rectangle detection update', {
521
- lastDetectionType: event.lastDetectionType,
522
- stableCounter,
523
- hasRectangle,
524
- captureReady,
525
- });
526
558
  }, [rectangleDetected]);
527
559
 
528
560
  useEffect(
@@ -542,6 +574,29 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
542
574
  {croppedImageData ? (
543
575
  // check_DP: Show confirmation screen
544
576
  <View style={styles.confirmationContainer}>
577
+ {/* 상단 회전 버튼들 */}
578
+ <View style={styles.rotateButtonsTop}>
579
+ <TouchableOpacity
580
+ style={[styles.rotateButtonTop, isRotating && styles.buttonDisabled]}
581
+ onPress={() => handleRotateImage(-90)}
582
+ disabled={isRotating}
583
+ accessibilityLabel="왼쪽으로 90도 회전"
584
+ accessibilityRole="button"
585
+ >
586
+ <Text style={styles.rotateIconText}>↺</Text>
587
+ <Text style={styles.rotateButtonLabel}>좌로 90°</Text>
588
+ </TouchableOpacity>
589
+ <TouchableOpacity
590
+ style={[styles.rotateButtonTop, isRotating && styles.buttonDisabled]}
591
+ onPress={() => handleRotateImage(90)}
592
+ disabled={isRotating}
593
+ accessibilityLabel="오른쪽으로 90도 회전"
594
+ accessibilityRole="button"
595
+ >
596
+ <Text style={styles.rotateIconText}>↻</Text>
597
+ <Text style={styles.rotateButtonLabel}>우로 90°</Text>
598
+ </TouchableOpacity>
599
+ </View>
545
600
  <Image
546
601
  source={{ uri: croppedImageData.path }}
547
602
  style={styles.previewImage}
@@ -779,6 +834,35 @@ const styles = StyleSheet.create({
779
834
  justifyContent: 'center',
780
835
  alignItems: 'center',
781
836
  },
837
+ rotateButtonsTop: {
838
+ position: 'absolute',
839
+ top: 60,
840
+ left: 20,
841
+ flexDirection: 'row',
842
+ gap: 12,
843
+ zIndex: 10,
844
+ },
845
+ rotateButtonTop: {
846
+ flexDirection: 'row',
847
+ alignItems: 'center',
848
+ backgroundColor: 'rgba(50,50,50,0.8)',
849
+ paddingVertical: 10,
850
+ paddingHorizontal: 16,
851
+ borderRadius: 24,
852
+ borderWidth: 1,
853
+ borderColor: 'rgba(255,255,255,0.3)',
854
+ gap: 6,
855
+ },
856
+ rotateIconText: {
857
+ fontSize: 24,
858
+ color: '#fff',
859
+ fontWeight: 'bold',
860
+ },
861
+ rotateButtonLabel: {
862
+ fontSize: 14,
863
+ color: '#fff',
864
+ fontWeight: '500',
865
+ },
782
866
  previewImage: {
783
867
  width: '100%',
784
868
  height: '80%',