react-native-rectangle-doc-scanner 3.41.0 → 3.43.1
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/SETUP.md +186 -0
- package/dist/FullDocScanner.d.ts +11 -10
- package/dist/FullDocScanner.js +75 -244
- package/package.json +6 -3
- package/src/FullDocScanner.tsx +141 -415
package/SETUP.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Setup Instructions
|
|
2
|
+
|
|
3
|
+
## 업데이트 내용 (v3.41.0)
|
|
4
|
+
|
|
5
|
+
이제 `FullDocScanner`가 `react-native-image-crop-picker`를 사용해서 크롭합니다!
|
|
6
|
+
|
|
7
|
+
### 주요 변경사항
|
|
8
|
+
|
|
9
|
+
1. **수동 촬영 후 자동으로 cropper 실행**
|
|
10
|
+
- 수동/자동 촬영 후 바로 `react-native-image-crop-picker`의 cropper가 실행됩니다
|
|
11
|
+
- 사용자가 직접 이미지를 자를 수 있습니다
|
|
12
|
+
|
|
13
|
+
2. **갤러리 버튼 추가**
|
|
14
|
+
- 촬영 버튼 옆에 갤러리 버튼이 추가되었습니다 (📁)
|
|
15
|
+
- 갤러리에서 이미지를 선택하고 바로 크롭할 수 있습니다
|
|
16
|
+
|
|
17
|
+
3. **간소화된 API**
|
|
18
|
+
- `CropEditor` 화면 제거
|
|
19
|
+
- `onResult` 콜백이 이제 `{ path, base64 }` 형태로 결과를 반환합니다
|
|
20
|
+
|
|
21
|
+
## 빌드 방법
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 1. 패키지 디렉토리로 이동
|
|
25
|
+
cd /Users/tt-mac-03/Documents/workspace/react-native-doc-scanner/react-native-rectangle-doc-scanner
|
|
26
|
+
|
|
27
|
+
# 2. TypeScript 빌드
|
|
28
|
+
npm run build
|
|
29
|
+
# 또는
|
|
30
|
+
yarn build
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## tdb 프로젝트에 설치
|
|
34
|
+
|
|
35
|
+
### 1. 필수 peer dependencies 설치
|
|
36
|
+
|
|
37
|
+
tdb 프로젝트에서 다음 패키지들을 설치해야 합니다:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd /Users/tt-mac-03/Documents/workspace/tdb
|
|
41
|
+
|
|
42
|
+
npm install react-native-image-picker react-native-image-crop-picker
|
|
43
|
+
# 또는
|
|
44
|
+
yarn add react-native-image-picker react-native-image-crop-picker
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. iOS 설정 (react-native-image-picker)
|
|
48
|
+
|
|
49
|
+
`tdb/ios/Podfile`에 다음 권한 추가:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
post_install do |installer|
|
|
53
|
+
installer.pods_project.targets.each do |target|
|
|
54
|
+
target.build_configurations.each do |config|
|
|
55
|
+
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`tdb/ios/tdb/Info.plist`에 권한 추가:
|
|
62
|
+
|
|
63
|
+
```xml
|
|
64
|
+
<key>NSPhotoLibraryUsageDescription</key>
|
|
65
|
+
<string>문서를 선택하기 위해 갤러리 접근이 필요합니다</string>
|
|
66
|
+
<key>NSCameraUsageDescription</key>
|
|
67
|
+
<string>문서를 촬영하기 위해 카메라 접근이 필요합니다</string>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 3. iOS pod install
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
cd /Users/tt-mac-03/Documents/workspace/tdb/ios
|
|
74
|
+
pod install
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 4. 패키지 재설치
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
cd /Users/tt-mac-03/Documents/workspace/tdb
|
|
81
|
+
|
|
82
|
+
# 기존 설치 제거
|
|
83
|
+
rm -rf node_modules/react-native-rectangle-doc-scanner
|
|
84
|
+
|
|
85
|
+
# 재설치
|
|
86
|
+
npm install
|
|
87
|
+
# 또는
|
|
88
|
+
yarn install
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 사용 방법
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { FullDocScanner } from 'react-native-rectangle-doc-scanner';
|
|
95
|
+
import type { FullDocScannerResult } from 'react-native-rectangle-doc-scanner';
|
|
96
|
+
|
|
97
|
+
function MyComponent() {
|
|
98
|
+
const handleResult = (result: FullDocScannerResult) => {
|
|
99
|
+
console.log('Cropped image path:', result.path);
|
|
100
|
+
console.log('Base64:', result.base64);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<FullDocScanner
|
|
105
|
+
onResult={handleResult}
|
|
106
|
+
onClose={() => console.log('Closed')}
|
|
107
|
+
enableGallery={true}
|
|
108
|
+
strings={{
|
|
109
|
+
captureHint: '문서를 프레임 안에 맞춰주세요',
|
|
110
|
+
manualHint: '아래 버튼을 눌러 촬영하세요',
|
|
111
|
+
cancel: '취소',
|
|
112
|
+
processing: '처리 중...',
|
|
113
|
+
galleryButton: '갤러리',
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## API 변경사항
|
|
121
|
+
|
|
122
|
+
### FullDocScannerResult (변경됨)
|
|
123
|
+
|
|
124
|
+
**이전:**
|
|
125
|
+
```typescript
|
|
126
|
+
{
|
|
127
|
+
original: CapturedDocument;
|
|
128
|
+
rectangle: Rectangle | null;
|
|
129
|
+
base64: string;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**현재:**
|
|
134
|
+
```typescript
|
|
135
|
+
{
|
|
136
|
+
path: string; // 크롭된 이미지 파일 경로
|
|
137
|
+
base64?: string; // Base64 인코딩된 이미지
|
|
138
|
+
original?: CapturedDocument; // 원본 문서 정보 (옵션)
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### FullDocScannerStrings (변경됨)
|
|
143
|
+
|
|
144
|
+
**제거된 필드:**
|
|
145
|
+
- `confirm` (cropper에서 사용)
|
|
146
|
+
- `retake` (cropper에서 사용)
|
|
147
|
+
- `cropTitle` (cropper에서 사용)
|
|
148
|
+
|
|
149
|
+
**추가된 필드:**
|
|
150
|
+
- `galleryButton` (갤러리 버튼 레이블)
|
|
151
|
+
|
|
152
|
+
### Props (추가됨)
|
|
153
|
+
|
|
154
|
+
- `enableGallery?: boolean` - 갤러리 버튼 표시 여부 (기본값: `true`)
|
|
155
|
+
- `cropWidth?: number` - 크롭 너비 (기본값: `1200`)
|
|
156
|
+
- `cropHeight?: number` - 크롭 높이 (기본값: `1600`)
|
|
157
|
+
|
|
158
|
+
## 문제 해결
|
|
159
|
+
|
|
160
|
+
### "Cannot find module 'react-native-image-picker'"
|
|
161
|
+
|
|
162
|
+
peer dependency를 설치하지 않았습니다:
|
|
163
|
+
```bash
|
|
164
|
+
npm install react-native-image-picker react-native-image-crop-picker
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### iOS 빌드 에러
|
|
168
|
+
|
|
169
|
+
1. pod install 실행:
|
|
170
|
+
```bash
|
|
171
|
+
cd ios && pod install
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
2. Info.plist에 권한 추가 확인
|
|
175
|
+
|
|
176
|
+
### TypeScript 타입 에러
|
|
177
|
+
|
|
178
|
+
패키지를 다시 빌드하고 재설치:
|
|
179
|
+
```bash
|
|
180
|
+
cd /Users/tt-mac-03/Documents/workspace/react-native-doc-scanner/react-native-rectangle-doc-scanner
|
|
181
|
+
npm run build
|
|
182
|
+
|
|
183
|
+
cd /Users/tt-mac-03/Documents/workspace/tdb
|
|
184
|
+
rm -rf node_modules/react-native-rectangle-doc-scanner
|
|
185
|
+
npm install
|
|
186
|
+
```
|
package/dist/FullDocScanner.d.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { CapturedDocument
|
|
2
|
+
import type { CapturedDocument } from './types';
|
|
3
3
|
import type { DetectionConfig } from './DocScanner';
|
|
4
4
|
export interface FullDocScannerResult {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
/** Base64-encoded
|
|
8
|
-
base64
|
|
5
|
+
/** File path to the cropped image */
|
|
6
|
+
path: string;
|
|
7
|
+
/** Base64-encoded image string (optional) */
|
|
8
|
+
base64?: string;
|
|
9
|
+
/** Original captured document info */
|
|
10
|
+
original?: CapturedDocument;
|
|
9
11
|
}
|
|
10
12
|
export interface FullDocScannerStrings {
|
|
11
13
|
captureHint?: string;
|
|
12
14
|
manualHint?: string;
|
|
13
15
|
cancel?: string;
|
|
14
|
-
confirm?: string;
|
|
15
|
-
retake?: string;
|
|
16
|
-
cropTitle?: string;
|
|
17
16
|
processing?: string;
|
|
17
|
+
galleryButton?: string;
|
|
18
18
|
}
|
|
19
19
|
export interface FullDocScannerProps {
|
|
20
20
|
onResult: (result: FullDocScannerResult) => void;
|
|
@@ -24,11 +24,12 @@ export interface FullDocScannerProps {
|
|
|
24
24
|
gridColor?: string;
|
|
25
25
|
gridLineWidth?: number;
|
|
26
26
|
showGrid?: boolean;
|
|
27
|
-
overlayStrokeColor?: string;
|
|
28
|
-
handlerColor?: string;
|
|
29
27
|
strings?: FullDocScannerStrings;
|
|
30
28
|
manualCapture?: boolean;
|
|
31
29
|
minStableFrames?: number;
|
|
32
30
|
onError?: (error: Error) => void;
|
|
31
|
+
enableGallery?: boolean;
|
|
32
|
+
cropWidth?: number;
|
|
33
|
+
cropHeight?: number;
|
|
33
34
|
}
|
|
34
35
|
export declare const FullDocScanner: React.FC<FullDocScannerProps>;
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -32,21 +32,17 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
exports.FullDocScanner = void 0;
|
|
37
40
|
const react_1 = __importStar(require("react"));
|
|
38
41
|
const react_native_1 = require("react-native");
|
|
42
|
+
const react_native_image_picker_1 = require("react-native-image-picker");
|
|
43
|
+
const react_native_image_crop_picker_1 = __importDefault(require("react-native-image-crop-picker"));
|
|
39
44
|
const DocScanner_1 = require("./DocScanner");
|
|
40
|
-
const CropEditor_1 = require("./CropEditor");
|
|
41
|
-
const coordinate_1 = require("./utils/coordinate");
|
|
42
45
|
const stripFileUri = (value) => value.replace(/^file:\/\//, '');
|
|
43
|
-
const ensureFileUri = (value) => (value.startsWith('file://') ? value : `file://${value}`);
|
|
44
|
-
const resolveImageSize = (path, fallbackWidth, fallbackHeight) => new Promise((resolve) => {
|
|
45
|
-
react_native_1.Image.getSize(ensureFileUri(path), (width, height) => resolve({ width, height }), () => resolve({
|
|
46
|
-
width: fallbackWidth > 0 ? fallbackWidth : 0,
|
|
47
|
-
height: fallbackHeight > 0 ? fallbackHeight : 0,
|
|
48
|
-
}));
|
|
49
|
-
});
|
|
50
46
|
const normalizeCapturedDocument = (document) => {
|
|
51
47
|
const { origin: _origin, ...rest } = document;
|
|
52
48
|
const normalizedPath = stripFileUri(document.initialPath ?? document.path);
|
|
@@ -57,61 +53,18 @@ const normalizeCapturedDocument = (document) => {
|
|
|
57
53
|
croppedPath: document.croppedPath ? stripFileUri(document.croppedPath) : null,
|
|
58
54
|
};
|
|
59
55
|
};
|
|
60
|
-
const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3170f3', gridColor, gridLineWidth, showGrid,
|
|
61
|
-
const [screen, setScreen] = (0, react_1.useState)('scanner');
|
|
62
|
-
const [capturedDoc, setCapturedDoc] = (0, react_1.useState)(null);
|
|
63
|
-
const [cropRectangle, setCropRectangle] = (0, react_1.useState)(null);
|
|
64
|
-
const [imageSize, setImageSize] = (0, react_1.useState)(null);
|
|
56
|
+
const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3170f3', gridColor, gridLineWidth, showGrid, strings, manualCapture = false, minStableFrames, onError, enableGallery = true, cropWidth = 1200, cropHeight = 1600, }) => {
|
|
65
57
|
const [processing, setProcessing] = (0, react_1.useState)(false);
|
|
66
58
|
const resolvedGridColor = gridColor ?? overlayColor;
|
|
67
59
|
const docScannerRef = (0, react_1.useRef)(null);
|
|
68
60
|
const manualCapturePending = (0, react_1.useRef)(false);
|
|
69
|
-
const processingCaptureRef = (0, react_1.useRef)(false);
|
|
70
|
-
const cropInitializedRef = (0, react_1.useRef)(false);
|
|
71
61
|
const mergedStrings = (0, react_1.useMemo)(() => ({
|
|
72
62
|
captureHint: strings?.captureHint,
|
|
73
63
|
manualHint: strings?.manualHint,
|
|
74
64
|
cancel: strings?.cancel,
|
|
75
|
-
confirm: strings?.confirm,
|
|
76
|
-
retake: strings?.retake,
|
|
77
|
-
cropTitle: strings?.cropTitle,
|
|
78
65
|
processing: strings?.processing,
|
|
66
|
+
galleryButton: strings?.galleryButton,
|
|
79
67
|
}), [strings]);
|
|
80
|
-
(0, react_1.useEffect)(() => {
|
|
81
|
-
if (!capturedDoc) {
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
react_native_1.Image.getSize(ensureFileUri(capturedDoc.path), (width, height) => setImageSize({ width, height }), () => setImageSize({ width: capturedDoc.width, height: capturedDoc.height }));
|
|
85
|
-
}, [capturedDoc]);
|
|
86
|
-
(0, react_1.useEffect)(() => {
|
|
87
|
-
if (!capturedDoc || !imageSize || cropInitializedRef.current) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const baseWidth = capturedDoc.width > 0 ? capturedDoc.width : imageSize.width;
|
|
91
|
-
const baseHeight = capturedDoc.height > 0 ? capturedDoc.height : imageSize.height;
|
|
92
|
-
let initialRectangle = null;
|
|
93
|
-
if (capturedDoc.rectangle) {
|
|
94
|
-
initialRectangle = (0, coordinate_1.scaleRectangle)(capturedDoc.rectangle, baseWidth, baseHeight, imageSize.width, imageSize.height);
|
|
95
|
-
}
|
|
96
|
-
else if (capturedDoc.quad && capturedDoc.quad.length === 4) {
|
|
97
|
-
const quadRectangle = (0, coordinate_1.quadToRectangle)(capturedDoc.quad);
|
|
98
|
-
if (quadRectangle) {
|
|
99
|
-
initialRectangle = (0, coordinate_1.scaleRectangle)(quadRectangle, baseWidth, baseHeight, imageSize.width, imageSize.height);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
cropInitializedRef.current = true;
|
|
103
|
-
setCropRectangle(initialRectangle ?? (0, coordinate_1.createFullImageRectangle)(imageSize.width || 1, imageSize.height || 1));
|
|
104
|
-
}, [capturedDoc, imageSize]);
|
|
105
|
-
const resetState = (0, react_1.useCallback)(() => {
|
|
106
|
-
setScreen('scanner');
|
|
107
|
-
setCapturedDoc(null);
|
|
108
|
-
setCropRectangle(null);
|
|
109
|
-
setImageSize(null);
|
|
110
|
-
setProcessing(false);
|
|
111
|
-
manualCapturePending.current = false;
|
|
112
|
-
processingCaptureRef.current = false;
|
|
113
|
-
cropInitializedRef.current = false;
|
|
114
|
-
}, []);
|
|
115
68
|
const emitError = (0, react_1.useCallback)((error, fallbackMessage) => {
|
|
116
69
|
console.error('[FullDocScanner] error', error);
|
|
117
70
|
onError?.(error);
|
|
@@ -119,108 +72,48 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
119
72
|
react_native_1.Alert.alert('Document Scanner', fallbackMessage);
|
|
120
73
|
}
|
|
121
74
|
}, [onError]);
|
|
122
|
-
const
|
|
123
|
-
console.log('[FullDocScanner] processAutoCapture started');
|
|
124
|
-
manualCapturePending.current = false;
|
|
125
|
-
const normalizedDoc = normalizeCapturedDocument(document);
|
|
126
|
-
const cropManager = react_native_1.NativeModules.CustomCropManager;
|
|
127
|
-
if (!cropManager?.crop) {
|
|
128
|
-
console.error('[FullDocScanner] CustomCropManager.crop is not available');
|
|
129
|
-
emitError(new Error('CustomCropManager.crop is not available'));
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
console.log('[FullDocScanner] Setting processing to true');
|
|
133
|
-
setProcessing(true);
|
|
75
|
+
const openCropper = (0, react_1.useCallback)(async (imagePath) => {
|
|
134
76
|
try {
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const scaledRectangle = rectangleBase
|
|
147
|
-
? (0, coordinate_1.scaleRectangle)(rectangleBase, baseWidth || targetWidth, baseHeight || targetHeight, targetWidth, targetHeight)
|
|
148
|
-
: null;
|
|
149
|
-
const rectangleToUse = scaledRectangle ?? (0, coordinate_1.createFullImageRectangle)(targetWidth, targetHeight);
|
|
150
|
-
console.log('[FullDocScanner] Calling CustomCropManager.crop with:', {
|
|
151
|
-
rectangle: rectangleToUse,
|
|
152
|
-
imageUri: ensureFileUri(normalizedDoc.path),
|
|
153
|
-
targetSize: { width: targetWidth, height: targetHeight },
|
|
154
|
-
});
|
|
155
|
-
const base64 = await new Promise((resolve, reject) => {
|
|
156
|
-
cropManager.crop({
|
|
157
|
-
topLeft: rectangleToUse.topLeft,
|
|
158
|
-
topRight: rectangleToUse.topRight,
|
|
159
|
-
bottomRight: rectangleToUse.bottomRight,
|
|
160
|
-
bottomLeft: rectangleToUse.bottomLeft,
|
|
161
|
-
width: targetWidth,
|
|
162
|
-
height: targetHeight,
|
|
163
|
-
}, ensureFileUri(normalizedDoc.path), (error, result) => {
|
|
164
|
-
if (error) {
|
|
165
|
-
console.error('[FullDocScanner] CustomCropManager.crop error:', error);
|
|
166
|
-
reject(error instanceof Error ? error : new Error('Crop failed'));
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
console.log('[FullDocScanner] CustomCropManager.crop success, base64 length:', result.image?.length);
|
|
170
|
-
resolve(result.image);
|
|
171
|
-
});
|
|
77
|
+
setProcessing(true);
|
|
78
|
+
const croppedImage = await react_native_image_crop_picker_1.default.openCropper({
|
|
79
|
+
path: imagePath,
|
|
80
|
+
mediaType: 'photo',
|
|
81
|
+
width: cropWidth,
|
|
82
|
+
height: cropHeight,
|
|
83
|
+
cropping: true,
|
|
84
|
+
cropperToolbarTitle: 'Crop Document',
|
|
85
|
+
freeStyleCropEnabled: true,
|
|
86
|
+
includeBase64: true,
|
|
87
|
+
compressImageQuality: 0.9,
|
|
172
88
|
});
|
|
173
|
-
|
|
174
|
-
...normalizedDoc,
|
|
175
|
-
rectangle: rectangleToUse,
|
|
176
|
-
};
|
|
177
|
-
console.log('[FullDocScanner] Calling onResult with base64 length:', base64?.length);
|
|
89
|
+
setProcessing(false);
|
|
178
90
|
onResult({
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
base64,
|
|
91
|
+
path: croppedImage.path,
|
|
92
|
+
base64: croppedImage.data ?? undefined,
|
|
182
93
|
});
|
|
183
|
-
console.log('[FullDocScanner] Resetting state');
|
|
184
|
-
resetState();
|
|
185
94
|
}
|
|
186
95
|
catch (error) {
|
|
187
96
|
setProcessing(false);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
processingCaptureRef.current = false;
|
|
97
|
+
if (error?.message !== 'User cancelled image selection') {
|
|
98
|
+
emitError(error instanceof Error ? error : new Error(String(error)), 'Failed to crop image.');
|
|
99
|
+
}
|
|
192
100
|
}
|
|
193
|
-
}, [emitError, onResult
|
|
194
|
-
const handleCapture = (0, react_1.useCallback)((document) => {
|
|
101
|
+
}, [cropWidth, cropHeight, emitError, onResult]);
|
|
102
|
+
const handleCapture = (0, react_1.useCallback)(async (document) => {
|
|
195
103
|
console.log('[FullDocScanner] handleCapture called:', {
|
|
196
104
|
origin: document.origin,
|
|
197
105
|
path: document.path,
|
|
198
|
-
width: document.width,
|
|
199
|
-
height: document.height,
|
|
200
|
-
hasQuad: !!document.quad,
|
|
201
|
-
hasRectangle: !!document.rectangle,
|
|
202
106
|
});
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
return;
|
|
107
|
+
if (manualCapturePending.current) {
|
|
108
|
+
manualCapturePending.current = false;
|
|
206
109
|
}
|
|
207
110
|
const normalizedDoc = normalizeCapturedDocument(document);
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
processingCaptureRef.current = false;
|
|
212
|
-
cropInitializedRef.current = false;
|
|
213
|
-
setCapturedDoc(normalizedDoc);
|
|
214
|
-
setImageSize(null);
|
|
215
|
-
setCropRectangle(null);
|
|
216
|
-
setScreen('crop');
|
|
217
|
-
}, []);
|
|
218
|
-
const handleCropChange = (0, react_1.useCallback)((rectangle) => {
|
|
219
|
-
setCropRectangle(rectangle);
|
|
220
|
-
}, []);
|
|
111
|
+
// Open cropper with the captured image
|
|
112
|
+
await openCropper(normalizedDoc.path);
|
|
113
|
+
}, [openCropper]);
|
|
221
114
|
const triggerManualCapture = (0, react_1.useCallback)(() => {
|
|
222
115
|
console.log('[FullDocScanner] triggerManualCapture called');
|
|
223
|
-
if (
|
|
116
|
+
if (processing) {
|
|
224
117
|
console.log('[FullDocScanner] Already processing, skipping manual capture');
|
|
225
118
|
return;
|
|
226
119
|
}
|
|
@@ -231,7 +124,6 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
231
124
|
console.log('[FullDocScanner] Setting manualCapturePending to true');
|
|
232
125
|
manualCapturePending.current = true;
|
|
233
126
|
const capturePromise = docScannerRef.current?.capture();
|
|
234
|
-
console.log('[FullDocScanner] capturePromise:', !!capturePromise);
|
|
235
127
|
if (capturePromise && typeof capturePromise.then === 'function') {
|
|
236
128
|
capturePromise
|
|
237
129
|
.then(() => {
|
|
@@ -246,82 +138,36 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
246
138
|
console.warn('[FullDocScanner] No capture promise returned');
|
|
247
139
|
manualCapturePending.current = false;
|
|
248
140
|
}
|
|
249
|
-
}, []);
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
const size = imageSize ?? { width: capturedDoc.width, height: capturedDoc.height };
|
|
255
|
-
const cropManager = react_native_1.NativeModules.CustomCropManager;
|
|
256
|
-
if (!cropManager?.crop) {
|
|
257
|
-
throw new Error('CustomCropManager.crop is not available');
|
|
258
|
-
}
|
|
259
|
-
const baseWidth = capturedDoc.width > 0 ? capturedDoc.width : size.width;
|
|
260
|
-
const baseHeight = capturedDoc.height > 0 ? capturedDoc.height : size.height;
|
|
261
|
-
const targetWidth = size.width > 0 ? size.width : baseWidth || 1;
|
|
262
|
-
const targetHeight = size.height > 0 ? size.height : baseHeight || 1;
|
|
263
|
-
let fallbackRectangle = null;
|
|
264
|
-
if (capturedDoc.rectangle) {
|
|
265
|
-
fallbackRectangle = (0, coordinate_1.scaleRectangle)(capturedDoc.rectangle, baseWidth || targetWidth, baseHeight || targetHeight, targetWidth, targetHeight);
|
|
266
|
-
}
|
|
267
|
-
else if (capturedDoc.quad && capturedDoc.quad.length === 4) {
|
|
268
|
-
const quadRectangle = (0, coordinate_1.quadToRectangle)(capturedDoc.quad);
|
|
269
|
-
if (quadRectangle) {
|
|
270
|
-
fallbackRectangle = (0, coordinate_1.scaleRectangle)(quadRectangle, baseWidth || targetWidth, baseHeight || targetHeight, targetWidth, targetHeight);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
const rectangleToUse = cropRectangle ?? fallbackRectangle ?? (0, coordinate_1.createFullImageRectangle)(targetWidth, targetHeight);
|
|
274
|
-
const base64 = await new Promise((resolve, reject) => {
|
|
275
|
-
cropManager.crop({
|
|
276
|
-
topLeft: rectangleToUse.topLeft,
|
|
277
|
-
topRight: rectangleToUse.topRight,
|
|
278
|
-
bottomRight: rectangleToUse.bottomRight,
|
|
279
|
-
bottomLeft: rectangleToUse.bottomLeft,
|
|
280
|
-
width: targetWidth,
|
|
281
|
-
height: targetHeight,
|
|
282
|
-
}, ensureFileUri(capturedDoc.path), (error, result) => {
|
|
283
|
-
if (error) {
|
|
284
|
-
reject(error instanceof Error ? error : new Error('Crop failed'));
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
resolve(result.image);
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
return { base64, rectangle: rectangleToUse };
|
|
291
|
-
}, [capturedDoc, cropRectangle, imageSize]);
|
|
292
|
-
const handleConfirm = (0, react_1.useCallback)(async () => {
|
|
293
|
-
if (!capturedDoc) {
|
|
141
|
+
}, [processing]);
|
|
142
|
+
const handleGalleryPick = (0, react_1.useCallback)(async () => {
|
|
143
|
+
console.log('[FullDocScanner] handleGalleryPick called');
|
|
144
|
+
if (processing) {
|
|
294
145
|
return;
|
|
295
146
|
}
|
|
296
147
|
try {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
...capturedDoc,
|
|
302
|
-
rectangle,
|
|
303
|
-
};
|
|
304
|
-
onResult({
|
|
305
|
-
original: finalDoc,
|
|
306
|
-
rectangle,
|
|
307
|
-
base64,
|
|
148
|
+
const result = await (0, react_native_image_picker_1.launchImageLibrary)({
|
|
149
|
+
mediaType: 'photo',
|
|
150
|
+
quality: 1,
|
|
151
|
+
selectionLimit: 1,
|
|
308
152
|
});
|
|
309
|
-
|
|
153
|
+
if (result.didCancel || !result.assets?.[0]?.uri) {
|
|
154
|
+
console.log('[FullDocScanner] User cancelled gallery picker');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const imageUri = result.assets[0].uri;
|
|
158
|
+
console.log('[FullDocScanner] Gallery image selected:', imageUri);
|
|
159
|
+
// Open cropper with the selected image
|
|
160
|
+
await openCropper(imageUri);
|
|
310
161
|
}
|
|
311
162
|
catch (error) {
|
|
312
|
-
|
|
313
|
-
emitError(error instanceof Error ? error : new Error(String(error)), 'Failed to process document.');
|
|
163
|
+
emitError(error instanceof Error ? error : new Error(String(error)), 'Failed to pick image from gallery.');
|
|
314
164
|
}
|
|
315
|
-
}, [
|
|
316
|
-
const handleRetake = (0, react_1.useCallback)(() => {
|
|
317
|
-
resetState();
|
|
318
|
-
}, [resetState]);
|
|
165
|
+
}, [processing, openCropper, emitError]);
|
|
319
166
|
const handleClose = (0, react_1.useCallback)(() => {
|
|
320
|
-
resetState();
|
|
321
167
|
onClose?.();
|
|
322
|
-
}, [onClose
|
|
168
|
+
}, [onClose]);
|
|
323
169
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
324
|
-
|
|
170
|
+
react_1.default.createElement(react_native_1.View, { style: styles.flex },
|
|
325
171
|
react_1.default.createElement(DocScanner_1.DocScanner, { ref: docScannerRef, autoCapture: !manualCapture, overlayColor: overlayColor, showGrid: showGrid, gridColor: resolvedGridColor, gridLineWidth: gridLineWidth, minStableFrames: minStableFrames ?? 6, detectionConfig: detectionConfig, onCapture: handleCapture, showManualCaptureButton: false },
|
|
326
172
|
react_1.default.createElement(react_native_1.View, { style: styles.overlayTop, pointerEvents: "box-none" },
|
|
327
173
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.closeButton, onPress: handleClose, accessibilityLabel: mergedStrings.cancel, accessibilityRole: "button" },
|
|
@@ -331,15 +177,12 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
331
177
|
mergedStrings.captureHint && (react_1.default.createElement(react_native_1.Text, { style: styles.captureText }, mergedStrings.captureHint)),
|
|
332
178
|
mergedStrings.manualHint && (react_1.default.createElement(react_native_1.Text, { style: styles.captureText }, mergedStrings.manualHint))))),
|
|
333
179
|
react_1.default.createElement(react_native_1.View, { style: styles.shutterContainer, pointerEvents: "box-none" },
|
|
334
|
-
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.
|
|
335
|
-
react_1.default.createElement(react_native_1.
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
react_1.default.createElement(react_native_1.View, { style: styles.cropFooter },
|
|
339
|
-
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.actionButton, styles.secondaryButton], onPress: handleRetake }, mergedStrings.retake && react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, mergedStrings.retake)),
|
|
340
|
-
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.actionButton, styles.primaryButton], onPress: handleConfirm }, mergedStrings.confirm && react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, mergedStrings.confirm))))),
|
|
180
|
+
enableGallery && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.galleryButton, processing && styles.buttonDisabled], onPress: handleGalleryPick, disabled: processing, accessibilityLabel: mergedStrings.galleryButton, accessibilityRole: "button" },
|
|
181
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.galleryButtonText }, "\uD83D\uDCC1"))),
|
|
182
|
+
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.shutterButton, processing && styles.buttonDisabled], onPress: triggerManualCapture, disabled: processing, accessibilityLabel: mergedStrings.manualHint, accessibilityRole: "button" },
|
|
183
|
+
react_1.default.createElement(react_native_1.View, { style: styles.shutterInner }))))),
|
|
341
184
|
processing && (react_1.default.createElement(react_native_1.View, { style: styles.processingOverlay },
|
|
342
|
-
react_1.default.createElement(react_native_1.ActivityIndicator, { size: "large", color:
|
|
185
|
+
react_1.default.createElement(react_native_1.ActivityIndicator, { size: "large", color: overlayColor }),
|
|
343
186
|
mergedStrings.processing && (react_1.default.createElement(react_native_1.Text, { style: styles.processingText }, mergedStrings.processing))))));
|
|
344
187
|
};
|
|
345
188
|
exports.FullDocScanner = FullDocScanner;
|
|
@@ -370,7 +213,10 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
370
213
|
bottom: 64,
|
|
371
214
|
left: 0,
|
|
372
215
|
right: 0,
|
|
216
|
+
flexDirection: 'row',
|
|
217
|
+
justifyContent: 'center',
|
|
373
218
|
alignItems: 'center',
|
|
219
|
+
gap: 24,
|
|
374
220
|
zIndex: 10,
|
|
375
221
|
},
|
|
376
222
|
closeButton: {
|
|
@@ -398,6 +244,19 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
398
244
|
fontSize: 15,
|
|
399
245
|
textAlign: 'center',
|
|
400
246
|
},
|
|
247
|
+
galleryButton: {
|
|
248
|
+
width: 60,
|
|
249
|
+
height: 60,
|
|
250
|
+
borderRadius: 30,
|
|
251
|
+
borderWidth: 3,
|
|
252
|
+
borderColor: '#fff',
|
|
253
|
+
justifyContent: 'center',
|
|
254
|
+
alignItems: 'center',
|
|
255
|
+
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
256
|
+
},
|
|
257
|
+
galleryButtonText: {
|
|
258
|
+
fontSize: 28,
|
|
259
|
+
},
|
|
401
260
|
shutterButton: {
|
|
402
261
|
width: 80,
|
|
403
262
|
height: 80,
|
|
@@ -408,7 +267,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
408
267
|
alignItems: 'center',
|
|
409
268
|
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
410
269
|
},
|
|
411
|
-
|
|
270
|
+
buttonDisabled: {
|
|
412
271
|
opacity: 0.4,
|
|
413
272
|
},
|
|
414
273
|
shutterInner: {
|
|
@@ -417,34 +276,6 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
417
276
|
borderRadius: 30,
|
|
418
277
|
backgroundColor: '#fff',
|
|
419
278
|
},
|
|
420
|
-
cropFooter: {
|
|
421
|
-
position: 'absolute',
|
|
422
|
-
bottom: 40,
|
|
423
|
-
left: 20,
|
|
424
|
-
right: 20,
|
|
425
|
-
flexDirection: 'row',
|
|
426
|
-
justifyContent: 'space-between',
|
|
427
|
-
},
|
|
428
|
-
actionButton: {
|
|
429
|
-
flex: 1,
|
|
430
|
-
paddingVertical: 14,
|
|
431
|
-
borderRadius: 12,
|
|
432
|
-
alignItems: 'center',
|
|
433
|
-
marginHorizontal: 6,
|
|
434
|
-
},
|
|
435
|
-
secondaryButton: {
|
|
436
|
-
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
437
|
-
borderWidth: 1,
|
|
438
|
-
borderColor: 'rgba(255,255,255,0.35)',
|
|
439
|
-
},
|
|
440
|
-
primaryButton: {
|
|
441
|
-
backgroundColor: '#3170f3',
|
|
442
|
-
},
|
|
443
|
-
buttonText: {
|
|
444
|
-
color: '#fff',
|
|
445
|
-
fontSize: 16,
|
|
446
|
-
fontWeight: '600',
|
|
447
|
-
},
|
|
448
279
|
processingOverlay: {
|
|
449
280
|
...react_native_1.StyleSheet.absoluteFillObject,
|
|
450
281
|
backgroundColor: 'rgba(0,0,0,0.65)',
|