react-native-rectangle-doc-scanner 3.27.0 → 3.31.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.
@@ -83,17 +83,22 @@ const CropEditor = ({ document, overlayColor = 'rgba(0,0,0,0.5)', overlayStrokeC
83
83
  }, [document]);
84
84
  // Get initial rectangle from detected quad or use default
85
85
  const getInitialRectangle = (0, react_1.useCallback)(() => {
86
- if (!document.quad || !imageSize) {
86
+ if (!imageSize) {
87
87
  return undefined;
88
88
  }
89
- const rect = (0, coordinate_1.quadToRectangle)(document.quad);
90
- if (!rect) {
89
+ const baseWidth = document.width > 0 ? document.width : imageSize.width;
90
+ const baseHeight = document.height > 0 ? document.height : imageSize.height;
91
+ const sourceRectangle = document.rectangle
92
+ ? document.rectangle
93
+ : document.quad && document.quad.length === 4
94
+ ? (0, coordinate_1.quadToRectangle)(document.quad)
95
+ : null;
96
+ if (!sourceRectangle) {
91
97
  return undefined;
92
98
  }
93
- // Scale from original detection coordinates to image coordinates
94
- const scaled = (0, coordinate_1.scaleRectangle)(rect, document.width, document.height, imageSize.width, imageSize.height);
99
+ const scaled = (0, coordinate_1.scaleRectangle)(sourceRectangle, baseWidth, baseHeight, imageSize.width, imageSize.height);
95
100
  return scaled;
96
- }, [document.quad, document.width, document.height, imageSize]);
101
+ }, [document.rectangle, document.quad, document.width, document.height, imageSize]);
97
102
  const handleImageLoad = (0, react_1.useCallback)((event) => {
98
103
  // This is just for debugging - actual size is loaded via Image.getSize in useEffect
99
104
  console.log('[CropEditor] Image onLoad event triggered');
@@ -17,8 +17,10 @@ export type DocScannerCapture = {
17
17
  initialPath: string | null;
18
18
  croppedPath: string | null;
19
19
  quad: Point[] | null;
20
+ rectangle: Rectangle | null;
20
21
  width: number;
21
22
  height: number;
23
+ origin: 'auto' | 'manual';
22
24
  };
23
25
  export interface DetectionConfig {
24
26
  processingWidth?: number;
@@ -42,6 +44,7 @@ interface Props {
42
44
  gridLineWidth?: number;
43
45
  detectionConfig?: DetectionConfig;
44
46
  onRectangleDetect?: (event: RectangleDetectEvent) => void;
47
+ showManualCaptureButton?: boolean;
45
48
  }
46
49
  export type DocScannerHandle = {
47
50
  capture: () => Promise<PictureEvent>;
@@ -68,11 +68,13 @@ const normalizeRectangle = (rectangle) => {
68
68
  };
69
69
  };
70
70
  const DEFAULT_OVERLAY_COLOR = '#0b7ef4';
71
- exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAULT_OVERLAY_COLOR, autoCapture = true, minStableFrames = 8, enableTorch = false, quality = 90, useBase64 = false, children, showGrid = true, gridColor, gridLineWidth, detectionConfig, onRectangleDetect, }, ref) => {
71
+ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAULT_OVERLAY_COLOR, autoCapture = true, minStableFrames = 8, enableTorch = false, quality = 90, useBase64 = false, children, showGrid = true, gridColor, gridLineWidth, detectionConfig, onRectangleDetect, showManualCaptureButton = false, }, ref) => {
72
72
  const scannerRef = (0, react_1.useRef)(null);
73
73
  const captureResolvers = (0, react_1.useRef)(null);
74
74
  const [isAutoCapturing, setIsAutoCapturing] = (0, react_1.useState)(false);
75
75
  const [detectedRectangle, setDetectedRectangle] = (0, react_1.useState)(null);
76
+ const lastRectangleRef = (0, react_1.useRef)(null);
77
+ const captureOriginRef = (0, react_1.useRef)('auto');
76
78
  (0, react_1.useEffect)(() => {
77
79
  if (!autoCapture) {
78
80
  setIsAutoCapturing(false);
@@ -87,19 +89,23 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
87
89
  }, [quality]);
88
90
  const handlePictureTaken = (0, react_1.useCallback)((event) => {
89
91
  setIsAutoCapturing(false);
90
- const normalizedRectangle = normalizeRectangle(event.rectangleCoordinates ?? null);
92
+ const normalizedRectangle = normalizeRectangle(event.rectangleCoordinates ?? null) ?? lastRectangleRef.current;
91
93
  const quad = normalizedRectangle ? (0, coordinate_1.rectangleToQuad)(normalizedRectangle) : null;
94
+ const origin = captureOriginRef.current;
95
+ captureOriginRef.current = 'auto';
92
96
  const initialPath = event.initialImage ?? null;
93
97
  const croppedPath = event.croppedImage ?? null;
94
- const bestPath = croppedPath ?? initialPath;
95
- if (bestPath) {
98
+ const editablePath = initialPath ?? croppedPath;
99
+ if (editablePath) {
96
100
  onCapture?.({
97
- path: bestPath,
101
+ path: editablePath,
98
102
  initialPath,
99
103
  croppedPath,
100
104
  quad,
105
+ rectangle: normalizedRectangle,
101
106
  width: event.width ?? 0,
102
107
  height: event.height ?? 0,
108
+ origin,
103
109
  });
104
110
  }
105
111
  setDetectedRectangle(null);
@@ -115,32 +121,50 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
115
121
  }
116
122
  }, []);
117
123
  const capture = (0, react_1.useCallback)(() => {
124
+ captureOriginRef.current = 'manual';
118
125
  const instance = scannerRef.current;
119
126
  if (!instance || typeof instance.capture !== 'function') {
127
+ captureOriginRef.current = 'auto';
120
128
  return Promise.reject(new Error('DocumentScanner native instance is not ready'));
121
129
  }
122
130
  if (captureResolvers.current) {
131
+ captureOriginRef.current = 'auto';
123
132
  return Promise.reject(new Error('capture_in_progress'));
124
133
  }
125
- const result = instance.capture();
134
+ let result;
135
+ try {
136
+ result = instance.capture();
137
+ }
138
+ catch (error) {
139
+ captureOriginRef.current = 'auto';
140
+ return Promise.reject(error);
141
+ }
126
142
  if (result && typeof result.then === 'function') {
127
- return result.then((payload) => {
128
- handlePictureTaken(payload);
129
- return payload;
143
+ return result.catch((error) => {
144
+ captureOriginRef.current = 'auto';
145
+ throw error;
130
146
  });
131
147
  }
132
148
  return new Promise((resolve, reject) => {
133
- captureResolvers.current = { resolve, reject };
149
+ captureResolvers.current = {
150
+ resolve: (value) => {
151
+ captureOriginRef.current = 'auto';
152
+ resolve(value);
153
+ },
154
+ reject: (reason) => {
155
+ captureOriginRef.current = 'auto';
156
+ reject(reason);
157
+ },
158
+ };
134
159
  });
135
- }, [handlePictureTaken]);
160
+ }, []);
136
161
  const handleManualCapture = (0, react_1.useCallback)(() => {
137
- if (autoCapture) {
138
- return;
139
- }
162
+ captureOriginRef.current = 'manual';
140
163
  capture().catch((error) => {
164
+ captureOriginRef.current = 'auto';
141
165
  console.warn('[DocScanner] manual capture failed', error);
142
166
  });
143
- }, [autoCapture, capture]);
167
+ }, [capture]);
144
168
  const handleRectangleDetect = (0, react_1.useCallback)((event) => {
145
169
  const rectangleCoordinates = normalizeRectangle(event.rectangleCoordinates ?? null);
146
170
  const rectangleOnScreen = normalizeRectangle(event.rectangleOnScreen ?? null);
@@ -157,6 +181,9 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
157
181
  setIsAutoCapturing(false);
158
182
  }
159
183
  }
