react-native-rectangle-doc-scanner 3.40.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 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
+ ```
@@ -1,20 +1,20 @@
1
1
  import React from 'react';
2
- import type { CapturedDocument, Rectangle } from './types';
2
+ import type { CapturedDocument } from './types';
3
3
  import type { DetectionConfig } from './DocScanner';
4
4
  export interface FullDocScannerResult {
5
- original: CapturedDocument;
6
- rectangle: Rectangle | null;
7
- /** Base64-encoded JPEG string returned by CustomCropManager */
8
- base64: string;
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>;
@@ -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, overlayStrokeColor = '#3170f3', handlerColor = '#3170f3', strings, manualCapture = false, minStableFrames, onError, }) => {
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 processAutoCapture = (0, react_1.useCallback)(async (document) => {
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
- const size = await resolveImageSize(normalizedDoc.path, normalizedDoc.width, normalizedDoc.height);
136
- const targetWidthRaw = size.width > 0 ? size.width : normalizedDoc.width;
137
- const targetHeightRaw = size.height > 0 ? size.height : normalizedDoc.height;
138
- const baseWidth = normalizedDoc.width > 0 ? normalizedDoc.width : targetWidthRaw;
139
- const baseHeight = normalizedDoc.height > 0 ? normalizedDoc.height : targetHeightRaw;
140
- const targetWidth = targetWidthRaw > 0 ? targetWidthRaw : baseWidth || 1;
141
- const targetHeight = targetHeightRaw > 0 ? targetHeightRaw : baseHeight || 1;
142
- let rectangleBase = normalizedDoc.rectangle ?? null;
143
- if (!rectangleBase && normalizedDoc.quad && normalizedDoc.quad.length === 4) {
144
- rectangleBase = (0, coordinate_1.quadToRectangle)(normalizedDoc.quad);
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
- const finalDoc = {
174
- ...normalizedDoc,
175
- rectangle: rectangleToUse,
176
- };
177
- console.log('[FullDocScanner] Calling onResult with base64 length:', base64?.length);
89
+ setProcessing(false);
178
90
  onResult({
179
- original: finalDoc,
180
- rectangle: rectangleToUse,
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
- emitError(error instanceof Error ? error : new Error(String(error)), 'Failed to process document.');
189
- }
190
- finally {
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, resetState]);
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 (processingCaptureRef.current) {
204
- console.log('[FullDocScanner] Already processing, skipping');
205
- return;
107
+ if (manualCapturePending.current) {
108
+ manualCapturePending.current = false;
206
109
  }
207
110
  const normalizedDoc = normalizeCapturedDocument(document);
208
- // 자동 촬영이든 수동 촬영이든 모두 crop 화면으로 이동
209
- console.log('[FullDocScanner] Moving to crop/preview screen');
210
- manualCapturePending.current = false;
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 (processingCaptureRef.current) {
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 performCrop = (0, react_1.useCallback)(async () => {
251
- if (!capturedDoc) {
252
- throw new Error('No captured document to crop');
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
- setProcessing(true);
298
- const { base64, rectangle } = await performCrop();
299
- setProcessing(false);
300
- const finalDoc = {
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
- resetState();
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
- setProcessing(false);
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
- }, [capturedDoc, emitError, onResult, performCrop, resetState]);
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, resetState]);
168
+ }, [onClose]);
323
169
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
324
- screen === 'scanner' && (react_1.default.createElement(react_native_1.View, { style: styles.flex },
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.shutterButton, processing && styles.shutterButtonDisabled], onPress: triggerManualCapture, disabled: processing, accessibilityLabel: mergedStrings.manualHint, accessibilityRole: "button" },
335
- react_1.default.createElement(react_native_1.View, { style: styles.shutterInner })))))),
336
- screen === 'crop' && capturedDoc && (react_1.default.createElement(react_native_1.View, { style: styles.flex },
337
- react_1.default.createElement(CropEditor_1.CropEditor, { document: capturedDoc, overlayColor: "rgba(0,0,0,0.6)", overlayStrokeColor: overlayStrokeColor, handlerColor: handlerColor, onCropChange: handleCropChange }),
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: overlayStrokeColor }),
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
- shutterButtonDisabled: {
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)',