react-native-rectangle-doc-scanner 0.63.0 → 0.65.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.
@@ -1,22 +1,16 @@
1
1
  import React, { ReactNode } from 'react';
2
- import { Camera } from 'react-native-vision-camera';
3
2
  import type { Point } from './types';
4
- type CameraOverrides = Omit<React.ComponentProps<typeof Camera>, 'style' | 'ref' | 'frameProcessor'>;
5
3
  /**
6
- * Configuration for detection quality and behavior
4
+ * Detection configuration is no longer used now that the native
5
+ * implementation handles edge detection. Keeping it for backwards
6
+ * compatibility with existing consumer code.
7
7
  */
8
8
  export interface DetectionConfig {
9
- /** Processing resolution width (default: 1280) - higher = more accurate but slower */
10
9
  processingWidth?: number;
11
- /** Canny edge detection lower threshold (default: 40) */
12
10
  cannyLowThreshold?: number;
13
- /** Canny edge detection upper threshold (default: 120) */
14
11
  cannyHighThreshold?: number;
15
- /** Snap distance in pixels for corner locking (default: 8) */
16
12
  snapDistance?: number;
17
- /** Max frames to hold anchor when detection fails (default: 20) */
18
13
  maxAnchorMisses?: number;
19
- /** Maximum center movement allowed while maintaining lock (default: 200px) */
20
14
  maxCenterDelta?: number;
21
15
  }