184
+ if (payload.rectangleCoordinates) {
185
+ lastRectangleRef.current = payload.rectangleCoordinates;
186
+ }
160
187
  const isGoodRectangle = payload.lastDetectionType === 0;
161
188
  setDetectedRectangle(isGoodRectangle && rectangleOnScreen ? payload : null);
162
189
  onRectangleDetect?.(payload);
@@ -168,14 +195,15 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
168
195
  captureResolvers.current.reject(new Error('reset'));
169
196
  captureResolvers.current = null;
170
197
  }
198
+ captureOriginRef.current = 'auto';
171
199
  },
172
200
  }), [capture]);
173
- const overlayPolygon = detectedRectangle?.rectangleOnScreen ?? null;
201
+ const overlayPolygon = detectedRectangle?.rectangleOnScreen ?? detectedRectangle?.rectangleCoordinates ?? null;
174
202
  const overlayIsActive = autoCapture ? isAutoCapturing : (detectedRectangle?.stableCounter ?? 0) > 0;
175
203
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
176
204
  react_1.default.createElement(react_native_document_scanner_1.default, { ref: scannerRef, style: styles.scanner, detectionCountBeforeCapture: minStableFrames, overlayColor: overlayColor, enableTorch: enableTorch, quality: normalizedQuality, useBase64: useBase64, manualOnly: !autoCapture, detectionConfig: detectionConfig, onPictureTaken: handlePictureTaken, onError: handleError, onRectangleDetect: handleRectangleDetect }),
