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 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 (ObjectiveC/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.
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
- # iOS
43
- cd ios && pod install
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
- Android automatically links the native module. If you manage packages manually (legacy architecture), register `DocumentScannerPackage()` in your `MainApplication`.
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: { flex: 1, backgroundColor: '#000' },
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: { color: '#fff', fontWeight: '600' },
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
- - `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 links either `expo-image-manipulator` or `react-native-image-rotate`, the confirmation screen exposes 90° rotation buttons; otherwise rotation controls remain hidden.
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
 
@@ -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
- onClose?.();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "15.2.0",
3
+ "version": "15.4.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -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
- onClose?.();
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