react-native-rectangle-doc-scanner 3.106.0 → 3.107.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 +1 -1
- package/dist/FullDocScanner.js +66 -20
- package/package.json +1 -1
- package/src/FullDocScanner.tsx +110 -43
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. If the host app
|
|
123
|
+
- `FullDocScanner` – puts the scanner and crop editor into a single modal-like flow. If the host app links either `expo-image-manipulator` or `react-native-image-rotate`, the confirmation screen exposes 90° rotation buttons; otherwise rotation controls remain hidden.
|
|
124
124
|
|
|
125
125
|
## License
|
|
126
126
|
|
package/SETUP.md
CHANGED
|
@@ -117,7 +117,7 @@ function MyComponent() {
|
|
|
117
117
|
}
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
> 참고: 최종 확인 화면의 회전 버튼은 프로젝트에 `expo-image-manipulator` 네이티브 모듈이
|
|
120
|
+
> 참고: 최종 확인 화면의 회전 버튼은 프로젝트에 `expo-image-manipulator` 또는 `react-native-image-rotate` 네이티브 모듈이 연결되어 있을 때만 노출됩니다. 둘 중 하나라도 없으면 버튼이 자동으로 숨겨지고 원본 각도로 결과가 반환됩니다.
|
|
121
121
|
|
|
122
122
|
## API 변경사항
|
|
123
123
|
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -44,14 +44,25 @@ const react_native_image_crop_picker_1 = __importDefault(require("react-native-i
|
|
|
44
44
|
const react_native_fs_1 = __importDefault(require("react-native-fs"));
|
|
45
45
|
const DocScanner_1 = require("./DocScanner");
|
|
46
46
|
let ImageManipulator = null;
|
|
47
|
+
let ImageRotate = null;
|
|
47
48
|
try {
|
|
48
49
|
ImageManipulator = require('expo-image-manipulator');
|
|
49
50
|
}
|
|
50
51
|
catch (error) {
|
|
51
|
-
console.warn('[FullDocScanner] expo-image-manipulator module unavailable.
|
|
52
|
+
console.warn('[FullDocScanner] expo-image-manipulator module unavailable. Checking fallback options.', error);
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
+
try {
|
|
55
|
+
const rotateModule = require('react-native-image-rotate');
|
|
56
|
+
ImageRotate = (rotateModule?.default ?? rotateModule);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.warn('[FullDocScanner] react-native-image-rotate module unavailable. Image rotation fallback disabled.', error);
|
|
60
|
+
}
|
|
61
|
+
const isExpoImageManipulatorAvailable = !!ImageManipulator?.manipulateAsync;
|
|
62
|
+
const isImageRotateAvailable = !!ImageRotate?.rotateImage;
|
|
63
|
+
const isImageRotationSupported = isExpoImageManipulatorAvailable || isImageRotateAvailable;
|
|
54
64
|
const stripFileUri = (value) => value.replace(/^file:\/\//, '');
|
|
65
|
+
const ensureFileUri = (value) => (value.startsWith('file://') ? value : `file://${value}`);
|
|
55
66
|
const CROPPER_TIMEOUT_MS = 8000;
|
|
56
67
|
const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
|
|
57
68
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -351,9 +362,25 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
351
362
|
const handleFlashToggle = (0, react_1.useCallback)(() => {
|
|
352
363
|
setFlashEnabled(prev => !prev);
|
|
353
364
|
}, []);
|
|
365
|
+
const rotateImageWithFallback = (0, react_1.useCallback)((uri, angle) => {
|
|
366
|
+
return new Promise((resolve, reject) => {
|
|
367
|
+
if (!ImageRotate?.rotateImage) {
|
|
368
|
+
reject(new Error('react-native-image-rotate unavailable'));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
ImageRotate.rotateImage(uri, angle, (rotatedUri) => resolve(rotatedUri), (error) => {
|
|
372
|
+
const message = typeof error === 'string'
|
|
373
|
+
? error
|
|
374
|
+
: error && typeof error === 'object' && 'message' in error
|
|
375
|
+
? String(error.message)
|
|
376
|
+
: 'Unknown rotation error';
|
|
377
|
+
reject(new Error(message));
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
}, []);
|
|
354
381
|
const handleRotateImage = (0, react_1.useCallback)((degrees) => {
|
|
355
382
|
if (!isImageRotationSupported) {
|
|
356
|
-
console.warn('[FullDocScanner] Image rotation requested but
|
|
383
|
+
console.warn('[FullDocScanner] Image rotation requested but no rotation module is available.');
|
|
357
384
|
return;
|
|
358
385
|
}
|
|
359
386
|
// UI만 회전 (실제 파일 회전은 confirm 시 처리)
|
|
@@ -372,10 +399,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
372
399
|
return;
|
|
373
400
|
}
|
|
374
401
|
// 회전이 없거나 모듈을 사용할 수 없으면 기존 데이터 그대로 전달
|
|
375
|
-
if (rotationDegrees === 0
|
|
376
|
-
if (rotationDegrees !== 0 && !isImageRotationSupported) {
|
|
377
|
-
console.warn('[FullDocScanner] Confirm requested with rotation but expo-image-manipulator is unavailable. Returning original image.');
|
|
378
|
-
}
|
|
402
|
+
if (rotationDegrees === 0) {
|
|
379
403
|
onResult({
|
|
380
404
|
path: croppedImageData.path,
|
|
381
405
|
base64: croppedImageData.base64,
|
|
@@ -386,25 +410,47 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
386
410
|
setProcessing(true);
|
|
387
411
|
// 회전 각도 정규화 (0, 90, 180, 270)
|
|
388
412
|
const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
|
|
389
|
-
|
|
390
|
-
if (!manipulator?.manipulateAsync) {
|
|
391
|
-
console.warn('[FullDocScanner] expo-image-manipulator is unavailable at runtime. Returning original image.');
|
|
413
|
+
if (rotationNormalized === 0) {
|
|
392
414
|
onResult({
|
|
393
415
|
path: croppedImageData.path,
|
|
394
416
|
base64: croppedImageData.base64,
|
|
395
417
|
});
|
|
396
418
|
return;
|
|
397
419
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
420
|
+
if (isExpoImageManipulatorAvailable && ImageManipulator?.manipulateAsync) {
|
|
421
|
+
// expo-image-manipulator로 이미지 회전
|
|
422
|
+
const result = await ImageManipulator.manipulateAsync(croppedImageData.path, [{ rotate: rotationNormalized }], {
|
|
423
|
+
compress: 0.9,
|
|
424
|
+
format: ImageManipulator.SaveFormat.JPEG,
|
|
425
|
+
base64: true,
|
|
426
|
+
});
|
|
427
|
+
onResult({
|
|
428
|
+
path: result.uri,
|
|
429
|
+
base64: result.base64,
|
|
430
|
+
});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (isImageRotateAvailable && ImageRotate?.rotateImage) {
|
|
434
|
+
const sourceUri = ensureFileUri(croppedImageData.path);
|
|
435
|
+
const rotatedUri = await rotateImageWithFallback(sourceUri, rotationNormalized);
|
|
436
|
+
let base64Result = croppedImageData.base64;
|
|
437
|
+
try {
|
|
438
|
+
base64Result = await react_native_fs_1.default.readFile(stripFileUri(rotatedUri), 'base64');
|
|
439
|
+
}
|
|
440
|
+
catch (readError) {
|
|
441
|
+
console.warn('[FullDocScanner] Failed to generate base64 for rotated image:', readError);
|
|
442
|
+
base64Result = undefined;
|
|
443
|
+
}
|
|
444
|
+
onResult({
|
|
445
|
+
path: rotatedUri,
|
|
446
|
+
base64: base64Result,
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
console.warn('[FullDocScanner] Rotation requested but no supported rotation module is available. Returning original image.');
|
|
405
451
|
onResult({
|
|
406
|
-
path:
|
|
407
|
-
base64:
|
|
452
|
+
path: croppedImageData.path,
|
|
453
|
+
base64: croppedImageData.base64,
|
|
408
454
|
});
|
|
409
455
|
}
|
|
410
456
|
catch (error) {
|
|
@@ -415,7 +461,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
415
461
|
finally {
|
|
416
462
|
setProcessing(false);
|
|
417
463
|
}
|
|
418
|
-
}, [croppedImageData, rotationDegrees, onResult]);
|
|
464
|
+
}, [croppedImageData, rotationDegrees, onResult, rotateImageWithFallback]);
|
|
419
465
|
const handleRetake = (0, react_1.useCallback)(() => {
|
|
420
466
|
console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
|
|
421
467
|
setCroppedImageData(null);
|
package/package.json
CHANGED
package/src/FullDocScanner.tsx
CHANGED
|
@@ -22,21 +22,43 @@ import type {
|
|
|
22
22
|
} from './DocScanner';
|
|
23
23
|
|
|
24
24
|
type ImageManipulatorModule = typeof import('expo-image-manipulator');
|
|
25
|
+
type ImageRotateModule = {
|
|
26
|
+
rotateImage: (
|
|
27
|
+
uri: string,
|
|
28
|
+
angle: number,
|
|
29
|
+
onSuccess: (uri: string) => void,
|
|
30
|
+
onError: (error: unknown) => void,
|
|
31
|
+
) => void;
|
|
32
|
+
} | null;
|
|
25
33
|
|
|
26
34
|
let ImageManipulator: ImageManipulatorModule | null = null;
|
|
35
|
+
let ImageRotate: ImageRotateModule = null;
|
|
27
36
|
|
|
28
37
|
try {
|
|
29
38
|
ImageManipulator = require('expo-image-manipulator') as ImageManipulatorModule;
|
|
30
39
|
} catch (error) {
|
|
31
40
|
console.warn(
|
|
32
|
-
'[FullDocScanner] expo-image-manipulator module unavailable.
|
|
41
|
+
'[FullDocScanner] expo-image-manipulator module unavailable. Checking fallback options.',
|
|
33
42
|
error,
|
|
34
43
|
);
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
try {
|
|
47
|
+
const rotateModule = require('react-native-image-rotate');
|
|
48
|
+
ImageRotate = (rotateModule?.default ?? rotateModule) as ImageRotateModule;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.warn(
|
|
51
|
+
'[FullDocScanner] react-native-image-rotate module unavailable. Image rotation fallback disabled.',
|
|
52
|
+
error,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const isExpoImageManipulatorAvailable = !!ImageManipulator?.manipulateAsync;
|
|
57
|
+
const isImageRotateAvailable = !!ImageRotate?.rotateImage;
|
|
58
|
+
const isImageRotationSupported = isExpoImageManipulatorAvailable || isImageRotateAvailable;
|
|
38
59
|
|
|
39
60
|
const stripFileUri = (value: string) => value.replace(/^file:\/\//, '');
|
|
61
|
+
const ensureFileUri = (value: string) => (value.startsWith('file://') ? value : `file://${value}`);
|
|
40
62
|
|
|
41
63
|
const CROPPER_TIMEOUT_MS = 8000;
|
|
42
64
|
const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
|
|
@@ -470,37 +492,58 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
470
492
|
setFlashEnabled(prev => !prev);
|
|
471
493
|
}, []);
|
|
472
494
|
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
'
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
495
|
+
const rotateImageWithFallback = useCallback((uri: string, angle: number) => {
|
|
496
|
+
return new Promise<string>((resolve, reject) => {
|
|
497
|
+
if (!ImageRotate?.rotateImage) {
|
|
498
|
+
reject(new Error('react-native-image-rotate unavailable'));
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
480
501
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
502
|
+
ImageRotate.rotateImage(
|
|
503
|
+
uri,
|
|
504
|
+
angle,
|
|
505
|
+
(rotatedUri) => resolve(rotatedUri),
|
|
506
|
+
(error) => {
|
|
507
|
+
const message =
|
|
508
|
+
typeof error === 'string'
|
|
509
|
+
? error
|
|
510
|
+
: error && typeof error === 'object' && 'message' in error
|
|
511
|
+
? String((error as any).message)
|
|
512
|
+
: 'Unknown rotation error';
|
|
513
|
+
reject(new Error(message));
|
|
514
|
+
},
|
|
515
|
+
);
|
|
488
516
|
});
|
|
489
517
|
}, []);
|
|
490
518
|
|
|
519
|
+
const handleRotateImage = useCallback(
|
|
520
|
+
(degrees: -90 | 90) => {
|
|
521
|
+
if (!isImageRotationSupported) {
|
|
522
|
+
console.warn(
|
|
523
|
+
'[FullDocScanner] Image rotation requested but no rotation module is available.',
|
|
524
|
+
);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// UI만 회전 (실제 파일 회전은 confirm 시 처리)
|
|
529
|
+
setRotationDegrees((prev) => {
|
|
530
|
+
const newRotation = prev + degrees;
|
|
531
|
+
// -360 ~ 360 범위로 정규화
|
|
532
|
+
if (newRotation <= -360) return newRotation + 360;
|
|
533
|
+
if (newRotation >= 360) return newRotation - 360;
|
|
534
|
+
return newRotation;
|
|
535
|
+
});
|
|
536
|
+
},
|
|
537
|
+
[],
|
|
538
|
+
);
|
|
539
|
+
|
|
491
540
|
const handleConfirm = useCallback(async () => {
|
|
492
541
|
if (!croppedImageData) {
|
|
493
542
|
return;
|
|
494
543
|
}
|
|
495
544
|
|
|
496
545
|
// 회전이 없거나 모듈을 사용할 수 없으면 기존 데이터 그대로 전달
|
|
497
|
-
if (rotationDegrees === 0
|
|
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
|
-
}
|
|
503
|
-
|
|
546
|
+
if (rotationDegrees === 0) {
|
|
504
547
|
onResult({
|
|
505
548
|
path: croppedImageData.path,
|
|
506
549
|
base64: croppedImageData.base64,
|
|
@@ -514,12 +557,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
514
557
|
// 회전 각도 정규화 (0, 90, 180, 270)
|
|
515
558
|
const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
|
|
516
559
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (!manipulator?.manipulateAsync) {
|
|
520
|
-
console.warn(
|
|
521
|
-
'[FullDocScanner] expo-image-manipulator is unavailable at runtime. Returning original image.',
|
|
522
|
-
);
|
|
560
|
+
if (rotationNormalized === 0) {
|
|
523
561
|
onResult({
|
|
524
562
|
path: croppedImageData.path,
|
|
525
563
|
base64: croppedImageData.base64,
|
|
@@ -527,21 +565,50 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
527
565
|
return;
|
|
528
566
|
}
|
|
529
567
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
568
|
+
if (isExpoImageManipulatorAvailable && ImageManipulator?.manipulateAsync) {
|
|
569
|
+
// expo-image-manipulator로 이미지 회전
|
|
570
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
571
|
+
croppedImageData.path,
|
|
572
|
+
[{ rotate: rotationNormalized }],
|
|
573
|
+
{
|
|
574
|
+
compress: 0.9,
|
|
575
|
+
format: ImageManipulator.SaveFormat.JPEG,
|
|
576
|
+
base64: true,
|
|
577
|
+
},
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
onResult({
|
|
581
|
+
path: result.uri,
|
|
582
|
+
base64: result.base64,
|
|
583
|
+
});
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
540
586
|
|
|
541
|
-
|
|
587
|
+
if (isImageRotateAvailable && ImageRotate?.rotateImage) {
|
|
588
|
+
const sourceUri = ensureFileUri(croppedImageData.path);
|
|
589
|
+
const rotatedUri = await rotateImageWithFallback(sourceUri, rotationNormalized);
|
|
590
|
+
let base64Result = croppedImageData.base64;
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
base64Result = await RNFS.readFile(stripFileUri(rotatedUri), 'base64');
|
|
594
|
+
} catch (readError) {
|
|
595
|
+
console.warn('[FullDocScanner] Failed to generate base64 for rotated image:', readError);
|
|
596
|
+
base64Result = undefined;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
onResult({
|
|
600
|
+
path: rotatedUri,
|
|
601
|
+
base64: base64Result,
|
|
602
|
+
});
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
console.warn(
|
|
607
|
+
'[FullDocScanner] Rotation requested but no supported rotation module is available. Returning original image.',
|
|
608
|
+
);
|
|
542
609
|
onResult({
|
|
543
|
-
path:
|
|
544
|
-
base64:
|
|
610
|
+
path: croppedImageData.path,
|
|
611
|
+
base64: croppedImageData.base64,
|
|
545
612
|
});
|
|
546
613
|
} catch (error) {
|
|
547
614
|
console.error('[FullDocScanner] Image rotation error on confirm:', error);
|
|
@@ -552,7 +619,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
552
619
|
} finally {
|
|
553
620
|
setProcessing(false);
|
|
554
621
|
}
|
|
555
|
-
}, [croppedImageData, rotationDegrees, onResult]);
|
|
622
|
+
}, [croppedImageData, rotationDegrees, onResult, rotateImageWithFallback]);
|
|
556
623
|
|
|
557
624
|
const handleRetake = useCallback(() => {
|
|
558
625
|
console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
|