177
205
  showGrid && overlayPolygon && (react_1.default.createElement(overlay_1.ScannerOverlay, { active: overlayIsActive, color: gridColor ?? overlayColor, lineWidth: gridLineWidth, polygon: overlayPolygon })),
178
- !autoCapture && react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: handleManualCapture }),
206
+ (showManualCaptureButton || !autoCapture) && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: handleManualCapture })),
179
207
  children));
180
208
  });
181
209
  const styles = react_native_1.StyleSheet.create({
@@ -41,6 +41,27 @@ const CropEditor_1 = require("./CropEditor");
41
41
  const coordinate_1 = require("./utils/coordinate");
42
42
  const stripFileUri = (value) => value.replace(/^file:\/\//, '');
43
43
  const ensureFileUri = (value) => (value.startsWith('file://') ? value : `file://${value}`);
44
+ const createFullImageRectangle = (width, height) => ({
45
+ topLeft: { x: 0, y: 0 },
46
+ topRight: { x: width, y: 0 },
47
+ bottomRight: { x: width, y: height },
48
+ bottomLeft: { x: 0, y: height },
49
+ });
50
+ const resolveImageSize = (path, fallbackWidth, fallbackHeight) => new Promise((resolve) => {
51
+ react_native_1.Image.getSize(ensureFileUri(path), (width, height) => resolve({ width, height }), () => resolve({
52
+ width: fallbackWidth > 0 ? fallbackWidth : 0,
53
+ height: fallbackHeight > 0 ? fallbackHeight : 0,
54
+ }));
55
+ });
56
+ const normalizeCapturedDocument = (document) => {
57
+ const normalizedPath = stripFileUri(document.initialPath ?? document.path);
58
+ return {
59
+ ...document,
60
+ path: normalizedPath,
61
+ initialPath: document.initialPath ? stripFileUri(document.initialPath) : normalizedPath,
62
+ croppedPath: document.croppedPath ? stripFileUri(document.croppedPath) : null,
63
+ };
64
+ };
44
65
  const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3170f3', gridColor, gridLineWidth, showGrid, overlayStrokeColor = '#3170f3', handlerColor = '#3170f3', strings, manualCapture = false, minStableFrames, onError, }) => {
45
66
  const [screen, setScreen] = (0, react_1.useState)('scanner');
46
67
  const [capturedDoc, setCapturedDoc] = (0, react_1.useState)(null);
@@ -48,6 +69,10 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
48
69
  const [imageSize, setImageSize] = (0, react_1.useState)(null);
49
70
  const [processing, setProcessing] = (0, react_1.useState)(false);
50
71
  const resolvedGridColor = gridColor ?? overlayColor;
72
+ const docScannerRef = (0, react_1.useRef)(null);
73
+ const manualCapturePending = (0, react_1.useRef)(false);
74
+ const processingCaptureRef = (0, react_1.useRef)(false);
75
+ const cropInitializedRef = (0, react_1.useRef)(false);
51
76
  const mergedStrings = (0, react_1.useMemo)(() => ({
52
77
  captureHint: strings?.captureHint ?? 'Align the document within the frame.',
53
78
  manualHint: strings?.manualHint ?? 'Tap the button below to capture.',
@@ -63,30 +88,36 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
63
88
  }
64
89
  react_native_1.Image.getSize(ensureFileUri(capturedDoc.path), (width, height) => setImageSize({ width, height }), () => setImageSize({ width: capturedDoc.width, height: capturedDoc.height }));
65
90
  }, [capturedDoc]);
91
+ (0, react_1.useEffect)(() => {
92
+ if (!capturedDoc || !imageSize || cropInitializedRef.current) {
93
+ return;
94
+ }
95
+ const baseWidth = capturedDoc.width > 0 ? capturedDoc.width : imageSize.width;
96
+ const baseHeight = capturedDoc.height > 0 ? capturedDoc.height : imageSize.height;
97
+ let initialRectangle = null;
98
+ if (capturedDoc.rectangle) {
99
+ initialRectangle = (0, coordinate_1.scaleRectangle)(capturedDoc.rectangle, baseWidth, baseHeight, imageSize.width, imageSize.height);
100
+ }
101
+ else if (capturedDoc.quad && capturedDoc.quad.length === 4) {
102
+ const quadRectangle = (0, coordinate_1.quadToRectangle)(capturedDoc.quad);
103
+ if (quadRectangle) {
104
+ initialRectangle = (0, coordinate_1.scaleRectangle)(quadRectangle, baseWidth, baseHeight, imageSize.width, imageSize.height);
105
+ }
106
+ }
107
+ if (initialRectangle) {
108
+ cropInitializedRef.current = true;
109
+ setCropRectangle(initialRectangle);
110
+ }
111
+ }, [capturedDoc, imageSize]);
66
112
  const resetState = (0, react_1.useCallback)(() => {
67
113
  setScreen('scanner');
68
114
  setCapturedDoc(null);
69
115
  setCropRectangle(null);
70
116
  setImageSize(null);
71
117
  setProcessing(false);
72
- }, []);
73
- const handleCapture = (0, react_1.useCallback)((document) => {
74
- const normalizedPath = stripFileUri(document.path);
75
- const nextQuad = document.quad && document.quad.length === 4 ? document.quad : null;
76
- const normalizedInitial = document.initialPath != null ? stripFileUri(document.initialPath) : normalizedPath;
77
- const normalizedCropped = document.croppedPath != null ? stripFileUri(document.croppedPath) : null;
78
- setCapturedDoc({
79
- ...document,
80
- path: normalizedPath,
81
- initialPath: normalizedInitial,
82
- croppedPath: normalizedCropped,
83
- quad: nextQuad,
84
- });
85
- setCropRectangle(nextQuad ? (0, coordinate_1.quadToRectangle)(nextQuad) : null);
86
- setScreen('crop');
87
- }, []);
88
- const handleCropChange = (0, react_1.useCallback)((rectangle) => {
89
- setCropRectangle(rectangle);
118
+ manualCapturePending.current = false;
119
+ processingCaptureRef.current = false;
120
+ cropInitializedRef.current = false;
90
121
  }, []);
91
122
  const emitError = (0, react_1.useCallback)((error, fallbackMessage) => {
92
123
  console.error('[FullDocScanner] error', error);
@@ -95,6 +126,104 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
95
126
  react_native_1.Alert.alert('Document Scanner', fallbackMessage);
96
127
  }
97
128
  }, [onError]);
129
+ const processAutoCapture = (0, react_1.useCallback)(async (document) => {
130
+ manualCapturePending.current = false;
131
+ const normalizedDoc = normalizeCapturedDocument(document);
132
+ const cropManager = react_native_1.NativeModules.CustomCropManager;
133
+ if (!cropManager?.crop) {
134
+ emitError(new Error('CustomCropManager.crop is not available'));
135
+ return;
136
+ }
137
+ setProcessing(true);
138
+ try {
139
+ const size = await resolveImageSize(normalizedDoc.path, normalizedDoc.width, normalizedDoc.height);
140
+ const targetWidthRaw = size.width > 0 ? size.width : normalizedDoc.width;
141
+ const targetHeightRaw = size.height > 0 ? size.height : normalizedDoc.height;
142
+ const baseWidth = normalizedDoc.width > 0 ? normalizedDoc.width : targetWidthRaw;
143
+ const baseHeight = normalizedDoc.height > 0 ? normalizedDoc.height : targetHeightRaw;
144
+ const targetWidth = targetWidthRaw > 0 ? targetWidthRaw : baseWidth || 1;
145
+ const targetHeight = targetHeightRaw > 0 ? targetHeightRaw : baseHeight || 1;
146
+ let rectangleBase = normalizedDoc.rectangle ?? null;
147
+ if (!rectangleBase && normalizedDoc.quad && normalizedDoc.quad.length === 4) {
148
+ rectangleBase = (0, coordinate_1.quadToRectangle)(normalizedDoc.quad);
149
+ }
150
+ const scaledRectangle = rectangleBase
151
+ ? (0, coordinate_1.scaleRectangle)(rectangleBase, baseWidth || targetWidth, baseHeight || targetHeight, targetWidth, targetHeight)
152
+ : null;
153
+ const rectangleToUse = scaledRectangle ?? createFullImageRectangle(targetWidth, targetHeight);
154
+ const base64 = await new Promise((resolve, reject) => {
155
+ cropManager.crop({
156
+ topLeft: rectangleToUse.topLeft,
157
+ topRight: rectangleToUse.topRight,
158
+ bottomRight: rectangleToUse.bottomRight,
159
+ bottomLeft: rectangleToUse.bottomLeft,
160
+ width: targetWidth,
161
+ height: targetHeight,
162
+ }, ensureFileUri(normalizedDoc.path), (error, result) => {
163
+ if (error) {
164
+ reject(error instanceof Error ? error : new Error('Crop failed'));
165
+ return;
166
+ }
167
+ resolve(result.image);
168
+ });
169
+ });
170
+ const finalDoc = {
171
+ ...normalizedDoc,
172
+ rectangle: rectangleToUse,
173
+ };
174
+ onResult({
175
+ original: finalDoc,
176
+ rectangle: rectangleToUse,
177
+ base64,
178
+ });
179
+ resetState();
180
+ }
181
+ catch (error) {
182
+ setProcessing(false);
183
+ emitError(error instanceof Error ? error : new Error(String(error)), 'Failed to process document.');
184
+ }
185
+ finally {
186
+ processingCaptureRef.current = false;
187
+ }
188
+ }, [emitError, onResult, resetState]);
189
+ const handleCapture = (0, react_1.useCallback)((document) => {
190
+ if (processingCaptureRef.current) {
191
+ return;
192
+ }
193
+ const isManualCapture = manualCapture || manualCapturePending.current || document.origin === 'manual';
194
+ const normalizedDoc = normalizeCapturedDocument(document);
195
+ if (isManualCapture) {
196
+ manualCapturePending.current = false;
197
+ processingCaptureRef.current = false;
198
+ cropInitializedRef.current = false;
199
+ setCapturedDoc(normalizedDoc);
200
+ setImageSize(null);
201
+ setCropRectangle(null);
202
+ setScreen('crop');
203
+ return;
204
+ }
205
+ processingCaptureRef.current = true;
206
+ processAutoCapture(document);
207
+ }, [manualCapture, processAutoCapture]);
208
+ const handleCropChange = (0, react_1.useCallback)((rectangle) => {
209
+ setCropRectangle(rectangle);
210
+ }, []);
211
+ const triggerManualCapture = (0, react_1.useCallback)(() => {
212
+ if (processingCaptureRef.current) {
213
+ return;
214
+ }
215
+ manualCapturePending.current = true;
216
+ const capturePromise = docScannerRef.current?.capture();
217
+ if (capturePromise && typeof capturePromise.catch === 'function') {
218
+ capturePromise.catch((error) => {
219
+ manualCapturePending.current = false;
220
+ console.warn('[FullDocScanner] manual capture failed', error);
221
+ });
222
+ }
223
+ else if (!capturePromise) {
224
+ manualCapturePending.current = false;
225
+ }
226
+ }, []);
98
227
  const performCrop = (0, react_1.useCallback)(async () => {
99
228
  if (!capturedDoc) {
100
229
  throw new Error('No captured document to crop');
@@ -104,21 +233,29 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
104
233
  if (!cropManager?.crop) {
105
234
  throw new Error('CustomCropManager.crop is not available');
106
235
  }
107
- const fallbackRectangle = capturedDoc.quad && capturedDoc.quad.length === 4
108
- ? (0, coordinate_1.quadToRectangle)(capturedDoc.quad)
109
- : null;
110
- const scaledFallback = fallbackRectangle
111
- ? (0, coordinate_1.scaleRectangle)(fallbackRectangle, capturedDoc.width, capturedDoc.height, size.width, size.height)
112
- : null;
113
- const rectangle = cropRectangle ?? scaledFallback;
236
+ const baseWidth = capturedDoc.width > 0 ? capturedDoc.width : size.width;
237
+ const baseHeight = capturedDoc.height > 0 ? capturedDoc.height : size.height;
238
+ const targetWidth = size.width > 0 ? size.width : baseWidth || 1;
239
+ const targetHeight = size.height > 0 ? size.height : baseHeight || 1;
240
+ let fallbackRectangle = null;
241
+ if (capturedDoc.rectangle) {
242
+ fallbackRectangle = (0, coordinate_1.scaleRectangle)(capturedDoc.rectangle, baseWidth || targetWidth, baseHeight || targetHeight, targetWidth, targetHeight);
243
+ }
244
+ else if (capturedDoc.quad && capturedDoc.quad.length === 4) {
245
+ const quadRectangle = (0, coordinate_1.quadToRectangle)(capturedDoc.quad);
246
+ if (quadRectangle) {
247
+ fallbackRectangle = (0, coordinate_1.scaleRectangle)(quadRectangle, baseWidth || targetWidth, baseHeight || targetHeight, targetWidth, targetHeight);
248
+ }
249
+ }
250
+ const rectangleToUse = cropRectangle ?? fallbackRectangle ?? createFullImageRectangle(targetWidth, targetHeight);
114
251
  const base64 = await new Promise((resolve, reject) => {
115
252
  cropManager.crop({
116
- topLeft: rectangle?.topLeft ?? { x: 0, y: 0 },
117
- topRight: rectangle?.topRight ?? { x: size.width, y: 0 },
118
- bottomRight: rectangle?.bottomRight ?? { x: size.width, y: size.height },
119
- bottomLeft: rectangle?.bottomLeft ?? { x: 0, y: size.height },
120
- width: size.width,
121
- height: size.height,
253
+ topLeft: rectangleToUse.topLeft,
254
+ topRight: rectangleToUse.topRight,
255
+ bottomRight: rectangleToUse.bottomRight,
256
+ bottomLeft: rectangleToUse.bottomLeft,
257
+ width: targetWidth,
258
+ height: targetHeight,
122
259
  }, ensureFileUri(capturedDoc.path), (error, result) => {
123
260
  if (error) {
124
261
  reject(error instanceof Error ? error : new Error('Crop failed'));
@@ -127,7 +264,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
127
264
  resolve(result.image);
128
265
  });
129
266
  });
130
- return base64;
267
+ return { base64, rectangle: rectangleToUse };
131
268
  }, [capturedDoc, cropRectangle, imageSize]);
132
269
  const handleConfirm = (0, react_1.useCallback)(async () => {
133
270
  if (!capturedDoc) {
@@ -135,11 +272,15 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
135
272
  }
136
273
  try {
137
274
  setProcessing(true);
138
- const base64 = await performCrop();
275
+ const { base64, rectangle } = await performCrop();
139
276
  setProcessing(false);
277
+ const finalDoc = {
278
+ ...capturedDoc,
279
+ rectangle,
280
+ };
140
281
  onResult({
141
- original: capturedDoc,
142
- rectangle: cropRectangle,
282
+ original: finalDoc,
283
+ rectangle,
143
284
  base64,
144
285
  });
145
286
  resetState();
@@ -148,7 +289,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
148
289
  setProcessing(false);
149
290
  emitError(error instanceof Error ? error : new Error(String(error)), 'Failed to process document.');
150
291
  }
151
- }, [capturedDoc, cropRectangle, emitError, onResult, performCrop, resetState]);
292
+ }, [capturedDoc, emitError, onResult, performCrop, resetState]);
152
293
  const handleRetake = (0, react_1.useCallback)(() => {
153
294
  resetState();
154
295
  }, [resetState]);
@@ -158,13 +299,15 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
158
299
  }, [onClose, resetState]);
