react-native-rectangle-doc-scanner 3.104.0 → 3.106.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/README.md +1 -1
- package/SETUP.md +2 -0
- package/dist/FullDocScanner.js +36 -12
- package/package.json +4 -4
- package/src/FullDocScanner.tsx +76 -33
package/README.md
CHANGED
|
@@ -120,7 +120,7 @@ Manual capture exposes an imperative `capture()` method via `ref`. Children rend
|
|
|
120
120
|
## Convenience APIs
|
|
121
121
|
|
|
122
122
|
- `CropEditor` – wraps `react-native-perspective-image-cropper` for manual corner adjustment.
|
|
123
|
-
- `FullDocScanner` – puts the scanner and crop editor into a single modal-like flow.
|
|
123
|
+
- `FullDocScanner` – puts the scanner and crop editor into a single modal-like flow. If the host app has the `expo-image-manipulator` native module installed, the confirmation screen exposes 90° rotation buttons; otherwise rotation controls remain hidden.
|
|
124
124
|
|
|
125
125
|
## License
|
|
126
126
|
|
package/SETUP.md
CHANGED
package/dist/FullDocScanner.js
CHANGED
|
@@ -42,8 +42,15 @@ const react_native_1 = require("react-native");
|
|
|
42
42
|
const react_native_image_picker_1 = require("react-native-image-picker");
|
|
43
43
|
const react_native_image_crop_picker_1 = __importDefault(require("react-native-image-crop-picker"));
|
|
44
44
|
const react_native_fs_1 = __importDefault(require("react-native-fs"));
|
|
45
|
-
const ImageManipulator = __importStar(require("expo-image-manipulator"));
|
|
46
45
|
const DocScanner_1 = require("./DocScanner");
|
|
46
|
+
let ImageManipulator = null;
|
|
47
|
+
try {
|
|
48
|
+
ImageManipulator = require('expo-image-manipulator');
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.warn('[FullDocScanner] expo-image-manipulator module unavailable. Image rotation will be disabled.', error);
|
|
52
|
+
}
|
|
53
|
+
const isImageRotationSupported = !!ImageManipulator?.manipulateAsync;
|
|
47
54
|
const stripFileUri = (value) => value.replace(/^file:\/\//, '');
|
|
48
55
|
const CROPPER_TIMEOUT_MS = 8000;
|
|
49
56
|
const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
|
|
@@ -345,8 +352,12 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
345
352
|
setFlashEnabled(prev => !prev);
|
|
346
353
|
}, []);
|
|
347
354
|
const handleRotateImage = (0, react_1.useCallback)((degrees) => {
|
|
355
|
+
if (!isImageRotationSupported) {
|
|
356
|
+
console.warn('[FullDocScanner] Image rotation requested but expo-image-manipulator is unavailable.');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
348
359
|
// UI만 회전 (실제 파일 회전은 confirm 시 처리)
|
|
349
|
-
setRotationDegrees(prev => {
|
|
360
|
+
setRotationDegrees((prev) => {
|
|
350
361
|
const newRotation = prev + degrees;
|
|
351
362
|
// -360 ~ 360 범위로 정규화
|
|
352
363
|
if (newRotation <= -360)
|
|
@@ -357,28 +368,39 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
357
368
|
});
|
|
358
369
|
}, []);
|
|
359
370
|
const handleConfirm = (0, react_1.useCallback)(async () => {
|
|
360
|
-
if (!croppedImageData)
|
|
371
|
+
if (!croppedImageData) {
|
|
361
372
|
return;
|
|
362
|
-
|
|
363
|
-
|
|
373
|
+
}
|
|
374
|
+
// 회전이 없거나 모듈을 사용할 수 없으면 기존 데이터 그대로 전달
|
|
375
|
+
if (rotationDegrees === 0 || !isImageRotationSupported || !ImageManipulator) {
|
|
376
|
+
if (rotationDegrees !== 0 && !isImageRotationSupported) {
|
|
377
|
+
console.warn('[FullDocScanner] Confirm requested with rotation but expo-image-manipulator is unavailable. Returning original image.');
|
|
378
|
+
}
|
|
364
379
|
onResult({
|
|
365
380
|
path: croppedImageData.path,
|
|
366
381
|
base64: croppedImageData.base64,
|
|
367
382
|
});
|
|
368
383
|
return;
|
|
369
384
|
}
|
|
370
|
-
// 회전이 있으면 실제 파일 회전 처리
|
|
371
385
|
try {
|
|
372
386
|
setProcessing(true);
|
|
373
387
|
// 회전 각도 정규화 (0, 90, 180, 270)
|
|
374
388
|
const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
|
|
389
|
+
const manipulator = ImageManipulator;
|
|
390
|
+
if (!manipulator?.manipulateAsync) {
|
|
391
|
+
console.warn('[FullDocScanner] expo-image-manipulator is unavailable at runtime. Returning original image.');
|
|
392
|
+
onResult({
|
|
393
|
+
path: croppedImageData.path,
|
|
394
|
+
base64: croppedImageData.base64,
|
|
395
|
+
});
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
375
398
|
// expo-image-manipulator로 이미지 회전
|
|
376
|
-
const result = await
|
|
399
|
+
const result = await manipulator.manipulateAsync(croppedImageData.path, [{ rotate: rotationNormalized }], {
|
|
377
400
|
compress: 0.9,
|
|
378
|
-
format:
|
|
401
|
+
format: manipulator.SaveFormat.JPEG,
|
|
379
402
|
base64: true,
|
|
380
403
|
});
|
|
381
|
-
setProcessing(false);
|
|
382
404
|
// 회전된 이미지로 결과 전달
|
|
383
405
|
onResult({
|
|
384
406
|
path: result.uri,
|
|
@@ -387,10 +409,12 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
387
409
|
}
|
|
388
410
|
catch (error) {
|
|
389
411
|
console.error('[FullDocScanner] Image rotation error on confirm:', error);
|
|
390
|
-
setProcessing(false);
|
|
391
412
|
const errorMessage = error && typeof error === 'object' && 'message' in error ? error.message : '';
|
|
392
413
|
react_native_1.Alert.alert('회전 실패', '이미지 회전 중 오류가 발생했습니다: ' + errorMessage);
|
|
393
414
|
}
|
|
415
|
+
finally {
|
|
416
|
+
setProcessing(false);
|
|
417
|
+
}
|
|
394
418
|
}, [croppedImageData, rotationDegrees, onResult]);
|
|
395
419
|
const handleRetake = (0, react_1.useCallback)(() => {
|
|
396
420
|
console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
|
|
@@ -466,13 +490,13 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
466
490
|
croppedImageData ? (
|
|
467
491
|
// check_DP: Show confirmation screen
|
|
468
492
|
react_1.default.createElement(react_native_1.View, { style: styles.confirmationContainer },
|
|
469
|
-
react_1.default.createElement(react_native_1.View, { style: styles.rotateButtonsCenter },
|
|
493
|
+
isImageRotationSupported ? (react_1.default.createElement(react_native_1.View, { style: styles.rotateButtonsCenter },
|
|
470
494
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.rotateButtonTop, onPress: () => handleRotateImage(-90), accessibilityLabel: "\uC67C\uCABD\uC73C\uB85C 90\uB3C4 \uD68C\uC804", accessibilityRole: "button" },
|
|
471
495
|
react_1.default.createElement(react_native_1.Text, { style: styles.rotateIconText }, "\u21BA"),
|
|
472
496
|
react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC88C\uB85C 90\u00B0")),
|
|
473
497
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.rotateButtonTop, onPress: () => handleRotateImage(90), accessibilityLabel: "\uC624\uB978\uCABD\uC73C\uB85C 90\uB3C4 \uD68C\uC804", accessibilityRole: "button" },
|
|
474
498
|
react_1.default.createElement(react_native_1.Text, { style: styles.rotateIconText }, "\u21BB"),
|
|
475
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC6B0\uB85C 90\u00B0"))),
|
|
499
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC6B0\uB85C 90\u00B0")))) : null,
|
|
476
500
|
react_1.default.createElement(react_native_1.Image, { source: { uri: croppedImageData.path }, style: [
|
|
477
501
|
styles.previewImage,
|
|
478
502
|
{ transform: [{ rotate: `${rotationDegrees}deg` }] }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-rectangle-doc-scanner",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.106.0",
|
|
4
4
|
"description": "Native-backed document scanner for React Native with customizable overlays.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"react-native-fs": "*",
|
|
37
37
|
"react-native-image-crop-picker": "*",
|
|
38
38
|
"react-native-image-picker": "*",
|
|
39
|
-
"react-native-svg": "*"
|
|
39
|
+
"react-native-svg": "*",
|
|
40
|
+
"expo-modules-core": "*"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@types/react": "^18.2.41",
|
|
@@ -47,7 +48,6 @@
|
|
|
47
48
|
"typescript": "^5.3.3"
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
50
|
-
"expo-image-manipulator": "^12.0.5"
|
|
51
|
-
"expo-modules-core": "^2.1.0"
|
|
51
|
+
"expo-image-manipulator": "^12.0.5"
|
|
52
52
|
}
|
|
53
53
|
}
|
package/src/FullDocScanner.tsx
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
import { launchImageLibrary } from 'react-native-image-picker';
|
|
13
13
|
import ImageCropPicker from 'react-native-image-crop-picker';
|
|
14
14
|
import RNFS from 'react-native-fs';
|
|
15
|
-
import * as ImageManipulator from 'expo-image-manipulator';
|
|
16
15
|
import { DocScanner } from './DocScanner';
|
|
17
16
|
import type { CapturedDocument } from './types';
|
|
18
17
|
import type {
|
|
@@ -22,6 +21,21 @@ import type {
|
|
|
22
21
|
RectangleDetectEvent,
|
|
23
22
|
} from './DocScanner';
|
|
24
23
|
|
|
24
|
+
type ImageManipulatorModule = typeof import('expo-image-manipulator');
|
|
25
|
+
|
|
26
|
+
let ImageManipulator: ImageManipulatorModule | null = null;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
ImageManipulator = require('expo-image-manipulator') as ImageManipulatorModule;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.warn(
|
|
32
|
+
'[FullDocScanner] expo-image-manipulator module unavailable. Image rotation will be disabled.',
|
|
33
|
+
error,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const isImageRotationSupported = !!ImageManipulator?.manipulateAsync;
|
|
38
|
+
|
|
25
39
|
const stripFileUri = (value: string) => value.replace(/^file:\/\//, '');
|
|
26
40
|
|
|
27
41
|
const CROPPER_TIMEOUT_MS = 8000;
|
|
@@ -457,8 +471,15 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
457
471
|
}, []);
|
|
458
472
|
|
|
459
473
|
const handleRotateImage = useCallback((degrees: -90 | 90) => {
|
|
474
|
+
if (!isImageRotationSupported) {
|
|
475
|
+
console.warn(
|
|
476
|
+
'[FullDocScanner] Image rotation requested but expo-image-manipulator is unavailable.',
|
|
477
|
+
);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
460
481
|
// UI만 회전 (실제 파일 회전은 confirm 시 처리)
|
|
461
|
-
setRotationDegrees(prev => {
|
|
482
|
+
setRotationDegrees((prev) => {
|
|
462
483
|
const newRotation = prev + degrees;
|
|
463
484
|
// -360 ~ 360 범위로 정규화
|
|
464
485
|
if (newRotation <= -360) return newRotation + 360;
|
|
@@ -468,10 +489,18 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
468
489
|
}, []);
|
|
469
490
|
|
|
470
491
|
const handleConfirm = useCallback(async () => {
|
|
471
|
-
if (!croppedImageData)
|
|
492
|
+
if (!croppedImageData) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 회전이 없거나 모듈을 사용할 수 없으면 기존 데이터 그대로 전달
|
|
497
|
+
if (rotationDegrees === 0 || !isImageRotationSupported || !ImageManipulator) {
|
|
498
|
+
if (rotationDegrees !== 0 && !isImageRotationSupported) {
|
|
499
|
+
console.warn(
|
|
500
|
+
'[FullDocScanner] Confirm requested with rotation but expo-image-manipulator is unavailable. Returning original image.',
|
|
501
|
+
);
|
|
502
|
+
}
|
|
472
503
|
|
|
473
|
-
// 회전이 없으면 기존 데이터 그대로 전달
|
|
474
|
-
if (rotationDegrees === 0) {
|
|
475
504
|
onResult({
|
|
476
505
|
path: croppedImageData.path,
|
|
477
506
|
base64: croppedImageData.base64,
|
|
@@ -479,26 +508,36 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
479
508
|
return;
|
|
480
509
|
}
|
|
481
510
|
|
|
482
|
-
// 회전이 있으면 실제 파일 회전 처리
|
|
483
511
|
try {
|
|
484
512
|
setProcessing(true);
|
|
485
513
|
|
|
486
514
|
// 회전 각도 정규화 (0, 90, 180, 270)
|
|
487
515
|
const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
|
|
488
516
|
|
|
517
|
+
const manipulator = ImageManipulator;
|
|
518
|
+
|
|
519
|
+
if (!manipulator?.manipulateAsync) {
|
|
520
|
+
console.warn(
|
|
521
|
+
'[FullDocScanner] expo-image-manipulator is unavailable at runtime. Returning original image.',
|
|
522
|
+
);
|
|
523
|
+
onResult({
|
|
524
|
+
path: croppedImageData.path,
|
|
525
|
+
base64: croppedImageData.base64,
|
|
526
|
+
});
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
489
530
|
// expo-image-manipulator로 이미지 회전
|
|
490
|
-
const result = await
|
|
531
|
+
const result = await manipulator.manipulateAsync(
|
|
491
532
|
croppedImageData.path,
|
|
492
533
|
[{ rotate: rotationNormalized }],
|
|
493
534
|
{
|
|
494
535
|
compress: 0.9,
|
|
495
|
-
format:
|
|
536
|
+
format: manipulator.SaveFormat.JPEG,
|
|
496
537
|
base64: true,
|
|
497
|
-
}
|
|
538
|
+
},
|
|
498
539
|
);
|
|
499
540
|
|
|
500
|
-
setProcessing(false);
|
|
501
|
-
|
|
502
541
|
// 회전된 이미지로 결과 전달
|
|
503
542
|
onResult({
|
|
504
543
|
path: result.uri,
|
|
@@ -506,10 +545,12 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
506
545
|
});
|
|
507
546
|
} catch (error) {
|
|
508
547
|
console.error('[FullDocScanner] Image rotation error on confirm:', error);
|
|
509
|
-
setProcessing(false);
|
|
510
548
|
|
|
511
|
-
const errorMessage =
|
|
549
|
+
const errorMessage =
|
|
550
|
+
error && typeof error === 'object' && 'message' in error ? (error as Error).message : '';
|
|
512
551
|
Alert.alert('회전 실패', '이미지 회전 중 오류가 발생했습니다: ' + errorMessage);
|
|
552
|
+
} finally {
|
|
553
|
+
setProcessing(false);
|
|
513
554
|
}
|
|
514
555
|
}, [croppedImageData, rotationDegrees, onResult]);
|
|
515
556
|
|
|
@@ -599,26 +640,28 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
599
640
|
// check_DP: Show confirmation screen
|
|
600
641
|
<View style={styles.confirmationContainer}>
|
|
601
642
|
{/* 회전 버튼들 - 가운데 정렬 */}
|
|
602
|
-
|
|
603
|
-
<
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
643
|
+
{isImageRotationSupported ? (
|
|
644
|
+
<View style={styles.rotateButtonsCenter}>
|
|
645
|
+
<TouchableOpacity
|
|
646
|
+
style={styles.rotateButtonTop}
|
|
647
|
+
onPress={() => handleRotateImage(-90)}
|
|
648
|
+
accessibilityLabel="왼쪽으로 90도 회전"
|
|
649
|
+
accessibilityRole="button"
|
|
650
|
+
>
|
|
651
|
+
<Text style={styles.rotateIconText}>↺</Text>
|
|
652
|
+
<Text style={styles.rotateButtonLabel}>좌로 90°</Text>
|
|
653
|
+
</TouchableOpacity>
|
|
654
|
+
<TouchableOpacity
|
|
655
|
+
style={styles.rotateButtonTop}
|
|
656
|
+
onPress={() => handleRotateImage(90)}
|
|
657
|
+
accessibilityLabel="오른쪽으로 90도 회전"
|
|
658
|
+
accessibilityRole="button"
|
|
659
|
+
>
|
|
660
|
+
<Text style={styles.rotateIconText}>↻</Text>
|
|
661
|
+
<Text style={styles.rotateButtonLabel}>우로 90°</Text>
|
|
662
|
+
</TouchableOpacity>
|
|
663
|
+
</View>
|
|
664
|
+
) : null}
|
|
622
665
|
<Image
|
|
623
666
|
source={{ uri: croppedImageData.path }}
|
|
624
667
|
style={[
|