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.
- package/dist/FullDocScanner.d.ts +1 -0
- package/dist/FullDocScanner.js +78 -15
- package/package.json +1 -1
- package/src/FullDocScanner.tsx +99 -15
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({
|
|
@@ -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
|
|
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
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({
|
|
@@ -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
|
|
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%',
|