159
300
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
160
301
  screen === 'scanner' && (react_1.default.createElement(react_native_1.View, { style: styles.flex },
161
- react_1.default.createElement(DocScanner_1.DocScanner, { autoCapture: !manualCapture, overlayColor: overlayColor, showGrid: showGrid, gridColor: resolvedGridColor, gridLineWidth: gridLineWidth, minStableFrames: minStableFrames ?? 6, detectionConfig: detectionConfig, onCapture: handleCapture },
302
+ 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 },
162
303
  react_1.default.createElement(react_native_1.View, { style: styles.overlay, pointerEvents: "box-none" },
163
304
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.closeButton, onPress: handleClose, accessibilityLabel: mergedStrings.cancel, accessibilityRole: "button" },
164
305
  react_1.default.createElement(react_native_1.Text, { style: styles.closeButtonLabel }, "\u00D7")),
165
306
  react_1.default.createElement(react_native_1.View, { style: styles.instructions, pointerEvents: "none" },
166
307
  react_1.default.createElement(react_native_1.Text, { style: styles.captureText }, mergedStrings.captureHint),
167
- manualCapture && react_1.default.createElement(react_native_1.Text, { style: styles.captureText }, mergedStrings.manualHint)))))),
308
+ react_1.default.createElement(react_native_1.Text, { style: styles.captureText }, mergedStrings.manualHint)),
309
+ react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.shutterButton, processing && styles.shutterButtonDisabled], onPress: triggerManualCapture, disabled: processing, accessibilityLabel: mergedStrings.manualHint, accessibilityRole: "button" },
310
+ react_1.default.createElement(react_native_1.View, { style: styles.shutterInner })))))),
168
311
  screen === 'crop' && capturedDoc && (react_1.default.createElement(react_native_1.View, { style: styles.flex },
169
312
  react_1.default.createElement(CropEditor_1.CropEditor, { document: capturedDoc, overlayColor: "rgba(0,0,0,0.6)", overlayStrokeColor: overlayStrokeColor, handlerColor: handlerColor, onCropChange: handleCropChange }),
170
313
  react_1.default.createElement(react_native_1.View, { style: styles.cropFooter },
@@ -219,6 +362,26 @@ const styles = react_native_1.StyleSheet.create({
219
362
  fontSize: 15,
220
363
  textAlign: 'center',
221
364
  },
365
+ shutterButton: {
366
+ alignSelf: 'center',
367
+ width: 80,
368
+ height: 80,
369
+ borderRadius: 40,
370
+ borderWidth: 4,
371
+ borderColor: '#fff',
372
+ justifyContent: 'center',
373
+ alignItems: 'center',
374
+ backgroundColor: 'rgba(255,255,255,0.1)',
375
+ },
376
+ shutterButtonDisabled: {
377
+ opacity: 0.4,
378
+ },
379
+ shutterInner: {
380
+ width: 60,
381
+ height: 60,
382
+ borderRadius: 30,
383
+ backgroundColor: '#fff',
384
+ },
222
385
  cropFooter: {
223
386
  position: 'absolute',
224
387
  bottom: 40,
package/dist/types.d.ts CHANGED
@@ -14,6 +14,7 @@ export type CapturedDocument = {
14
14
  initialPath?: string | null;
15
15
  croppedPath?: string | null;
16
16
  quad: Point[] | null;
17
+ rectangle?: Rectangle | null;
17
18
  width: number;
18
19
  height: number;
19
20
  };