react-native-rectangle-doc-scanner 3.100.0 → 3.102.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.js +68 -46
- package/package.json +2 -1
- package/src/FullDocScanner.tsx +73 -49
- package/src/types/react-native-fast-opencv.d.ts +20 -0
package/dist/FullDocScanner.js
CHANGED
|
@@ -42,6 +42,7 @@ 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 react_native_fast_opencv_1 = require("react-native-fast-opencv");
|
|
45
46
|
const DocScanner_1 = require("./DocScanner");
|
|
46
47
|
const stripFileUri = (value) => value.replace(/^file:\/\//, '');
|
|
47
48
|
const CROPPER_TIMEOUT_MS = 8000;
|
|
@@ -343,61 +344,82 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
343
344
|
const handleFlashToggle = (0, react_1.useCallback)(() => {
|
|
344
345
|
setFlashEnabled(prev => !prev);
|
|
345
346
|
}, []);
|
|
346
|
-
const handleRotateImage = (0, react_1.useCallback)(
|
|
347
|
+
const handleRotateImage = (0, react_1.useCallback)((degrees) => {
|
|
348
|
+
// UI만 회전 (실제 파일 회전은 confirm 시 처리)
|
|
349
|
+
setRotationDegrees(prev => {
|
|
350
|
+
const newRotation = prev + degrees;
|
|
351
|
+
// -360 ~ 360 범위로 정규화
|
|
352
|
+
if (newRotation <= -360)
|
|
353
|
+
return newRotation + 360;
|
|
354
|
+
if (newRotation >= 360)
|
|
355
|
+
return newRotation - 360;
|
|
356
|
+
return newRotation;
|
|
357
|
+
});
|
|
358
|
+
}, []);
|
|
359
|
+
const handleConfirm = (0, react_1.useCallback)(async () => {
|
|
347
360
|
if (!croppedImageData)
|
|
348
361
|
return;
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
base64Length: croppedImageData.base64?.length,
|
|
353
|
-
});
|
|
354
|
-
try {
|
|
355
|
-
// 원본 파일을 ImageCropPicker로 열어서 회전 적용
|
|
356
|
-
// 사용자가 수동으로 회전 버튼을 클릭하고 완료를 눌러야 함
|
|
357
|
-
const rotatedImage = await react_native_image_crop_picker_1.default.openCropper({
|
|
362
|
+
// 회전이 없으면 기존 데이터 그대로 전달
|
|
363
|
+
if (rotationDegrees === 0) {
|
|
364
|
+
onResult({
|
|
358
365
|
path: croppedImageData.path,
|
|
359
|
-
|
|
360
|
-
cropping: true,
|
|
361
|
-
freeStyleCropEnabled: true,
|
|
362
|
-
includeBase64: true,
|
|
363
|
-
compressImageQuality: 0.9,
|
|
364
|
-
cropperToolbarTitle: degrees === 90 ? '우측 90° 회전 → 회전 버튼 클릭 → 완료' : '좌측 90° 회전 → 회전 버튼 클릭 → 완료',
|
|
365
|
-
cropperChooseText: '완료',
|
|
366
|
-
cropperCancelText: '취소',
|
|
367
|
-
cropperRotateButtonsHidden: false,
|
|
368
|
-
enableRotationGesture: true,
|
|
366
|
+
base64: croppedImageData.base64,
|
|
369
367
|
});
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
// 회전이 있으면 실제 파일 회전 처리
|
|
371
|
+
try {
|
|
372
|
+
setProcessing(true);
|
|
373
|
+
// 회전 각도 정규화 (0, 90, 180, 270)
|
|
374
|
+
const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
|
|
375
|
+
// OpenCV 회전 코드 매핑
|
|
376
|
+
let rotateCode;
|
|
377
|
+
if (rotationNormalized === 90) {
|
|
378
|
+
rotateCode = react_native_fast_opencv_1.RotateFlags.ROTATE_90_CLOCKWISE;
|
|
379
|
+
}
|
|
380
|
+
else if (rotationNormalized === 180) {
|
|
381
|
+
rotateCode = react_native_fast_opencv_1.RotateFlags.ROTATE_180;
|
|
382
|
+
}
|
|
383
|
+
else if (rotationNormalized === 270 || rotationNormalized === -90) {
|
|
384
|
+
rotateCode = react_native_fast_opencv_1.RotateFlags.ROTATE_90_COUNTERCLOCKWISE;
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
// 회전 없음
|
|
388
|
+
onResult({
|
|
389
|
+
path: croppedImageData.path,
|
|
390
|
+
base64: croppedImageData.base64,
|
|
391
|
+
});
|
|
392
|
+
setProcessing(false);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
// 이미지 파일을 Mat 객체로 로드
|
|
396
|
+
const srcMat = await react_native_fast_opencv_1.OpenCV.invoke('imread', croppedImageData.path);
|
|
397
|
+
// 빈 Mat 객체 생성 (회전된 이미지 저장용)
|
|
398
|
+
const dstMat = new react_native_fast_opencv_1.OpenCV.Mat();
|
|
399
|
+
// OpenCV로 이미지 회전
|
|
400
|
+
react_native_fast_opencv_1.OpenCV.invoke('rotate', srcMat, dstMat, rotateCode);
|
|
401
|
+
// 회전된 이미지를 임시 파일로 저장
|
|
402
|
+
const rotatedPath = `${react_native_fs_1.default.CachesDirectoryPath}/rotated_${Date.now()}.jpg`;
|
|
403
|
+
await react_native_fast_opencv_1.OpenCV.invoke('imwrite', rotatedPath, dstMat);
|
|
404
|
+
// Mat 객체 메모리 해제
|
|
405
|
+
srcMat.delete();
|
|
406
|
+
dstMat.delete();
|
|
407
|
+
// 회전된 이미지를 base64로 변환
|
|
408
|
+
const base64Data = await react_native_fs_1.default.readFile(rotatedPath, 'base64');
|
|
409
|
+
setProcessing(false);
|
|
410
|
+
// 회전된 이미지로 결과 전달
|
|
411
|
+
onResult({
|
|
412
|
+
path: rotatedPath,
|
|
413
|
+
base64: base64Data,
|
|
379
414
|
});
|
|
380
|
-
// rotation degrees는 0으로 리셋
|
|
381
|
-
setRotationDegrees(0);
|
|
382
415
|
}
|
|
383
416
|
catch (error) {
|
|
384
|
-
console.error('[FullDocScanner] Image rotation error:', error);
|
|
385
|
-
|
|
417
|
+
console.error('[FullDocScanner] Image rotation error on confirm:', error);
|
|
418
|
+
setProcessing(false);
|
|
386
419
|
const errorMessage = error && typeof error === 'object' && 'message' in error ? error.message : '';
|
|
387
|
-
|
|
388
|
-
// 에러 메시지 표시
|
|
389
|
-
react_native_1.Alert.alert('회전 실패', '이미지 회전 중 오류가 발생했습니다.');
|
|
390
|
-
}
|
|
420
|
+
react_native_1.Alert.alert('회전 실패', '이미지 회전 중 오류가 발생했습니다: ' + errorMessage);
|
|
391
421
|
}
|
|
392
|
-
}, [croppedImageData]);
|
|
393
|
-
const handleConfirm = (0, react_1.useCallback)(() => {
|
|
394
|
-
if (!croppedImageData)
|
|
395
|
-
return;
|
|
396
|
-
onResult({
|
|
397
|
-
path: croppedImageData.path,
|
|
398
|
-
base64: croppedImageData.base64,
|
|
399
|
-
});
|
|
400
|
-
}, [croppedImageData, onResult]);
|
|
422
|
+
}, [croppedImageData, rotationDegrees, onResult]);
|
|
401
423
|
const handleRetake = (0, react_1.useCallback)(() => {
|
|
402
424
|
console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
|
|
403
425
|
setCroppedImageData(null);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-rectangle-doc-scanner",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.102.0",
|
|
4
4
|
"description": "Native-backed document scanner for React Native with customizable overlays.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"react": "*",
|
|
35
35
|
"react-native": "*",
|
|
36
|
+
"react-native-fast-opencv": "*",
|
|
36
37
|
"react-native-fs": "*",
|
|
37
38
|
"react-native-image-crop-picker": "*",
|
|
38
39
|
"react-native-image-picker": "*",
|
package/src/FullDocScanner.tsx
CHANGED
|
@@ -12,6 +12,7 @@ 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 { OpenCV, RotateFlags } from 'react-native-fast-opencv';
|
|
15
16
|
import { DocScanner } from './DocScanner';
|
|
16
17
|
import type { CapturedDocument } from './types';
|
|
17
18
|
import type {
|
|
@@ -455,66 +456,89 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
455
456
|
setFlashEnabled(prev => !prev);
|
|
456
457
|
}, []);
|
|
457
458
|
|
|
458
|
-
const handleRotateImage = useCallback(
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
459
|
+
const handleRotateImage = useCallback((degrees: -90 | 90) => {
|
|
460
|
+
// UI만 회전 (실제 파일 회전은 confirm 시 처리)
|
|
461
|
+
setRotationDegrees(prev => {
|
|
462
|
+
const newRotation = prev + degrees;
|
|
463
|
+
// -360 ~ 360 범위로 정규화
|
|
464
|
+
if (newRotation <= -360) return newRotation + 360;
|
|
465
|
+
if (newRotation >= 360) return newRotation - 360;
|
|
466
|
+
return newRotation;
|
|
465
467
|
});
|
|
468
|
+
}, []);
|
|
466
469
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
470
|
+
const handleConfirm = useCallback(async () => {
|
|
471
|
+
if (!croppedImageData) return;
|
|
472
|
+
|
|
473
|
+
// 회전이 없으면 기존 데이터 그대로 전달
|
|
474
|
+
if (rotationDegrees === 0) {
|
|
475
|
+
onResult({
|
|
471
476
|
path: croppedImageData.path,
|
|
472
|
-
|
|
473
|
-
cropping: true,
|
|
474
|
-
freeStyleCropEnabled: true,
|
|
475
|
-
includeBase64: true,
|
|
476
|
-
compressImageQuality: 0.9,
|
|
477
|
-
cropperToolbarTitle: degrees === 90 ? '우측 90° 회전 → 회전 버튼 클릭 → 완료' : '좌측 90° 회전 → 회전 버튼 클릭 → 완료',
|
|
478
|
-
cropperChooseText: '완료',
|
|
479
|
-
cropperCancelText: '취소',
|
|
480
|
-
cropperRotateButtonsHidden: false,
|
|
481
|
-
enableRotationGesture: true,
|
|
477
|
+
base64: croppedImageData.base64,
|
|
482
478
|
});
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
483
481
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
482
|
+
// 회전이 있으면 실제 파일 회전 처리
|
|
483
|
+
try {
|
|
484
|
+
setProcessing(true);
|
|
485
|
+
|
|
486
|
+
// 회전 각도 정규화 (0, 90, 180, 270)
|
|
487
|
+
const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
|
|
488
|
+
|
|
489
|
+
// OpenCV 회전 코드 매핑
|
|
490
|
+
let rotateCode: RotateFlags;
|
|
491
|
+
if (rotationNormalized === 90) {
|
|
492
|
+
rotateCode = RotateFlags.ROTATE_90_CLOCKWISE;
|
|
493
|
+
} else if (rotationNormalized === 180) {
|
|
494
|
+
rotateCode = RotateFlags.ROTATE_180;
|
|
495
|
+
} else if (rotationNormalized === 270 || rotationNormalized === -90) {
|
|
496
|
+
rotateCode = RotateFlags.ROTATE_90_COUNTERCLOCKWISE;
|
|
497
|
+
} else {
|
|
498
|
+
// 회전 없음
|
|
499
|
+
onResult({
|
|
500
|
+
path: croppedImageData.path,
|
|
501
|
+
base64: croppedImageData.base64,
|
|
502
|
+
});
|
|
503
|
+
setProcessing(false);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
489
506
|
|
|
490
|
-
//
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
507
|
+
// 이미지 파일을 Mat 객체로 로드
|
|
508
|
+
const srcMat = await OpenCV.invoke('imread', croppedImageData.path);
|
|
509
|
+
|
|
510
|
+
// 빈 Mat 객체 생성 (회전된 이미지 저장용)
|
|
511
|
+
const dstMat = new OpenCV.Mat();
|
|
512
|
+
|
|
513
|
+
// OpenCV로 이미지 회전
|
|
514
|
+
OpenCV.invoke('rotate', srcMat, dstMat, rotateCode);
|
|
515
|
+
|
|
516
|
+
// 회전된 이미지를 임시 파일로 저장
|
|
517
|
+
const rotatedPath = `${RNFS.CachesDirectoryPath}/rotated_${Date.now()}.jpg`;
|
|
518
|
+
await OpenCV.invoke('imwrite', rotatedPath, dstMat);
|
|
519
|
+
|
|
520
|
+
// Mat 객체 메모리 해제
|
|
521
|
+
srcMat.delete();
|
|
522
|
+
dstMat.delete();
|
|
523
|
+
|
|
524
|
+
// 회전된 이미지를 base64로 변환
|
|
525
|
+
const base64Data = await RNFS.readFile(rotatedPath, 'base64');
|
|
495
526
|
|
|
496
|
-
|
|
497
|
-
|
|
527
|
+
setProcessing(false);
|
|
528
|
+
|
|
529
|
+
// 회전된 이미지로 결과 전달
|
|
530
|
+
onResult({
|
|
531
|
+
path: rotatedPath,
|
|
532
|
+
base64: base64Data,
|
|
533
|
+
});
|
|
498
534
|
} catch (error) {
|
|
499
|
-
console.error('[FullDocScanner] Image rotation error:', error);
|
|
535
|
+
console.error('[FullDocScanner] Image rotation error on confirm:', error);
|
|
536
|
+
setProcessing(false);
|
|
500
537
|
|
|
501
|
-
// 사용자가 취소했으면 아무것도 안함
|
|
502
538
|
const errorMessage = error && typeof error === 'object' && 'message' in error ? (error as Error).message : '';
|
|
503
|
-
|
|
504
|
-
// 에러 메시지 표시
|
|
505
|
-
Alert.alert('회전 실패', '이미지 회전 중 오류가 발생했습니다.');
|
|
506
|
-
}
|
|
539
|
+
Alert.alert('회전 실패', '이미지 회전 중 오류가 발생했습니다: ' + errorMessage);
|
|
507
540
|
}
|
|
508
|
-
}, [croppedImageData]);
|
|
509
|
-
|
|
510
|
-
const handleConfirm = useCallback(() => {
|
|
511
|
-
if (!croppedImageData) return;
|
|
512
|
-
|
|
513
|
-
onResult({
|
|
514
|
-
path: croppedImageData.path,
|
|
515
|
-
base64: croppedImageData.base64,
|
|
516
|
-
});
|
|
517
|
-
}, [croppedImageData, onResult]);
|
|
541
|
+
}, [croppedImageData, rotationDegrees, onResult]);
|
|
518
542
|
|
|
519
543
|
const handleRetake = useCallback(() => {
|
|
520
544
|
console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
declare module 'react-native-fast-opencv' {
|
|
2
|
+
export enum RotateFlags {
|
|
3
|
+
ROTATE_90_CLOCKWISE = 0,
|
|
4
|
+
ROTATE_180 = 1,
|
|
5
|
+
ROTATE_90_COUNTERCLOCKWISE = 2
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class Mat {
|
|
9
|
+
delete(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface OpenCVModel {
|
|
13
|
+
Mat: typeof Mat;
|
|
14
|
+
invoke(name: 'imread', path: string): Promise<Mat>;
|
|
15
|
+
invoke(name: 'imwrite', path: string, mat: Mat): Promise<void>;
|
|
16
|
+
invoke(name: 'rotate', src: Mat, dst: Mat, code: RotateFlags): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const OpenCV: OpenCVModel;
|
|
20
|
+
}
|