react-native-rectangle-doc-scanner 15.2.0 → 15.4.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 +679 -9
- package/dist/FullDocScanner.js +52 -4
- package/package.json +1 -1
- package/src/FullDocScanner.tsx +60 -4
package/README.md
CHANGED
|
@@ -1,8 +1,446 @@
|
|
|
1
1
|
# React Native Document Scanner Wrapper
|
|
2
2
|
|
|
3
|
+
[English](#english-version) | [한국어](#한국어-버전)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 한국어 버전
|
|
8
|
+
|
|
9
|
+
React Native용 문서 스캐너 라이브러리입니다. [`react-native-document-scanner`](https://github.com/Michaelvilleneuve/react-native-document-scanner)를 래핑하여 iOS와 Android 모두에서 네이티브 문서 스캐너를 제공합니다.
|
|
10
|
+
|
|
11
|
+
> 네이티브 구현은 업스트림 라이브러리에 포함되어 있습니다 (iOS: Objective-C/OpenCV, Android: Kotlin/OpenCV). 이 패키지는 타입 안전한 래퍼, 선택적 크롭 에디터 헬퍼, 전체 화면 스캐너를 제공합니다.
|
|
12
|
+
|
|
13
|
+
## ✨ 전문가급 카메라 품질 (v3.2+)
|
|
14
|
+
|
|
15
|
+
**주요 업데이트:** 최신 `AVCapturePhotoOutput` API로 업그레이드되어 이미지 품질이 대폭 향상되었습니다!
|
|
16
|
+
|
|
17
|
+
### 🚀 새로운 기능:
|
|
18
|
+
- **최신 카메라 API** - 구형 `AVCaptureStillImageOutput` 대신 `AVCapturePhotoOutput` (iOS 10+) 사용
|
|
19
|
+
- **iPhone 네이티브 품질** - 기본 카메라 앱과 동일한 품질
|
|
20
|
+
- **컴퓨테이셔널 포토그래피** - 자동 HDR, Deep Fusion, Smart HDR 지원
|
|
21
|
+
- **12MP+ 해상도** - 최신 iPhone에서 전체 해상도 캡처 (iPhone 14 Pro+ 기준 최대 48MP)
|
|
22
|
+
- **최대 품질 우선순위** - iOS 13+ 품질 우선순위 활성화
|
|
23
|
+
- **95%+ JPEG 품질** - 품질 손실 방지를 위한 최소 압축 품질 강제
|
|
24
|
+
|
|
25
|
+
### 🎯 자동 최적화:
|
|
26
|
+
- **고해상도 캡처** - 전체 센서 해상도 활성화 (`AVCaptureSessionPresetHigh`)
|
|
27
|
+
- **최소 95% JPEG** - 압축으로 인한 품질 저하 방지
|
|
28
|
+
- **고급 기능**:
|
|
29
|
+
- 더 선명한 이미지를 위한 비디오 안정화
|
|
30
|
+
- 항상 선명한 캡처를 위한 연속 자동 초점
|
|
31
|
+
- 자동 노출 및 화이트 밸런스
|
|
32
|
+
- 어두운 환경에서 저조도 부스트
|
|
33
|
+
- **하드웨어 가속** - 효율적인 처리를 위한 CIContext
|
|
34
|
+
|
|
35
|
+
### ⚡ 완전 자동 설치:
|
|
36
|
+
yarn/npm으로 설치하기만 하면 됩니다 - **수동 설정 불필요!**
|
|
37
|
+
- Postinstall 스크립트가 자동으로 카메라 품질 패치
|
|
38
|
+
- 설치 중 iOS 최적화 파일 자동 복사
|
|
39
|
+
- `pod install` 후 즉시 사용 가능
|
|
40
|
+
|
|
41
|
+
## 설치 방법
|
|
42
|
+
|
|
43
|
+
### 1. 패키지 설치
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
yarn add react-native-rectangle-doc-scanner \
|
|
47
|
+
github:Michaelvilleneuve/react-native-document-scanner \
|
|
48
|
+
react-native-perspective-image-cropper
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
또는 npm 사용:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install react-native-rectangle-doc-scanner \
|
|
55
|
+
github:Michaelvilleneuve/react-native-document-scanner \
|
|
56
|
+
react-native-perspective-image-cropper
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Peer Dependencies 설치
|
|
60
|
+
|
|
61
|
+
이 라이브러리는 다음 peer dependencies를 필요로 합니다:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
yarn add react-native-fs \
|
|
65
|
+
react-native-image-crop-picker \
|
|
66
|
+
react-native-image-picker \
|
|
67
|
+
react-native-svg \
|
|
68
|
+
expo-modules-core
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
또는 npm 사용:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install react-native-fs \
|
|
75
|
+
react-native-image-crop-picker \
|
|
76
|
+
react-native-image-picker \
|
|
77
|
+
react-native-svg \
|
|
78
|
+
expo-modules-core
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**선택사항 (이미지 회전 기능을 사용하려면):**
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# 둘 중 하나 선택
|
|
85
|
+
yarn add expo-image-manipulator
|
|
86
|
+
# 또는
|
|
87
|
+
yarn add react-native-image-rotate
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 2-1. Babel 및 Reanimated 설정 (필요시)
|
|
91
|
+
|
|
92
|
+
프로젝트에 `babel.config.js` 파일이 있는 경우, 다음 플러그인이 필요할 수 있습니다:
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
module.exports = {
|
|
96
|
+
presets: ['module:@react-native/babel-preset'],
|
|
97
|
+
plugins: [
|
|
98
|
+
'react-native-reanimated/plugin' // 마지막에 위치해야 함
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**필요한 경우 추가 패키지:**
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
yarn add react-native-reanimated
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 3. iOS 설정
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
cd ios && pod install && cd ..
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Info.plist에 카메라 권한 추가:**
|
|
116
|
+
|
|
117
|
+
`ios/YourApp/Info.plist` 파일에 다음 권한을 추가하세요:
|
|
118
|
+
|
|
119
|
+
```xml
|
|
120
|
+
<key>NSCameraUsageDescription</key>
|
|
121
|
+
<string>문서를 스캔하기 위해 카메라 접근이 필요합니다</string>
|
|
122
|
+
<key>NSPhotoLibraryUsageDescription</key>
|
|
123
|
+
<string>스캔한 문서를 저장하기 위해 사진 라이브러리 접근이 필요합니다</string>
|
|
124
|
+
<key>NSPhotoLibraryAddUsageDescription</key>
|
|
125
|
+
<string>스캔한 문서를 저장하기 위해 사진 라이브러리 접근이 필요합니다</string>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 4. Android 설정
|
|
129
|
+
|
|
130
|
+
Android는 자동으로 네이티브 모듈을 링크합니다. 레거시 아키텍처를 사용하는 경우, `MainApplication.java`에서 `DocumentScannerPackage()`를 수동으로 등록해야 합니다.
|
|
131
|
+
|
|
132
|
+
**AndroidManifest.xml에 권한 추가:**
|
|
133
|
+
|
|
134
|
+
`android/app/src/main/AndroidManifest.xml` 파일에 다음 권한이 자동으로 포함됩니다:
|
|
135
|
+
|
|
136
|
+
```xml
|
|
137
|
+
<uses-permission android:name="android.permission.CAMERA" />
|
|
138
|
+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
|
139
|
+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
|
140
|
+
|
|
141
|
+
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
|
142
|
+
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
|
143
|
+
<uses-feature android:name="android.hardware.camera.flash" android:required="false" />
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Gradle 설정:**
|
|
147
|
+
|
|
148
|
+
라이브러리는 다음 최소 요구사항을 가지고 있습니다:
|
|
149
|
+
- `minSdkVersion`: 21
|
|
150
|
+
- `compileSdkVersion`: 33
|
|
151
|
+
- `targetSdkVersion`: 33
|
|
152
|
+
- Kotlin: 1.8.21
|
|
153
|
+
- Java: 17
|
|
154
|
+
|
|
155
|
+
이 설정은 자동으로 적용되지만, 프로젝트의 `android/build.gradle`에서 호환되는 버전을 사용하는지 확인하세요.
|
|
156
|
+
|
|
157
|
+
**프로젝트의 `android/build.gradle` 예시:**
|
|
158
|
+
|
|
159
|
+
```gradle
|
|
160
|
+
buildscript {
|
|
161
|
+
ext {
|
|
162
|
+
buildToolsVersion = "33.0.0"
|
|
163
|
+
minSdkVersion = 21
|
|
164
|
+
compileSdkVersion = 33
|
|
165
|
+
targetSdkVersion = 33
|
|
166
|
+
kotlinVersion = "1.8.21"
|
|
167
|
+
}
|
|
168
|
+
dependencies {
|
|
169
|
+
classpath("com.android.tools.build:gradle:7.4.2")
|
|
170
|
+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**프로젝트의 `android/app/build.gradle` 예시:**
|
|
176
|
+
|
|
177
|
+
```gradle
|
|
178
|
+
android {
|
|
179
|
+
compileSdkVersion rootProject.ext.compileSdkVersion
|
|
180
|
+
|
|
181
|
+
compileOptions {
|
|
182
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
183
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
kotlinOptions {
|
|
187
|
+
jvmTarget = '17'
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
defaultConfig {
|
|
191
|
+
minSdkVersion rootProject.ext.minSdkVersion
|
|
192
|
+
targetSdkVersion rootProject.ext.targetSdkVersion
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 5. 자동 품질 패치 (Postinstall)
|
|
198
|
+
|
|
199
|
+
이 라이브러리는 **postinstall 스크립트**를 통해 자동으로 카메라 품질을 최적화합니다:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# 패키지 설치 시 자동 실행됨
|
|
203
|
+
node scripts/postinstall.js
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**postinstall이 하는 일:**
|
|
207
|
+
1. `react-native-document-scanner` 패키지를 찾습니다 (node_modules에서 자동 감지)
|
|
208
|
+
2. vendor 폴더의 최적화된 iOS 파일들을 복사합니다:
|
|
209
|
+
- `IPDFCameraViewController.m/h` - AVCapturePhotoOutput 사용
|
|
210
|
+
- `DocumentScannerView.m/h` - 고품질 설정
|
|
211
|
+
- `RNPdfScannerManager.m/h` - 네이티브 브릿지
|
|
212
|
+
- `ios.js`, `index.js` - JavaScript 인터페이스
|
|
213
|
+
3. 원본 파일은 `.original` 확장자로 백업됩니다
|
|
214
|
+
|
|
215
|
+
**수동으로 실행하려면:**
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
npm run postinstall
|
|
219
|
+
# 또는
|
|
220
|
+
node scripts/postinstall.js
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**문제 해결:**
|
|
224
|
+
- postinstall이 실패하는 경우, `react-native-document-scanner`가 설치되어 있는지 확인하세요
|
|
225
|
+
- yarn workspaces나 monorepo를 사용하는 경우, 패키지 호이스팅으로 인해 경로가 다를 수 있습니다
|
|
226
|
+
|
|
227
|
+
### 6. 런타임 권한 요청
|
|
228
|
+
|
|
229
|
+
앱에서 런타임에 카메라 권한을 요청해야 합니다:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { PermissionsAndroid, Platform } from 'react-native';
|
|
233
|
+
|
|
234
|
+
async function requestCameraPermission() {
|
|
235
|
+
if (Platform.OS === 'android') {
|
|
236
|
+
try {
|
|
237
|
+
const granted = await PermissionsAndroid.request(
|
|
238
|
+
PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
239
|
+
{
|
|
240
|
+
title: '카메라 권한',
|
|
241
|
+
message: '문서를 스캔하기 위해 카메라 접근이 필요합니다',
|
|
242
|
+
buttonNeutral: '나중에',
|
|
243
|
+
buttonNegative: '거부',
|
|
244
|
+
buttonPositive: '허용',
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.warn(err);
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## 사용 방법
|
|
258
|
+
|
|
259
|
+
### 기본 사용 예제
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
import React, { useRef } from 'react';
|
|
263
|
+
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
264
|
+
import { DocScanner, type DocScannerHandle } from 'react-native-rectangle-doc-scanner';
|
|
265
|
+
|
|
266
|
+
export const ScanScreen = () => {
|
|
267
|
+
const scannerRef = useRef<DocScannerHandle>(null);
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<View style={styles.container}>
|
|
271
|
+
<DocScanner
|
|
272
|
+
ref={scannerRef}
|
|
273
|
+
overlayColor="rgba(0, 126, 244, 0.35)"
|
|
274
|
+
autoCapture
|
|
275
|
+
minStableFrames={6}
|
|
276
|
+
onCapture={(result) => {
|
|
277
|
+
console.log('문서 캡처됨:', result.path);
|
|
278
|
+
console.log('크기:', result.width, 'x', result.height);
|
|
279
|
+
}}
|
|
280
|
+
>
|
|
281
|
+
<View style={styles.overlay} pointerEvents="none">
|
|
282
|
+
<Text style={styles.hint}>프레임 안에 문서를 정렬하세요</Text>
|
|
283
|
+
</View>
|
|
284
|
+
</DocScanner>
|
|
285
|
+
|
|
286
|
+
<TouchableOpacity
|
|
287
|
+
style={styles.captureButton}
|
|
288
|
+
onPress={() => scannerRef.current?.capture()}
|
|
289
|
+
>
|
|
290
|
+
<Text style={styles.captureButtonText}>촬영</Text>
|
|
291
|
+
</TouchableOpacity>
|
|
292
|
+
</View>
|
|
293
|
+
);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const styles = StyleSheet.create({
|
|
297
|
+
container: {
|
|
298
|
+
flex: 1,
|
|
299
|
+
backgroundColor: '#000'
|
|
300
|
+
},
|
|
301
|
+
overlay: {
|
|
302
|
+
position: 'absolute',
|
|
303
|
+
top: 60,
|
|
304
|
+
alignSelf: 'center',
|
|
305
|
+
paddingHorizontal: 20,
|
|
306
|
+
paddingVertical: 10,
|
|
307
|
+
borderRadius: 12,
|
|
308
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
309
|
+
},
|
|
310
|
+
hint: {
|
|
311
|
+
color: '#fff',
|
|
312
|
+
fontWeight: '600'
|
|
313
|
+
},
|
|
314
|
+
captureButton: {
|
|
315
|
+
position: 'absolute',
|
|
316
|
+
bottom: 40,
|
|
317
|
+
alignSelf: 'center',
|
|
318
|
+
width: 70,
|
|
319
|
+
height: 70,
|
|
320
|
+
borderRadius: 35,
|
|
321
|
+
backgroundColor: '#fff',
|
|
322
|
+
justifyContent: 'center',
|
|
323
|
+
alignItems: 'center',
|
|
324
|
+
},
|
|
325
|
+
captureButtonText: {
|
|
326
|
+
color: '#000',
|
|
327
|
+
fontWeight: '600',
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Props
|
|
333
|
+
|
|
334
|
+
`<DocScanner />` 컴포넌트는 다음 props를 지원합니다:
|
|
335
|
+
|
|
336
|
+
| Prop | 타입 | 기본값 | 설명 |
|
|
337
|
+
| --- | --- | --- | --- |
|
|
338
|
+
| `overlayColor` | `string` | `#0b7ef4` | 네이티브 오버레이 색상 |
|
|
339
|
+
| `autoCapture` | `boolean` | `true` | 자동 캡처 활성화 (내부적으로 `manualOnly`로 매핑됨) |
|
|
340
|
+
| `minStableFrames` | `number` | `8` | 자동 캡처 전 필요한 안정적인 프레임 수 |
|
|
341
|
+
| `enableTorch` | `boolean` | `false` | 플래시 켜기/끄기 |
|
|
342
|
+
| `quality` | `number` | `90` | 이미지 품질 (0–100, 네이티브용으로 변환됨) |
|
|
343
|
+
| `useBase64` | `boolean` | `false` | 파일 URI 대신 base64로 반환 |
|
|
344
|
+
| `onCapture` | `(result) => void` | — | `{ path, quad: null, width, height }` 객체를 전달받음 |
|
|
345
|
+
|
|
346
|
+
### 수동 캡처
|
|
347
|
+
|
|
348
|
+
ref를 통해 `capture()` 메서드를 사용하여 수동으로 캡처할 수 있습니다. children을 사용하여 카메라 프리뷰 위에 커스텀 UI(버튼, 진행 표시기, 온보딩 팁 등)를 렌더링할 수 있습니다.
|
|
349
|
+
|
|
350
|
+
## 추가 API
|
|
351
|
+
|
|
352
|
+
### CropEditor
|
|
353
|
+
|
|
354
|
+
`react-native-perspective-image-cropper`를 래핑하여 수동으로 모서리를 조정할 수 있는 크롭 에디터를 제공합니다.
|
|
355
|
+
|
|
356
|
+
```tsx
|
|
357
|
+
import { CropEditor } from 'react-native-rectangle-doc-scanner';
|
|
358
|
+
|
|
359
|
+
<CropEditor
|
|
360
|
+
imagePath={capturedImagePath}
|
|
361
|
+
onCropComplete={(croppedPath) => {
|
|
362
|
+
console.log('크롭된 이미지:', croppedPath);
|
|
363
|
+
}}
|
|
364
|
+
onCancel={() => {
|
|
365
|
+
console.log('크롭 취소');
|
|
366
|
+
}}
|
|
367
|
+
/>
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### FullDocScanner
|
|
371
|
+
|
|
372
|
+
스캐너와 크롭 에디터를 단일 모달형 플로우로 제공합니다. `expo-image-manipulator` 또는 `react-native-image-rotate`가 설치되어 있으면, 확인 화면에서 90° 회전 버튼이 표시됩니다.
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
import { FullDocScanner } from 'react-native-rectangle-doc-scanner';
|
|
376
|
+
|
|
377
|
+
<FullDocScanner
|
|
378
|
+
onComplete={(result) => {
|
|
379
|
+
console.log('완료:', result);
|
|
380
|
+
}}
|
|
381
|
+
onCancel={() => {
|
|
382
|
+
console.log('취소');
|
|
383
|
+
}}
|
|
384
|
+
/>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## 기술 스택
|
|
388
|
+
|
|
389
|
+
### iOS
|
|
390
|
+
- **언어**: Objective-C
|
|
391
|
+
- **카메라 API**: AVCapturePhotoOutput (iOS 10+)
|
|
392
|
+
- **이미지 처리**: OpenCV, CoreImage (CIContext)
|
|
393
|
+
- **최소 버전**: iOS 11.0
|
|
394
|
+
|
|
395
|
+
### Android
|
|
396
|
+
- **언어**: Kotlin
|
|
397
|
+
- **카메라**: CameraX 1.3.0, Camera2 API
|
|
398
|
+
- **이미지 처리**: OpenCV 4.9.0
|
|
399
|
+
- **ML Kit**: 문서 스캔 및 객체 감지
|
|
400
|
+
- **최소 SDK**: 21 (Android 5.0)
|
|
401
|
+
- **타겟 SDK**: 33 (Android 13)
|
|
402
|
+
- **Kotlin**: 1.8.21
|
|
403
|
+
- **Java**: 17
|
|
404
|
+
|
|
405
|
+
## 문제 해결
|
|
406
|
+
|
|
407
|
+
### iOS 빌드 오류
|
|
408
|
+
|
|
409
|
+
Pod 설치 후에도 빌드 오류가 발생하는 경우:
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
cd ios
|
|
413
|
+
rm -rf Pods Podfile.lock
|
|
414
|
+
pod cache clean --all
|
|
415
|
+
pod install
|
|
416
|
+
cd ..
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Android 빌드 오류
|
|
420
|
+
|
|
421
|
+
Gradle 빌드 오류가 발생하는 경우:
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
cd android
|
|
425
|
+
./gradlew clean
|
|
426
|
+
cd ..
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### 권한 오류
|
|
430
|
+
|
|
431
|
+
카메라가 작동하지 않는 경우, 런타임 권한이 올바르게 요청되었는지 확인하세요. iOS의 경우 Info.plist에 권한 설명이 추가되어 있는지, Android의 경우 PermissionsAndroid로 권한을 요청했는지 확인하세요.
|
|
432
|
+
|
|
433
|
+
## 라이선스
|
|
434
|
+
|
|
435
|
+
MIT
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## English Version
|
|
440
|
+
|
|
3
441
|
React Native-friendly wrapper around [`react-native-document-scanner`](https://github.com/Michaelvilleneuve/react-native-document-scanner). It exposes a declarative `<DocScanner />` component that renders the native document scanner on both iOS and Android while keeping the surface area small enough to plug into custom UIs.
|
|
4
442
|
|
|
5
|
-
> The native implementation lives inside the upstream library (Objective
|
|
443
|
+
> The native implementation lives inside the upstream library (Objective-C/OpenCV on iOS, Kotlin/OpenCV on Android). This package simply re-exports a type-safe wrapper, optional crop editor helpers, and a full-screen scanner flow.
|
|
6
444
|
|
|
7
445
|
## ✨ Professional Camera Quality (v3.2+)
|
|
8
446
|
|
|
@@ -34,19 +472,154 @@ Just install with yarn/npm - **no manual configuration needed!**
|
|
|
34
472
|
|
|
35
473
|
## Installation
|
|
36
474
|
|
|
475
|
+
### 1. Install the Package
|
|
476
|
+
|
|
37
477
|
```bash
|
|
38
478
|
yarn add react-native-rectangle-doc-scanner \
|
|
39
479
|
github:Michaelvilleneuve/react-native-document-scanner \
|
|
40
480
|
react-native-perspective-image-cropper
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Or using npm:
|
|
41
484
|
|
|
42
|
-
|
|
43
|
-
|
|
485
|
+
```bash
|
|
486
|
+
npm install react-native-rectangle-doc-scanner \
|
|
487
|
+
github:Michaelvilleneuve/react-native-document-scanner \
|
|
488
|
+
react-native-perspective-image-cropper
|
|
44
489
|
```
|
|
45
490
|
|
|
46
|
-
|
|
491
|
+
### 2. Install Peer Dependencies
|
|
492
|
+
|
|
493
|
+
This library requires the following peer dependencies:
|
|
494
|
+
|
|
495
|
+
```bash
|
|
496
|
+
yarn add react-native-fs \
|
|
497
|
+
react-native-image-crop-picker \
|
|
498
|
+
react-native-image-picker \
|
|
499
|
+
react-native-svg \
|
|
500
|
+
expo-modules-core
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Or using npm:
|
|
504
|
+
|
|
505
|
+
```bash
|
|
506
|
+
npm install react-native-fs \
|
|
507
|
+
react-native-image-crop-picker \
|
|
508
|
+
react-native-image-picker \
|
|
509
|
+
react-native-svg \
|
|
510
|
+
expo-modules-core
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
**Optional (for image rotation features):**
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
# Choose one
|
|
517
|
+
yarn add expo-image-manipulator
|
|
518
|
+
# or
|
|
519
|
+
yarn add react-native-image-rotate
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### 2-1. Babel and Reanimated Setup (if needed)
|
|
523
|
+
|
|
524
|
+
If your project has a `babel.config.js` file, you may need the following plugins:
|
|
525
|
+
|
|
526
|
+
```javascript
|
|
527
|
+
module.exports = {
|
|
528
|
+
presets: ['module:@react-native/babel-preset'],
|
|
529
|
+
plugins: [
|
|
530
|
+
'react-native-reanimated/plugin' // Must be listed last
|
|
531
|
+
],
|
|
532
|
+
};
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
**Install additional packages if needed:**
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
yarn add react-native-reanimated
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### 3. iOS Setup
|
|
542
|
+
|
|
543
|
+
```bash
|
|
544
|
+
cd ios && pod install && cd ..
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**Add Camera Permissions to Info.plist:**
|
|
548
|
+
|
|
549
|
+
Add the following permissions to your `ios/YourApp/Info.plist` file:
|
|
550
|
+
|
|
551
|
+
```xml
|
|
552
|
+
<key>NSCameraUsageDescription</key>
|
|
553
|
+
<string>We need camera access to scan documents</string>
|
|
554
|
+
<key>NSPhotoLibraryUsageDescription</key>
|
|
555
|
+
<string>We need photo library access to save scanned documents</string>
|
|
556
|
+
<key>NSPhotoLibraryAddUsageDescription</key>
|
|
557
|
+
<string>We need photo library access to save scanned documents</string>
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### 4. Android Setup
|
|
561
|
+
|
|
562
|
+
Android automatically links the native module. If you manage packages manually (legacy architecture), register `DocumentScannerPackage()` in your `MainApplication.java`.
|
|
563
|
+
|
|
564
|
+
**Permissions are automatically included:**
|
|
565
|
+
|
|
566
|
+
The following permissions are automatically included in the library's `AndroidManifest.xml`:
|
|
567
|
+
|
|
568
|
+
```xml
|
|
569
|
+
<uses-permission android:name="android.permission.CAMERA" />
|
|
570
|
+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
|
571
|
+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
|
572
|
+
|
|
573
|
+
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
|
574
|
+
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
|
575
|
+
<uses-feature android:name="android.hardware.camera.flash" android:required="false" />
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
**Gradle Configuration:**
|
|
579
|
+
|
|
580
|
+
The library has the following minimum requirements:
|
|
581
|
+
- `minSdkVersion`: 21
|
|
582
|
+
- `compileSdkVersion`: 33
|
|
583
|
+
- `targetSdkVersion`: 33
|
|
584
|
+
- Kotlin: 1.8.21
|
|
585
|
+
- Java: 17
|
|
586
|
+
|
|
587
|
+
These are automatically applied, but make sure your project's `android/build.gradle` uses compatible versions.
|
|
588
|
+
|
|
589
|
+
### 5. Request Runtime Permissions
|
|
590
|
+
|
|
591
|
+
You need to request camera permissions at runtime in your app:
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
import { PermissionsAndroid, Platform } from 'react-native';
|
|
595
|
+
|
|
596
|
+
async function requestCameraPermission() {
|
|
597
|
+
if (Platform.OS === 'android') {
|
|
598
|
+
try {
|
|
599
|
+
const granted = await PermissionsAndroid.request(
|
|
600
|
+
PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
601
|
+
{
|
|
602
|
+
title: 'Camera Permission',
|
|
603
|
+
message: 'We need camera access to scan documents',
|
|
604
|
+
buttonNeutral: 'Ask Me Later',
|
|
605
|
+
buttonNegative: 'Cancel',
|
|
606
|
+
buttonPositive: 'OK',
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
|
610
|
+
} catch (err) {
|
|
611
|
+
console.warn(err);
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
617
|
+
```
|
|
47
618
|
|
|
48
619
|
## Usage
|
|
49
620
|
|
|
621
|
+
### Basic Example
|
|
622
|
+
|
|
50
623
|
```tsx
|
|
51
624
|
import React, { useRef } from 'react';
|
|
52
625
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
@@ -64,6 +637,7 @@ export const ScanScreen = () => {
|
|
|
64
637
|
minStableFrames={6}
|
|
65
638
|
onCapture={(result) => {
|
|
66
639
|
console.log('Captured document:', result.path);
|
|
640
|
+
console.log('Dimensions:', result.width, 'x', result.height);
|
|
67
641
|
}}
|
|
68
642
|
>
|
|
69
643
|
<View style={styles.overlay} pointerEvents="none">
|
|
@@ -74,13 +648,18 @@ export const ScanScreen = () => {
|
|
|
74
648
|
<TouchableOpacity
|
|
75
649
|
style={styles.captureButton}
|
|
76
650
|
onPress={() => scannerRef.current?.capture()}
|
|
77
|
-
|
|
651
|
+
>
|
|
652
|
+
<Text style={styles.captureButtonText}>Capture</Text>
|
|
653
|
+
</TouchableOpacity>
|
|
78
654
|
</View>
|
|
79
655
|
);
|
|
80
656
|
};
|
|
81
657
|
|
|
82
658
|
const styles = StyleSheet.create({
|
|
83
|
-
container: {
|
|
659
|
+
container: {
|
|
660
|
+
flex: 1,
|
|
661
|
+
backgroundColor: '#000'
|
|
662
|
+
},
|
|
84
663
|
overlay: {
|
|
85
664
|
position: 'absolute',
|
|
86
665
|
top: 60,
|
|
@@ -90,7 +669,10 @@ const styles = StyleSheet.create({
|
|
|
90
669
|
borderRadius: 12,
|
|
91
670
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
92
671
|
},
|
|
93
|
-
hint: {
|
|
672
|
+
hint: {
|
|
673
|
+
color: '#fff',
|
|
674
|
+
fontWeight: '600'
|
|
675
|
+
},
|
|
94
676
|
captureButton: {
|
|
95
677
|
position: 'absolute',
|
|
96
678
|
bottom: 40,
|
|
@@ -99,10 +681,18 @@ const styles = StyleSheet.create({
|
|
|
99
681
|
height: 70,
|
|
100
682
|
borderRadius: 35,
|
|
101
683
|
backgroundColor: '#fff',
|
|
684
|
+
justifyContent: 'center',
|
|
685
|
+
alignItems: 'center',
|
|
686
|
+
},
|
|
687
|
+
captureButtonText: {
|
|
688
|
+
color: '#000',
|
|
689
|
+
fontWeight: '600',
|
|
102
690
|
},
|
|
103
691
|
});
|
|
104
692
|
```
|
|
105
693
|
|
|
694
|
+
## Props
|
|
695
|
+
|
|
106
696
|
`<DocScanner />` passes through the important upstream props:
|
|
107
697
|
|
|
108
698
|
| Prop | Type | Default | Notes |
|
|
@@ -115,12 +705,92 @@ const styles = StyleSheet.create({
|
|
|
115
705
|
| `useBase64` | `boolean` | `false` | Return base64 payloads instead of file URIs. |
|
|
116
706
|
| `onCapture` | `(result) => void` | — | Receives `{ path, quad: null, width, height }`. |
|
|
117
707
|
|
|
708
|
+
### Manual Capture
|
|
709
|
+
|
|
118
710
|
Manual capture exposes an imperative `capture()` method via `ref`. Children render on top of the camera preview so you can build your own buttons, progress indicators, or onboarding tips.
|
|
119
711
|
|
|
120
712
|
## Convenience APIs
|
|
121
713
|
|
|
122
|
-
|
|
123
|
-
|
|
714
|
+
### CropEditor
|
|
715
|
+
|
|
716
|
+
Wraps `react-native-perspective-image-cropper` for manual corner adjustment.
|
|
717
|
+
|
|
718
|
+
```tsx
|
|
719
|
+
import { CropEditor } from 'react-native-rectangle-doc-scanner';
|
|
720
|
+
|
|
721
|
+
<CropEditor
|
|
722
|
+
imagePath={capturedImagePath}
|
|
723
|
+
onCropComplete={(croppedPath) => {
|
|
724
|
+
console.log('Cropped image:', croppedPath);
|
|
725
|
+
}}
|
|
726
|
+
onCancel={() => {
|
|
727
|
+
console.log('Crop cancelled');
|
|
728
|
+
}}
|
|
729
|
+
/>
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
### FullDocScanner
|
|
733
|
+
|
|
734
|
+
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.
|
|
735
|
+
|
|
736
|
+
```tsx
|
|
737
|
+
import { FullDocScanner } from 'react-native-rectangle-doc-scanner';
|
|
738
|
+
|
|
739
|
+
<FullDocScanner
|
|
740
|
+
onComplete={(result) => {
|
|
741
|
+
console.log('Completed:', result);
|
|
742
|
+
}}
|
|
743
|
+
onCancel={() => {
|
|
744
|
+
console.log('Cancelled');
|
|
745
|
+
}}
|
|
746
|
+
/>
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
## Tech Stack
|
|
750
|
+
|
|
751
|
+
### iOS
|
|
752
|
+
- **Language**: Objective-C
|
|
753
|
+
- **Camera API**: AVCapturePhotoOutput (iOS 10+)
|
|
754
|
+
- **Image Processing**: OpenCV, CoreImage (CIContext)
|
|
755
|
+
- **Minimum Version**: iOS 11.0
|
|
756
|
+
|
|
757
|
+
### Android
|
|
758
|
+
- **Language**: Kotlin
|
|
759
|
+
- **Camera**: CameraX 1.3.0, Camera2 API
|
|
760
|
+
- **Image Processing**: OpenCV 4.9.0
|
|
761
|
+
- **ML Kit**: Document scanning and object detection
|
|
762
|
+
- **Minimum SDK**: 21 (Android 5.0)
|
|
763
|
+
- **Target SDK**: 33 (Android 13)
|
|
764
|
+
- **Kotlin**: 1.8.21
|
|
765
|
+
- **Java**: 17
|
|
766
|
+
|
|
767
|
+
## Troubleshooting
|
|
768
|
+
|
|
769
|
+
### iOS Build Errors
|
|
770
|
+
|
|
771
|
+
If you encounter build errors after pod install:
|
|
772
|
+
|
|
773
|
+
```bash
|
|
774
|
+
cd ios
|
|
775
|
+
rm -rf Pods Podfile.lock
|
|
776
|
+
pod cache clean --all
|
|
777
|
+
pod install
|
|
778
|
+
cd ..
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### Android Build Errors
|
|
782
|
+
|
|
783
|
+
If you encounter Gradle build errors:
|
|
784
|
+
|
|
785
|
+
```bash
|
|
786
|
+
cd android
|
|
787
|
+
./gradlew clean
|
|
788
|
+
cd ..
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
### Permission Errors
|
|
792
|
+
|
|
793
|
+
If the camera is not working, make sure you have requested runtime permissions correctly. For iOS, check that permission descriptions are added to Info.plist. For Android, ensure you've requested permissions using PermissionsAndroid.
|
|
124
794
|
|
|
125
795
|
## License
|
|
126
796
|
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -483,6 +483,35 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
483
483
|
resetScannerView,
|
|
484
484
|
usesAndroidScannerActivity,
|
|
485
485
|
]);
|
|
486
|
+
const startAndroidScan = (0, react_1.useCallback)(async () => {
|
|
487
|
+
if (!usesAndroidScannerActivity || !pdfScannerManager?.startDocumentScanner) {
|
|
488
|
+
throw new Error('document_scanner_not_available');
|
|
489
|
+
}
|
|
490
|
+
if (captureInProgressRef.current) {
|
|
491
|
+
throw new Error('capture_in_progress');
|
|
492
|
+
}
|
|
493
|
+
captureInProgressRef.current = true;
|
|
494
|
+
captureModeRef.current = 'grid';
|
|
495
|
+
try {
|
|
496
|
+
const payload = await pdfScannerManager.startDocumentScanner({ pageLimit: 2 });
|
|
497
|
+
const normalizedPath = stripFileUri(payload?.initialImage ?? payload?.croppedImage ?? '');
|
|
498
|
+
const capturePayload = {
|
|
499
|
+
path: normalizedPath,
|
|
500
|
+
initialPath: payload?.initialImage ? stripFileUri(payload.initialImage) : normalizedPath,
|
|
501
|
+
croppedPath: payload?.croppedImage ? stripFileUri(payload.croppedImage) : normalizedPath,
|
|
502
|
+
quad: null,
|
|
503
|
+
rectangle: null,
|
|
504
|
+
width: payload?.width ?? 0,
|
|
505
|
+
height: payload?.height ?? 0,
|
|
506
|
+
origin: 'manual',
|
|
507
|
+
pages: payload?.pages ?? null,
|
|
508
|
+
};
|
|
509
|
+
await handleCapture(capturePayload);
|
|
510
|
+
}
|
|
511
|
+
finally {
|
|
512
|
+
captureInProgressRef.current = false;
|
|
513
|
+
}
|
|
514
|
+
}, [handleCapture, pdfScannerManager, usesAndroidScannerActivity]);
|
|
486
515
|
const triggerManualCapture = (0, react_1.useCallback)(() => {
|
|
487
516
|
const scannerInstance = docScannerRef.current;
|
|
488
517
|
const hasScanner = !!scannerInstance;
|
|
@@ -508,6 +537,21 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
508
537
|
return;
|
|
509
538
|
}
|
|
510
539
|
if (!hasScanner) {
|
|
540
|
+
if (usesAndroidScannerActivity) {
|
|
541
|
+
startAndroidScan().catch((error) => {
|
|
542
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
543
|
+
if (errorMessage.includes('SCAN_CANCELLED') || errorMessage.includes('Document scan cancelled')) {
|
|
544
|
+
resetScannerView({ remount: true });
|
|
545
|
+
requestAnimationFrame(() => {
|
|
546
|
+
startAndroidScan().catch(() => null);
|
|
547
|
+
});
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
console.error('[FullDocScanner] Android scan failed:', errorMessage, error);
|
|
551
|
+
emitError(error instanceof Error ? error : new Error(String(error)), 'Failed to capture image. Please try again.');
|
|
552
|
+
});
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
511
555
|
console.error('[FullDocScanner] DocScanner ref not available');
|
|
512
556
|
return;
|
|
513
557
|
}
|
|
@@ -537,26 +581,30 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
537
581
|
.catch((error) => {
|
|
538
582
|
clearTimeout(captureTimeout);
|
|
539
583
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
540
|
-
console.error('[FullDocScanner] Manual capture failed:', errorMessage, error);
|
|
541
584
|
captureModeRef.current = null;
|
|
542
585
|
captureInProgressRef.current = false;
|
|
543
|
-
if (errorMessage.includes('SCAN_CANCELLED')) {
|
|
586
|
+
if (errorMessage.includes('SCAN_CANCELLED') || errorMessage.includes('Document scan cancelled')) {
|
|
544
587
|
resetScannerView({ remount: true });
|
|
545
|
-
|
|
588
|
+
if (usesAndroidScannerActivity) {
|
|
589
|
+
requestAnimationFrame(() => {
|
|
590
|
+
startAndroidScan().catch(() => null);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
546
593
|
return;
|
|
547
594
|
}
|
|
595
|
+
console.error('[FullDocScanner] Manual capture failed:', errorMessage, error);
|
|
548
596
|
if (error instanceof Error && error.message !== 'capture_in_progress') {
|
|
549
597
|
emitError(error, 'Failed to capture image. Please try again.');
|
|
550
598
|
}
|
|
551
599
|
});
|
|
552
600
|
}, [
|
|
553
601
|
emitError,
|
|
554
|
-
onClose,
|
|
555
602
|
processing,
|
|
556
603
|
rectangleDetected,
|
|
557
604
|
rectangleHint,
|
|
558
605
|
captureReady,
|
|
559
606
|
resetScannerView,
|
|
607
|
+
startAndroidScan,
|
|
560
608
|
usesAndroidScannerActivity,
|
|
561
609
|
]);
|
|
562
610
|
const handleGalleryPick = (0, react_1.useCallback)(async () => {
|
package/package.json
CHANGED
package/src/FullDocScanner.tsx
CHANGED
|
@@ -654,6 +654,40 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
654
654
|
],
|
|
655
655
|
);
|
|
656
656
|
|
|
657
|
+
const startAndroidScan = useCallback(async () => {
|
|
658
|
+
if (!usesAndroidScannerActivity || !pdfScannerManager?.startDocumentScanner) {
|
|
659
|
+
throw new Error('document_scanner_not_available');
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (captureInProgressRef.current) {
|
|
663
|
+
throw new Error('capture_in_progress');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
captureInProgressRef.current = true;
|
|
667
|
+
captureModeRef.current = 'grid';
|
|
668
|
+
|
|
669
|
+
try {
|
|
670
|
+
const payload = await pdfScannerManager.startDocumentScanner({ pageLimit: 2 });
|
|
671
|
+
const normalizedPath = stripFileUri(payload?.initialImage ?? payload?.croppedImage ?? '');
|
|
672
|
+
|
|
673
|
+
const capturePayload: DocScannerCapture = {
|
|
674
|
+
path: normalizedPath,
|
|
675
|
+
initialPath: payload?.initialImage ? stripFileUri(payload.initialImage) : normalizedPath,
|
|
676
|
+
croppedPath: payload?.croppedImage ? stripFileUri(payload.croppedImage) : normalizedPath,
|
|
677
|
+
quad: null,
|
|
678
|
+
rectangle: null,
|
|
679
|
+
width: payload?.width ?? 0,
|
|
680
|
+
height: payload?.height ?? 0,
|
|
681
|
+
origin: 'manual',
|
|
682
|
+
pages: payload?.pages ?? null,
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
await handleCapture(capturePayload);
|
|
686
|
+
} finally {
|
|
687
|
+
captureInProgressRef.current = false;
|
|
688
|
+
}
|
|
689
|
+
}, [handleCapture, pdfScannerManager, usesAndroidScannerActivity]);
|
|
690
|
+
|
|
657
691
|
const triggerManualCapture = useCallback(() => {
|
|
658
692
|
const scannerInstance = docScannerRef.current;
|
|
659
693
|
const hasScanner = !!scannerInstance;
|
|
@@ -683,6 +717,24 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
683
717
|
}
|
|
684
718
|
|
|
685
719
|
if (!hasScanner) {
|
|
720
|
+
if (usesAndroidScannerActivity) {
|
|
721
|
+
startAndroidScan().catch((error: unknown) => {
|
|
722
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
723
|
+
if (errorMessage.includes('SCAN_CANCELLED') || errorMessage.includes('Document scan cancelled')) {
|
|
724
|
+
resetScannerView({ remount: true });
|
|
725
|
+
requestAnimationFrame(() => {
|
|
726
|
+
startAndroidScan().catch(() => null);
|
|
727
|
+
});
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
console.error('[FullDocScanner] Android scan failed:', errorMessage, error);
|
|
731
|
+
emitError(
|
|
732
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
733
|
+
'Failed to capture image. Please try again.',
|
|
734
|
+
);
|
|
735
|
+
});
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
686
738
|
console.error('[FullDocScanner] DocScanner ref not available');
|
|
687
739
|
return;
|
|
688
740
|
}
|
|
@@ -719,16 +771,20 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
719
771
|
.catch((error: unknown) => {
|
|
720
772
|
clearTimeout(captureTimeout);
|
|
721
773
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
722
|
-
console.error('[FullDocScanner] Manual capture failed:', errorMessage, error);
|
|
723
774
|
captureModeRef.current = null;
|
|
724
775
|
captureInProgressRef.current = false;
|
|
725
776
|
|
|
726
|
-
if (errorMessage.includes('SCAN_CANCELLED')) {
|
|
777
|
+
if (errorMessage.includes('SCAN_CANCELLED') || errorMessage.includes('Document scan cancelled')) {
|
|
727
778
|
resetScannerView({ remount: true });
|
|
728
|
-
|
|
779
|
+
if (usesAndroidScannerActivity) {
|
|
780
|
+
requestAnimationFrame(() => {
|
|
781
|
+
startAndroidScan().catch(() => null);
|
|
782
|
+
});
|
|
783
|
+
}
|
|
729
784
|
return;
|
|
730
785
|
}
|
|
731
786
|
|
|
787
|
+
console.error('[FullDocScanner] Manual capture failed:', errorMessage, error);
|
|
732
788
|
if (error instanceof Error && error.message !== 'capture_in_progress') {
|
|
733
789
|
emitError(
|
|
734
790
|
error,
|
|
@@ -738,12 +794,12 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
738
794
|
});
|
|
739
795
|
}, [
|
|
740
796
|
emitError,
|
|
741
|
-
onClose,
|
|
742
797
|
processing,
|
|
743
798
|
rectangleDetected,
|
|
744
799
|
rectangleHint,
|
|
745
800
|
captureReady,
|
|
746
801
|
resetScannerView,
|
|
802
|
+
startAndroidScan,
|
|
747
803
|
usesAndroidScannerActivity,
|
|
748
804
|
]);
|
|
749
805
|
|