22
16
  interface Props {
@@ -29,9 +23,13 @@ interface Props {
29
23
  overlayColor?: string;
30
24
  autoCapture?: boolean;
31
25
  minStableFrames?: number;
32
- cameraProps?: CameraOverrides;
26
+ enableTorch?: boolean;
27
+ quality?: number;
28
+ useBase64?: boolean;
33
29
  children?: ReactNode;
34
- /** Advanced detection configuration */
30
+ showGrid?: boolean;
31
+ gridColor?: string;
32
+ gridLineWidth?: number;
35
33
  detectionConfig?: DetectionConfig;
36
34
  }
37
35
  export declare const DocScanner: React.FC<Props>;
@@ -32,485 +32,84 @@ 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.DocScanner = void 0;
37
40
  const react_1 = __importStar(require("react"));
38
41
  const react_native_1 = require("react-native");
39
- const react_native_vision_camera_1 = require("react-native-vision-camera");
40
- const vision_camera_resize_plugin_1 = require("vision-camera-resize-plugin");
41
- const react_native_worklets_core_1 = require("react-native-worklets-core");
42
- const react_native_fast_opencv_1 = require("react-native-fast-opencv");
42
+ const react_native_document_scanner_plugin_1 = __importDefault(require("react-native-document-scanner-plugin"));
43
43
  const overlay_1 = require("./utils/overlay");
44
- const stability_1 = require("./utils/stability");
45
- const quad_1 = require("./utils/quad");
46
- const isConvexQuadrilateral = (points) => {
47
- 'worklet';
48
- try {
49
- if (points.length !== 4) {
50
- return false;
51
- }
52
- // Validate all points have valid x and y
53
- for (const p of points) {
54
- if (typeof p.x !== 'number' || typeof p.y !== 'number' ||
55
- !isFinite(p.x) || !isFinite(p.y)) {
56
- return false;
57
- }
58
- }
59
- let previous = 0;
60
- for (let i = 0; i < 4; i++) {
61
- const p0 = points[i];
62
- const p1 = points[(i + 1) % 4];
63
- const p2 = points[(i + 2) % 4];
64
- const cross = (p1.x - p0.x) * (p2.y - p1.y) - (p1.y - p0.y) * (p2.x - p1.x);
65
- // Relax the collinearity check - allow very small cross products
66
- if (Math.abs(cross) < 0.1) {
67
- return false;
68
- }
69
- if (i === 0) {
70
- previous = cross;
71
- }
72
- else if (previous * cross < 0) {
73
- return false;
74
- }
75
- }
76
- return true;
77
- }
78
- catch (err) {
79
- return false;
80
- }
81
- };
82
- const DocScanner = ({ onCapture, overlayColor = '#e7a649', autoCapture = true, minStableFrames = 8, cameraProps, children, detectionConfig = {}, }) => {
83
- const device = (0, react_native_vision_camera_1.useCameraDevice)('back');
84
- const { hasPermission, requestPermission } = (0, react_native_vision_camera_1.useCameraPermission)();
85
- const { resize } = (0, vision_camera_resize_plugin_1.useResizePlugin)();
86
- const camera = (0, react_1.useRef)(null);
87
- const handleCameraRef = (0, react_1.useCallback)((ref) => {
88
- camera.current = ref;
89
- }, []);
44
+ const NativeDocumentScanner = react_native_document_scanner_plugin_1.default;
45
+ const DEFAULT_OVERLAY_COLOR = '#e7a649';
46
+ const GRID_COLOR_FALLBACK = 'rgba(231, 166, 73, 0.35)';
47
+ const DocScanner = ({ onCapture, overlayColor = DEFAULT_OVERLAY_COLOR, autoCapture = true, minStableFrames = 8, enableTorch = false, quality, useBase64 = false, children, showGrid = true, gridColor, gridLineWidth = 2, }) => {
48
+ const scannerRef = (0, react_1.useRef)(null);
49
+ const capturingRef = (0, react_1.useRef)(false);
90
50
  const [quad, setQuad] = (0, react_1.useState)(null);
91
- const [stable, setStable] = (0, react_1.useState)(0);
92
- (0, react_1.useEffect)(() => {
93
- requestPermission();
94
- }, [requestPermission]);
95
- const lastQuadRef = (0, react_1.useRef)(null);
96
- const smoothingBufferRef = (0, react_1.useRef)([]);
97
- const anchorQuadRef = (0, react_1.useRef)(null);
98
- const anchorMissesRef = (0, react_1.useRef)(0);
99
- const anchorConfidenceRef = (0, react_1.useRef)(0);
100
- const lastMeasurementRef = (0, react_1.useRef)(null);
101
- const frameSizeRef = (0, react_1.useRef)(null);
102
- // Detection parameters - configurable via props with sensible defaults
103
- const PROCESSING_WIDTH = detectionConfig.processingWidth ?? 1280;
104
- const CANNY_LOW = detectionConfig.cannyLowThreshold ?? 40;
105
- const CANNY_HIGH = detectionConfig.cannyHighThreshold ?? 120;
106
- const SNAP_DISTANCE = detectionConfig.snapDistance ?? 8;
107
- const MAX_ANCHOR_MISSES = detectionConfig.maxAnchorMisses ?? 20;
108
- const REJECT_CENTER_DELTA = detectionConfig.maxCenterDelta ?? 200;
109
- // Fixed parameters for algorithm stability
110
- const MAX_HISTORY = 5;
111
- const SNAP_CENTER_DISTANCE = 18;
112
- const BLEND_DISTANCE = 80;
113
- const MAX_CENTER_DELTA = 120;
114
- const MAX_AREA_SHIFT = 0.55;
115
- const HISTORY_RESET_DISTANCE = 90;
116
- const MIN_AREA_RATIO = 0.0002;
117
- const MAX_AREA_RATIO = 0.9;
118
- const MIN_EDGE_RATIO = 0.015;
119
- const MIN_CONFIDENCE_TO_HOLD = 2;
120
- const MAX_ANCHOR_CONFIDENCE = 30;
121
- const updateQuad = (0, react_native_worklets_core_1.useRunOnJS)((value) => {
122
- if (__DEV__) {
123
- console.log('[DocScanner] quad', value);
51
+ const [frameSize, setFrameSize] = (0, react_1.useState)(null);
52
+ const effectiveGridColor = (0, react_1.useMemo)(() => gridColor ?? GRID_COLOR_FALLBACK, [gridColor]);
53
+ const handleLayout = (0, react_1.useCallback)((event) => {
54
+ const { width, height } = event.nativeEvent.layout;
55
+ if (width > 0 && height > 0) {
56
+ setFrameSize({ width, height });
124
57
  }
125
- const fallbackToAnchor = (resetHistory) => {
126
- if (resetHistory) {
127
- smoothingBufferRef.current = [];
128
- lastMeasurementRef.current = null;
129
- }
130
- const anchor = anchorQuadRef.current;
131
- const anchorConfidence = anchorConfidenceRef.current;
132
- if (anchor && anchorConfidence >= MIN_CONFIDENCE_TO_HOLD) {
133
- anchorMissesRef.current += 1;
134
- if (anchorMissesRef.current <= MAX_ANCHOR_MISSES) {
135
- anchorConfidenceRef.current = Math.max(1, anchorConfidence - 1);
136
- lastQuadRef.current = anchor;
137
- setQuad(anchor);
138
- return true;
139
- }
140
- }
141
- anchorMissesRef.current = 0;
142
- anchorConfidenceRef.current = 0;
143
- anchorQuadRef.current = null;
144
- lastQuadRef.current = null;
58
+ }, []);
59
+ const handleRectangleDetect = (0, react_1.useCallback)((event) => {
60
+ const coordinates = event?.rectangleCoordinates;
61
+ if (!coordinates) {
145
62
  setQuad(null);
146
- return false;
147
- };
148
- if (!(0, quad_1.isValidQuad)(value)) {
149
- const handled = fallbackToAnchor(false);
150
- if (handled) {
151
- return;
152
- }
153
- return;
154
- }
155
- anchorMissesRef.current = 0;
156
- const ordered = (0, quad_1.orderQuadPoints)(value);
157
- const sanitized = (0, quad_1.sanitizeQuad)(ordered);
158
- const frameSize = frameSizeRef.current;
159
- const frameArea = frameSize ? frameSize.width * frameSize.height : null;
160
- const area = (0, quad_1.quadArea)(sanitized);
161
- const edges = (0, quad_1.quadEdgeLengths)(sanitized);
162
- const minEdge = Math.min(...edges);
163
- const maxEdge = Math.max(...edges);
164
- const aspectRatio = maxEdge > 0 ? maxEdge / Math.max(minEdge, 1) : 0;
165
- const minEdgeThreshold = frameSize
166
- ? Math.max(14, Math.min(frameSize.width, frameSize.height) * MIN_EDGE_RATIO)
167
- : 14;
168
- const areaTooSmall = frameArea ? area < frameArea * MIN_AREA_RATIO : area === 0;
169
- const areaTooLarge = frameArea ? area > frameArea * MAX_AREA_RATIO : false;
170
- const edgesTooShort = minEdge < minEdgeThreshold;
171
- const aspectTooExtreme = aspectRatio > 7;
172
- if (areaTooSmall || areaTooLarge || edgesTooShort || aspectTooExtreme) {
173
- const handled = fallbackToAnchor(true);
174
- if (handled) {
175
- return;
176
- }
177
63
  return;
178
64
  }
179
- const lastMeasurement = lastMeasurementRef.current;
180
- const shouldResetHistory = lastMeasurement && (0, quad_1.quadDistance)(lastMeasurement, sanitized) > HISTORY_RESET_DISTANCE;
181
- const existingHistory = shouldResetHistory ? [] : smoothingBufferRef.current;
182
- const nextHistory = existingHistory.length >= MAX_HISTORY
183
- ? [...existingHistory.slice(existingHistory.length - (MAX_HISTORY - 1)), sanitized]
184
- : [...existingHistory, sanitized];
185
- const hasHistory = nextHistory.length >= 2;
186
- let candidate = hasHistory ? (0, quad_1.weightedAverageQuad)(nextHistory) : sanitized;
187
- const anchor = anchorQuadRef.current;
188
- if (anchor && (0, quad_1.isValidQuad)(anchor)) {
189
- const delta = (0, quad_1.quadDistance)(candidate, anchor);
190
- const anchorCenter = (0, quad_1.quadCenter)(anchor);
191
- const candidateCenter = (0, quad_1.quadCenter)(candidate);
192
- const anchorArea = (0, quad_1.quadArea)(anchor);
193
- const candidateArea = (0, quad_1.quadArea)(candidate);
194
- const centerDelta = Math.hypot(candidateCenter.x - anchorCenter.x, candidateCenter.y - anchorCenter.y);
195
- const areaShift = anchorArea > 0 ? Math.abs(anchorArea - candidateArea) / anchorArea : 0;
196
- if (centerDelta >= REJECT_CENTER_DELTA || areaShift > 1.2) {
197
- smoothingBufferRef.current = [sanitized];
198
- lastMeasurementRef.current = sanitized;
199
- anchorQuadRef.current = candidate;
200
- anchorConfidenceRef.current = 1;
201
- anchorMissesRef.current = 0;
202
- lastQuadRef.current = candidate;
203
- setQuad(candidate);
204
- return;
205
- }
206
- if (delta <= SNAP_DISTANCE && centerDelta <= SNAP_CENTER_DISTANCE && areaShift <= 0.08) {
207
- candidate = anchor;
208
- smoothingBufferRef.current = nextHistory;
209
- lastMeasurementRef.current = sanitized;
210
- anchorConfidenceRef.current = Math.min(anchorConfidenceRef.current + 1, MAX_ANCHOR_CONFIDENCE);
211
- }
212
- else if (delta <= BLEND_DISTANCE && centerDelta <= MAX_CENTER_DELTA && areaShift <= MAX_AREA_SHIFT) {
213
- const normalizedDelta = Math.min(1, delta / BLEND_DISTANCE);
214
- const adaptiveAlpha = 0.25 + normalizedDelta * 0.45; // 0.25..0.7 range
215
- candidate = (0, quad_1.blendQuads)(anchor, candidate, adaptiveAlpha);
216
- smoothingBufferRef.current = nextHistory;
217
- lastMeasurementRef.current = sanitized;
218
- anchorConfidenceRef.current = Math.min(anchorConfidenceRef.current + 1, MAX_ANCHOR_CONFIDENCE);
219
- }
220
- else {
221
- const handled = fallbackToAnchor(true);
222
- if (handled) {
223
- return;
224
- }
225
- return;
226
- }
227
- }
228
- else {
229
- smoothingBufferRef.current = nextHistory;
230
- lastMeasurementRef.current = sanitized;
231
- anchorConfidenceRef.current = Math.min(anchorConfidenceRef.current + 1, MAX_ANCHOR_CONFIDENCE);
232
- }
233
- candidate = (0, quad_1.orderQuadPoints)(candidate);
234
- anchorQuadRef.current = candidate;
235
- lastQuadRef.current = candidate;
236
- setQuad(candidate);
237
- anchorMissesRef.current = 0;
238
- }, []);
239
- const reportError = (0, react_native_worklets_core_1.useRunOnJS)((step, error) => {
240
- const message = error instanceof Error ? error.message : `${error}`;
241
- console.warn(`[DocScanner] frame error at ${step}: ${message}`);
242
- }, []);
243
- const reportStage = (0, react_native_worklets_core_1.useRunOnJS)((_stage) => {
244
- // Disabled for performance
245
- }, []);
246
- const [frameSize, setFrameSize] = (0, react_1.useState)(null);
247
- const updateFrameSize = (0, react_native_worklets_core_1.useRunOnJS)((width, height) => {
248
- frameSizeRef.current = { width, height };
249
- setFrameSize({ width, height });
65
+ const nextQuad = [
66
+ coordinates.topLeft,
67
+ coordinates.topRight,
68
+ coordinates.bottomRight,
69
+ coordinates.bottomLeft,
70
+ ];
71
+ setQuad(nextQuad);
250
72
  }, []);
251
- const frameProcessor = (0, react_native_vision_camera_1.useFrameProcessor)((frame) => {
252
- 'worklet';
253
- let step = 'start';
254
- try {
255
- // Report frame size for coordinate transformation
256
- updateFrameSize(frame.width, frame.height);
257
- // Use configurable resolution for accuracy vs performance balance
258
- const ratio = PROCESSING_WIDTH / frame.width;
259
- const width = Math.floor(frame.width * ratio);
260
- const height = Math.floor(frame.height * ratio);
261
- step = 'resize';
262
- reportStage(step);
263
- const resized = resize(frame, {
264
- dataType: 'uint8',
265
- pixelFormat: 'bgr',
266
- scale: { width: width, height: height },
267
- });
268
- step = 'frameBufferToMat';
269
- reportStage(step);
270
- let mat = react_native_fast_opencv_1.OpenCV.frameBufferToMat(height, width, 3, resized);
271
- step = 'cvtColor';
272
- reportStage(step);
273
- react_native_fast_opencv_1.OpenCV.invoke('cvtColor', mat, mat, react_native_fast_opencv_1.ColorConversionCodes.COLOR_BGR2GRAY);
274
- // Enhanced morphological operations for noise reduction
275
- const morphologyKernel = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 7, 7);
276
- step = 'getStructuringElement';
277
- reportStage(step);
278
- const element = react_native_fast_opencv_1.OpenCV.invoke('getStructuringElement', react_native_fast_opencv_1.MorphShapes.MORPH_RECT, morphologyKernel);
279
- step = 'morphologyEx';
280
- reportStage(step);
281
- react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', mat, mat, react_native_fast_opencv_1.MorphTypes.MORPH_CLOSE, element);
282
- react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', mat, mat, react_native_fast_opencv_1.MorphTypes.MORPH_OPEN, element);
283
- const ADAPTIVE_THRESH_GAUSSIAN_C = 1;
284
- const THRESH_BINARY = 0;
285
- const THRESH_OTSU = 8;
286
- // Bilateral filter for edge-preserving smoothing (better quality than Gaussian)
287
- step = 'bilateralFilter';
288
- reportStage(step);
289
- let processed = mat;
290
- try {
291
- const tempMat = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat);
292
- react_native_fast_opencv_1.OpenCV.invoke('bilateralFilter', mat, tempMat, 9, 75, 75);
293
- processed = tempMat;
294
- }
295
- catch (error) {
296
- if (__DEV__) {
297
- console.warn('[DocScanner] bilateralFilter unavailable, falling back to GaussianBlur', error);
298
- }
299
- const blurKernel = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 5, 5);
300
- react_native_fast_opencv_1.OpenCV.invoke('GaussianBlur', mat, mat, blurKernel, 0);
301
- processed = mat;
302
- }
303
- // Additional blur and close pass to smooth jagged edges
304
- step = 'gaussianBlur';
305
- reportStage(step);
306
- const gaussianKernel = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 5, 5);
307
- react_native_fast_opencv_1.OpenCV.invoke('GaussianBlur', processed, processed, gaussianKernel, 0);
308
- react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', processed, processed, react_native_fast_opencv_1.MorphTypes.MORPH_CLOSE, element);
309
- const baseMat = react_native_fast_opencv_1.OpenCV.invoke('clone', processed);
310
- const frameArea = width * height;
311
- const originalArea = frame.width * frame.height;
312
- const minEdgeThreshold = Math.max(14, Math.min(frame.width, frame.height) * MIN_EDGE_RATIO);
313
- const epsilonValues = [
314
- 0.001, 0.002, 0.003, 0.004, 0.005, 0.006, 0.007, 0.008, 0.009,
315
- 0.01, 0.012, 0.015, 0.018, 0.02, 0.025, 0.03, 0.035, 0.04, 0.05,
316
- 0.06, 0.07, 0.08, 0.09, 0.1, 0.12,
317
- ];
318
- let bestQuad = null;
319
- let bestArea = 0;
320
- let convexHullWarned = false;
321
- const considerCandidate = (candidate) => {
322
- 'worklet';
323
- if (!candidate) {
324
- return;
325
- }
326
- if (!bestQuad || candidate.area > bestArea) {
327
- bestQuad = candidate.quad;
328
- bestArea = candidate.area;
329
- }
330
- };
331
- const evaluateContours = (inputMat, attemptLabel) => {
332
- 'worklet';
333
- step = `findContours_${attemptLabel}`;
334
- reportStage(step);
335
- const contours = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVectorOfVectors);
336
- react_native_fast_opencv_1.OpenCV.invoke('findContours', inputMat, contours, react_native_fast_opencv_1.RetrievalModes.RETR_EXTERNAL, react_native_fast_opencv_1.ContourApproximationModes.CHAIN_APPROX_SIMPLE);
337
- const contourVector = react_native_fast_opencv_1.OpenCV.toJSValue(contours);
338
- const contourArray = Array.isArray(contourVector?.array) ? contourVector.array : [];
339
- let bestLocal = null;
340
- for (let i = 0; i < contourArray.length; i += 1) {
341
- step = `${attemptLabel}_contour_${i}_copy`;
342
- reportStage(step);
343
- const contour = react_native_fast_opencv_1.OpenCV.copyObjectFromVector(contours, i);
344
- step = `${attemptLabel}_contour_${i}_area`;
345
- reportStage(step);
346
- const { value: area } = react_native_fast_opencv_1.OpenCV.invoke('contourArea', contour, false);
347
- if (typeof area !== 'number' || !isFinite(area) || area < 60) {
348
- continue;
349
- }
350
- const resizedRatio = area / frameArea;
351
- if (resizedRatio < 0.00012 || resizedRatio > 0.98) {
352
- continue;
353
- }
354
- let contourToUse = contour;
355
- try {
356
- const hull = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVector);
357
- react_native_fast_opencv_1.OpenCV.invoke('convexHull', contour, hull, false, true);
358
- contourToUse = hull;
359
- }
360
- catch (err) {
361
- if (__DEV__ && !convexHullWarned) {
362
- console.warn('[DocScanner] convexHull failed, using original contour');
363
- convexHullWarned = true;
364
- }
365
- }
366
- const { value: perimeter } = react_native_fast_opencv_1.OpenCV.invoke('arcLength', contourToUse, true);
367
- if (typeof perimeter !== 'number' || !isFinite(perimeter) || perimeter < 80) {
368
- continue;
369
- }
370
- const approx = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVector);
371
- let approxArray = [];
372
- for (let attempt = 0; attempt < epsilonValues.length; attempt += 1) {
373
- const epsilon = epsilonValues[attempt] * perimeter;
374
- step = `${attemptLabel}_contour_${i}_approx_${attempt}`;
375
- reportStage(step);
376
- react_native_fast_opencv_1.OpenCV.invoke('approxPolyDP', contourToUse, approx, epsilon, true);
377
- const approxValue = react_native_fast_opencv_1.OpenCV.toJSValue(approx);
378
- const candidate = Array.isArray(approxValue?.array) ? approxValue.array : [];
379
- if (candidate.length === 4) {
380
- approxArray = candidate;
381
- break;
382
- }
383
- }
384
- if (approxArray.length !== 4) {
385
- continue;
386
- }
387
- const isValidPoint = (pt) => typeof pt.x === 'number' && typeof pt.y === 'number' && isFinite(pt.x) && isFinite(pt.y);
388
- if (!approxArray.every(isValidPoint)) {
389
- continue;
390
- }
391
- const normalizedPoints = approxArray.map((pt) => ({
392
- x: pt.x / ratio,
393
- y: pt.y / ratio,
394
- }));
395
- if (!isConvexQuadrilateral(normalizedPoints)) {
396
- continue;
397
- }
398
- const sanitized = (0, quad_1.sanitizeQuad)((0, quad_1.orderQuadPoints)(normalizedPoints));
399
- if (!(0, quad_1.isValidQuad)(sanitized)) {
400
- continue;
401
- }
402
- const edges = (0, quad_1.quadEdgeLengths)(sanitized);
403
- const minEdge = Math.min(...edges);
404
- const maxEdge = Math.max(...edges);
405
- if (!Number.isFinite(minEdge) || minEdge < minEdgeThreshold) {
406
- continue;
407
- }
408
- const aspectRatio = maxEdge / Math.max(minEdge, 1);
409
- if (!Number.isFinite(aspectRatio) || aspectRatio > 8.5) {
410
- continue;
411
- }
412
- const quadAreaValue = (0, quad_1.quadArea)(sanitized);
413
- const originalRatio = originalArea > 0 ? quadAreaValue / originalArea : 0;
414
- if (originalRatio < 0.00012 || originalRatio > 0.92) {
415
- continue;
416
- }
417
- const candidate = {
418
- quad: sanitized,
419
- area: quadAreaValue,
420
- };
421
- if (!bestLocal || candidate.area > bestLocal.area) {
422
- bestLocal = candidate;
423
- }
424
- }
425
- return bestLocal;
426
- };
427
- const runCanny = (label, low, high) => {
428
- 'worklet';
429
- const working = react_native_fast_opencv_1.OpenCV.invoke('clone', baseMat);
430
- step = `${label}_canny`;
431
- reportStage(step);
432
- react_native_fast_opencv_1.OpenCV.invoke('Canny', working, working, low, high);
433
- react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', working, working, react_native_fast_opencv_1.MorphTypes.MORPH_CLOSE, element);
434
- considerCandidate(evaluateContours(working, label));
435
- };
436
- const runAdaptive = (label, blockSize, c) => {
437
- 'worklet';
438
- const working = react_native_fast_opencv_1.OpenCV.invoke('clone', baseMat);
439
- step = `${label}_adaptive`;
440
- reportStage(step);
441
- react_native_fast_opencv_1.OpenCV.invoke('adaptiveThreshold', working, working, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, blockSize, c);
442
- react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', working, working, react_native_fast_opencv_1.MorphTypes.MORPH_CLOSE, element);
443
- considerCandidate(evaluateContours(working, label));
444
- };
445
- const runOtsu = () => {
446
- 'worklet';
447
- const working = react_native_fast_opencv_1.OpenCV.invoke('clone', baseMat);
448
- step = 'otsu_threshold';
449
- reportStage(step);
450
- react_native_fast_opencv_1.OpenCV.invoke('threshold', working, working, 0, 255, THRESH_BINARY | THRESH_OTSU);
451
- react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', working, working, react_native_fast_opencv_1.MorphTypes.MORPH_CLOSE, element);
452
- considerCandidate(evaluateContours(working, 'otsu'));
453
- };
454
- runCanny('canny_primary', CANNY_LOW, CANNY_HIGH);
455
- runCanny('canny_soft', Math.max(6, CANNY_LOW * 0.6), Math.max(CANNY_LOW * 1.2, CANNY_HIGH * 0.75));
456
- runCanny('canny_hard', Math.max(12, CANNY_LOW * 1.1), CANNY_HIGH * 1.25);
457
- runAdaptive('adaptive_19', 19, 7);
458
- runAdaptive('adaptive_23', 23, 5);
459
- runOtsu();
460
- step = 'clearBuffers';
461
- reportStage(step);
462
- react_native_fast_opencv_1.OpenCV.clearBuffers();
463
- step = 'updateQuad';
464
- reportStage(step);
465
- updateQuad(bestQuad);
73
+ const handlePictureTaken = (0, react_1.useCallback)((event) => {
74
+ capturingRef.current = false;
75
+ const path = event?.croppedImage ?? event?.initialImage;
76
+ if (!path) {
77
+ return;
466
78
  }
467
- catch (error) {
468
- reportError(step, error);
79
+ const width = event?.width ?? frameSize?.width ?? 0;
80
+ const height = event?.height ?? frameSize?.height ?? 0;
81
+ onCapture?.({
82
+ path,
83
+ quad,
84
+ width,
85
+ height,
86
+ });
87
+ }, [frameSize, onCapture, quad]);
88
+ const handleManualCapture = (0, react_1.useCallback)(() => {
89
+ if (autoCapture || capturingRef.current || !scannerRef.current) {
90
+ return;
469
91
  }
470
- }, [resize, reportError, updateQuad]);
471
- (0, react_1.useEffect)(() => {
472
- const s = (0, stability_1.checkStability)(quad);
473
- setStable(s);
474
- }, [quad]);
475
- (0, react_1.useEffect)(() => {
476
- const capture = async () => {
477
- if (autoCapture && quad && stable >= minStableFrames && camera.current && frameSize) {
478
- const photo = await camera.current.takePhoto({ qualityPrioritization: 'quality' });
479
- onCapture?.({
480
- path: photo.path,
481
- quad,
482
- width: frameSize.width,
483
- height: frameSize.height,
484
- });
485
- setStable(0);
486
- }
487
- };
488
- capture();
489
- }, [autoCapture, minStableFrames, onCapture, quad, stable, frameSize]);
490
- const { device: overrideDevice, ...cameraRestProps } = cameraProps ?? {};
491
- const resolvedDevice = overrideDevice ?? device;
492
- if (!resolvedDevice || !hasPermission) {
493
- return null;
494
- }
495
- return (react_1.default.createElement(react_native_1.View, { style: { flex: 1 } },
496
- react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: handleCameraRef, style: react_native_1.StyleSheet.absoluteFillObject, device: resolvedDevice, isActive: true, photo: true, frameProcessor: frameProcessor, frameProcessorFps: 15, ...cameraRestProps }),
497
- react_1.default.createElement(overlay_1.Overlay, { quad: quad, color: overlayColor, frameSize: frameSize }),
498
- !autoCapture && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: async () => {
499
- if (!camera.current || !frameSize) {
500
- return;
501
- }
502
- const photo = await camera.current.takePhoto({ qualityPrioritization: 'quality' });
503
- onCapture?.({
504
- path: photo.path,
505
- quad,
506
- width: frameSize.width,
507
- height: frameSize.height,
508
- });
509
- } })),
92
+ capturingRef.current = true;
93
+ scannerRef.current
94
+ .capture()
95
+ .catch((error) => {
96
+ console.warn('[DocScanner] manual capture failed', error);
97
+ capturingRef.current = false;
98
+ });
99
+ }, [autoCapture]);
100
+ return (react_1.default.createElement(react_native_1.View, { style: styles.container, onLayout: handleLayout },
101
+ react_1.default.createElement(NativeDocumentScanner, { ref: (instance) => {
102
+ scannerRef.current = instance;
103
+ }, style: react_native_1.StyleSheet.absoluteFill, overlayColor: "transparent", detectionCountBeforeCapture: autoCapture ? minStableFrames : 10000, enableTorch: enableTorch, hideControls: true, useBase64: useBase64, quality: quality, onRectangleDetect: handleRectangleDetect, onPictureTaken: handlePictureTaken }),
104
+ react_1.default.createElement(overlay_1.Overlay, { quad: quad, color: overlayColor, frameSize: frameSize, showGrid: showGrid, gridColor: effectiveGridColor, gridLineWidth: gridLineWidth }),
105
+ !autoCapture && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: handleManualCapture })),
510
106
  children));
511
107
  };
512
108
  exports.DocScanner = DocScanner;
513
109
  const styles = react_native_1.StyleSheet.create({
110
+ container: {
111
+ flex: 1,
112
+ },
514
113
  button: {
515
114
  position: 'absolute',
516
115
  bottom: 40,
package/dist/index.d.ts CHANGED
@@ -5,4 +5,3 @@ export type { FullDocScannerResult, FullDocScannerProps, FullDocScannerStrings,
5
5
  export type { Point, Quad, Rectangle, CapturedDocument } from './types';
6
6
  export type { DetectionConfig } from './DocScanner';
7
7
  export { quadToRectangle, rectangleToQuad, scaleCoordinates, scaleRectangle, } from './utils/coordinate';
8
- export { DocumentDetector } from './utils/documentDetection';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DocumentDetector = exports.scaleRectangle = exports.scaleCoordinates = exports.rectangleToQuad = exports.quadToRectangle = exports.FullDocScanner = exports.CropEditor = exports.DocScanner = void 0;
3
+ exports.scaleRectangle = exports.scaleCoordinates = exports.rectangleToQuad = exports.quadToRectangle = exports.FullDocScanner = exports.CropEditor = exports.DocScanner = void 0;
4
4
  // Main components
5
5
  var DocScanner_1 = require("./DocScanner");
6
6
  Object.defineProperty(exports, "DocScanner", { enumerable: true, get: function () { return DocScanner_1.DocScanner; } });
@@ -14,5 +14,3 @@ Object.defineProperty(exports, "quadToRectangle", { enumerable: true, get: funct
14
14
  Object.defineProperty(exports, "rectangleToQuad", { enumerable: true, get: function () { return coordinate_1.rectangleToQuad; } });
15
15
  Object.defineProperty(exports, "scaleCoordinates", { enumerable: true, get: function () { return coordinate_1.scaleCoordinates; } });
16
16
  Object.defineProperty(exports, "scaleRectangle", { enumerable: true, get: function () { return coordinate_1.scaleRectangle; } });
17
- var documentDetection_1 = require("./utils/documentDetection");
18
- Object.defineProperty(exports, "DocumentDetector", { enumerable: true, get: function () { return documentDetection_1.DocumentDetector; } });
@@ -7,6 +7,9 @@ type OverlayProps = {
7
7
  width: number;
8
8
  height: number;
9
9
  } | null;
10
+ showGrid?: boolean;
11
+ gridColor?: string;
12
+ gridLineWidth?: number;
10
13
  };
11
14
  export declare const Overlay: React.FC<OverlayProps>;
12
15
  export {};