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.
package/src/external.d.ts CHANGED
@@ -1,78 +1,46 @@
1
- declare module 'react-native-vision-camera' {
1
+ declare module 'react-native-document-scanner-plugin' {
2
2
  import type { ComponentType } from 'react';
3
3
  import type { ViewStyle } from 'react-native';
4
4
 
5
- export type CameraDevice = {
6
- id: string;
7
- name: string;
8
- } | null;
9
-
10
- export type Frame = {
11
- width: number;
12
- height: number;
5
+ export type RectangleCoordinates = {
6
+ topLeft: { x: number; y: number };
7
+ topRight: { x: number; y: number };
8
+ bottomRight: { x: number; y: number };
9
+ bottomLeft: { x: number; y: number };
13
10
  };
14
11
 
15
- export type TakePhotoOptions = {
16
- qualityPrioritization?: 'balanced' | 'quality' | 'speed';
12
+ export type RectangleEvent = {
13
+ rectangleCoordinates?: RectangleCoordinates;
14
+ stableCounter?: number;
15
+ lastDetectionType?: 'initial' | 'updated' | 'lost';
17
16
  };
18
17
 
19
- export type CameraRef = {
20
- takePhoto: (options?: TakePhotoOptions) => Promise<{
21
- path: string;
22
- }>;
18
+ export type CaptureResult = {
19
+ croppedImage?: string;
20
+ initialImage?: string;
21
+ width?: number;
22
+ height?: number;
23
23
  };
24
24
 
25
- export type CameraProps = {
26
- ref?: (value: CameraRef | null) => void;
25
+ export type DocumentScannerProps = {
26
+ ref?: (value: DocumentScannerHandle | null) => void;
27
27
  style?: ViewStyle;
28
- device: CameraDevice;
29
- isActive?: boolean;
30
- photo?: boolean;
31
- frameProcessor?: (frame: Frame) => void;
32
- frameProcessorFps?: number;
33
- };
34
-
35
- export const Camera: ComponentType<CameraProps>;
36
- export function useCameraDevice(position?: 'back' | 'front'): CameraDevice;
37
- export function useCameraPermission(): {
38
- hasPermission: boolean;
39
- requestPermission: () => Promise<void>;
40
- };
41
- export function useFrameProcessor(
42
- processor: (frame: Frame) => void,
43
- deps?: ReadonlyArray<unknown>
44
- ): (frame: Frame) => void;
45
- }
46
-
47
- declare module 'react-native-reanimated' {
48
- export function runOnJS<T extends (...args: any[]) => any>(fn: T): T;
49
- }
50
-
51
- declare module 'vision-camera-resize-plugin' {
52
- import type { Frame } from 'react-native-vision-camera';
53
-
54
- type ResizeOptions = {
55
- dataType: 'uint8';
56
- pixelFormat: 'bgr';
57
- scale: {
58
- width: number;
59
- height: number;
60
- };
28
+ overlayColor?: string;
29
+ detectionCountBeforeCapture?: number;
30
+ enableTorch?: boolean;
31
+ hideControls?: boolean;
32
+ useBase64?: boolean;
33
+ quality?: number;
34
+ onRectangleDetect?: (event: RectangleEvent) => void;
35
+ onPictureTaken?: (result: CaptureResult) => void;
61
36
  };
62
37
 
63
- export function useResizePlugin(): {
64
- resize: (frame: Frame, options: ResizeOptions) => ArrayBuffer;
38
+ export type DocumentScannerHandle = {
39
+ capture: () => Promise<CaptureResult>;
65
40
  };
66
- }
67
41
 
68
- declare module 'react-native-fast-opencv' {
69
- export const OpenCV: any;
70
- export const ColorConversionCodes: any;
71
- export const MorphTypes: any;
72
- export const MorphShapes: any;
73
- export const RetrievalModes: any;
74
- export const ContourApproximationModes: any;
75
- export const ObjectType: any;
42
+ const DocumentScanner: ComponentType<DocumentScannerProps>;
43
+ export default DocumentScanner;
76
44
  }
77
45
 
78
46
  declare module '@shopify/react-native-skia' {
package/src/index.ts CHANGED
@@ -20,4 +20,3 @@ export {
20
20
  scaleCoordinates,
21
21
  scaleRectangle,
22
22
  } from './utils/coordinate';
23
- export { DocumentDetector } from './utils/documentDetection';
@@ -3,21 +3,41 @@ import { View, StyleSheet, useWindowDimensions } from 'react-native';
3
3
  import { Canvas, Path, Skia } from '@shopify/react-native-skia';
4
4
  import type { Point } from '../types';
5
5
 
6
+ const lerp = (start: Point, end: Point, t: number): Point => ({
7
+ x: start.x + (end.x - start.x) * t,
8
+ y: start.y + (end.y - start.y) * t,
9
+ });
10
+
6
11
  type OverlayProps = {
7
12
  quad: Point[] | null;
8
13
  color?: string;
9
14
  frameSize: { width: number; height: number } | null;
15
+ showGrid?: boolean;
16
+ gridColor?: string;
17
+ gridLineWidth?: number;
18
+ };
19
+
20
+ type OverlayGeometry = {
21
+ outlinePath: ReturnType<typeof Skia.Path.Make> | null;
22
+ gridPaths: ReturnType<typeof Skia.Path.Make>[];
10
23
  };
11
24
 
12
- export const Overlay: React.FC<OverlayProps> = ({ quad, color = '#e7a649', frameSize }) => {
25
+ export const Overlay: React.FC<OverlayProps> = ({
26
+ quad,
27
+ color = '#e7a649',
28
+ frameSize,
29
+ showGrid = true,
30
+ gridColor = 'rgba(231, 166, 73, 0.35)',
31
+ gridLineWidth = 2,
32
+ }) => {
13
33
  const { width: screenWidth, height: screenHeight } = useWindowDimensions();
14
34
 
15
- const path = useMemo(() => {
35
+ const { outlinePath, gridPaths }: OverlayGeometry = useMemo(() => {
16
36
  if (!quad || !frameSize) {
17
37
  if (__DEV__) {
18
38
  console.log('[Overlay] no quad or frameSize', { quad, frameSize });
19
39
  }
20
- return null;
40
+ return { outlinePath: null, gridPaths: [] };
21
41
  }
22
42
 
23
43
  if (__DEV__) {
@@ -76,8 +96,33 @@ export const Overlay: React.FC<OverlayProps> = ({ quad, color = '#e7a649', frame
76
96
  skPath.moveTo(transformedQuad[0].x, transformedQuad[0].y);
77
97
  transformedQuad.slice(1).forEach((p) => skPath.lineTo(p.x, p.y));
78
98
  skPath.close();
79
- return skPath;
80
- }, [quad, color, screenWidth, screenHeight, frameSize]);
99
+ const grid: ReturnType<typeof Skia.Path.Make>[] = [];
100
+
101
+ if (showGrid) {
102
+ const [topLeft, topRight, bottomRight, bottomLeft] = transformedQuad;
103
+ const steps = [1 / 3, 2 / 3];
104
+
105
+ steps.forEach((t) => {
106
+ const start = lerp(topLeft, topRight, t);
107
+ const end = lerp(bottomLeft, bottomRight, t);
108
+ const verticalPath = Skia.Path.Make();
109
+ verticalPath.moveTo(start.x, start.y);
110
+ verticalPath.lineTo(end.x, end.y);
111
+ grid.push(verticalPath);
112
+ });
113
+
114
+ steps.forEach((t) => {
115
+ const start = lerp(topLeft, bottomLeft, t);
116
+ const end = lerp(topRight, bottomRight, t);
117
+ const horizontalPath = Skia.Path.Make();
118
+ horizontalPath.moveTo(start.x, start.y);
119
+ horizontalPath.lineTo(end.x, end.y);
120
+ grid.push(horizontalPath);
121
+ });
122
+ }
123
+
124
+ return { outlinePath: skPath, gridPaths: grid };
125
+ }, [quad, screenWidth, screenHeight, frameSize, showGrid]);
81
126
 
82
127
  if (__DEV__) {
83
128
  console.log('[Overlay] rendering Canvas with dimensions:', screenWidth, 'x', screenHeight);
@@ -86,10 +131,20 @@ export const Overlay: React.FC<OverlayProps> = ({ quad, color = '#e7a649', frame
86
131
  return (
87
132
  <View style={styles.container} pointerEvents="none">
88
133
  <Canvas style={{ width: screenWidth, height: screenHeight }}>
89
- {path && (
134
+ {outlinePath && (
90
135
  <>
91
- <Path path={path} color={color} style="stroke" strokeWidth={8} />
92
- <Path path={path} color="rgba(231, 166, 73, 0.2)" style="fill" />
136
+ <Path path={outlinePath} color={color} style="stroke" strokeWidth={8} />
137
+ <Path path={outlinePath} color="rgba(231, 166, 73, 0.2)" style="fill" />
138
+ {gridPaths.map((gridPath, index) => (
139
+ <Path
140
+ // eslint-disable-next-line react/no-array-index-key
141
+ key={`grid-${index}`}
142
+ path={gridPath}
143
+ color={gridColor}
144
+ style="stroke"
145
+ strokeWidth={gridLineWidth}
146
+ />
147
+ ))}
93
148
  </>
94
149
  )}
95
150
  </Canvas>
@@ -1,278 +0,0 @@
1
- import {
2
- OpenCV,
3
- ColorConversionCodes,
4
- MorphShapes,
5
- MorphTypes,
6
- RetrievalModes,
7
- ContourApproximationModes,
8
- } from 'react-native-fast-opencv';
9
- import type { Point } from '../types';
10
-
11
- type MatLike = { release?: () => void } | null | undefined;
12
-
13
- type Size = { width: number; height: number };
14
-
15
- type Quad = [Point, Point, Point, Point];
16
-
17
- const OUTPUT_SIZE: Size = { width: 800, height: 600 };
18
- const MIN_AREA = 1000;
19
- const GAUSSIAN_KERNEL: Size = { width: 5, height: 5 };
20
- const MORPH_KERNEL: Size = { width: 3, height: 3 };
21
- const ADAPTIVE_THRESH_GAUSSIAN_C = 1;
22
- const THRESH_BINARY = 0;
23
-
24
- const safeRelease = (mat: MatLike) => {
25
- if (mat && typeof mat.release === 'function') {
26
- mat.release();
27
- }
28
- };
29
-
30
- const normalizePoint = (value: unknown): Point | null => {
31
- if (!value) {
32
- return null;
33
- }
34
-
35
- if (Array.isArray(value) && value.length >= 2) {
36
- const [x, y] = value;
37
- const px = Number(x);
38
- const py = Number(y);
39
- return Number.isFinite(px) && Number.isFinite(py) ? { x: px, y: py } : null;
40
- }
41
-
42
- if (typeof value === 'object') {
43
- const maybePoint = value as { x?: unknown; y?: unknown };
44
- const px = Number(maybePoint.x);
45
- const py = Number(maybePoint.y);
46
- return Number.isFinite(px) && Number.isFinite(py) ? { x: px, y: py } : null;
47
- }
48
-
49
- return null;
50
- };
51
-
52
- const toPointArray = (value: unknown): Point[] | null => {
53
- if (!value) {
54
- return null;
55
- }
56
-
57
- if (Array.isArray(value)) {
58
- const points = value.map(normalizePoint).filter((point): point is Point => point !== null);
59
- return points.length === value.length ? points : null;
60
- }
61
-
62
- if (typeof value === 'object') {
63
- const mat = value as { data32F?: number[]; data64F?: number[]; data32S?: number[] };
64
- const data = mat.data32F ?? mat.data64F ?? mat.data32S;
65
- if (!data || data.length < 8) {
66
- return null;
67
- }
68
-
69
- const points: Point[] = [];
70
- for (let i = 0; i + 1 < data.length; i += 2) {
71
- const x = data[i];
72
- const y = data[i + 1];
73
- if (Number.isFinite(x) && Number.isFinite(y)) {
74
- points.push({ x, y });
75
- }
76
- }
77
-
78
- return points.length >= 4 ? points.slice(0, 4) : null;
79
- }
80
-
81
- return null;
82
- };
83
-
84
- const ensureQuad = (points: Point[] | null): Quad | null => {
85
- if (!points || points.length < 4) {
86
- return null;
87
- }
88
-
89
- const quad: Quad = [points[0], points[1], points[2], points[3]];
90
- for (const point of quad) {
91
- if (typeof point.x !== 'number' || typeof point.y !== 'number') {
92
- return null;
93
- }
94
- }
95
-
96
- return quad;
97
- };
98
-
99
- /**
100
- * Provides document detection utilities using react-native-fast-opencv.
101
- */
102
- export class DocumentDetector {
103
- private static initialized = false;
104
-
105
- /** Initialize OpenCV runtime once */
106
- static async initialize(): Promise<void> {
107
- if (!DocumentDetector.initialized) {
108
- await OpenCV.initialize();
109
- DocumentDetector.initialized = true;
110
- }
111
- }
112
-
113
- /** Find document contours and return the largest quadrilateral */
114
- static async findDocumentContours(imagePath: string): Promise<Quad | null> {
115
- await DocumentDetector.initialize();
116
-
117
- let image: MatLike;
118
- let gray: MatLike;
119
- let blurred: MatLike;
120
- let thresh: MatLike;
121
- let morphed: MatLike;
122
- let kernel: MatLike;
123
-
124
- try {
125
- image = OpenCV.imread(imagePath);
126
- gray = OpenCV.cvtColor(image, ColorConversionCodes.COLOR_BGR2GRAY);
127
- blurred = OpenCV.GaussianBlur(gray, GAUSSIAN_KERNEL, 0);
128
-
129
- thresh = OpenCV.adaptiveThreshold(
130
- blurred,
131
- 255,
132
- ADAPTIVE_THRESH_GAUSSIAN_C,
133
- THRESH_BINARY,
134
- 11,
135
- 2,
136
- );
137
-
138
- kernel = OpenCV.getStructuringElement(MorphShapes.MORPH_RECT, MORPH_KERNEL);
139
- morphed = OpenCV.morphologyEx(thresh, MorphTypes.MORPH_CLOSE, kernel);
140
-
141
- const contours = OpenCV.findContours(
142
- morphed,
143
- RetrievalModes.RETR_EXTERNAL,
144
- ContourApproximationModes.CHAIN_APPROX_SIMPLE,
145
- );
146
-
147
- let largestQuad: Quad | null = null;
148
- let maxArea = 0;
149
-
150
- for (const contour of contours) {
151
- const area = OpenCV.contourArea(contour);
152
- if (area <= maxArea || area <= MIN_AREA) {
153
- continue;
154
- }
155
-
156
- const perimeter = OpenCV.arcLength(contour, true);
157
- const approx = OpenCV.approxPolyDP(contour, 0.02 * perimeter, true);
158
- const points = ensureQuad(toPointArray(approx));
159
-
160
- safeRelease(approx as MatLike);
161
-
162
- if (!points) {
163
- continue;
164
- }
165
-
166
- maxArea = area;
167
- largestQuad = points;
168
- }
169
-
170
- return largestQuad;
171
- } catch (error) {
172
- if (__DEV__) {
173
- console.error('[DocumentDetector] findDocumentContours error', error);
174
- }
175
- return null;
176
- } finally {
177
- safeRelease(kernel);
178
- safeRelease(morphed);
179
- safeRelease(thresh);
180
- safeRelease(blurred);
181
- safeRelease(gray);
182
- safeRelease(image);
183
- }
184
- }
185
-
186
- /** Apply a perspective transform using detected corners */
187
- static async perspectiveTransform(
188
- imagePath: string,
189
- corners: Quad,
190
- outputSize: Size = OUTPUT_SIZE,
191
- ): Promise<string | null> {
192
- await DocumentDetector.initialize();
193
-
194
- let image: MatLike;
195
- let srcPoints: MatLike;
196
- let dstPoints: MatLike;
197
- let transformMatrix: MatLike;
198
- let warped: MatLike;
199
-
200
- try {
201
- image = OpenCV.imread(imagePath);
202
-
203
- srcPoints = OpenCV.matFromArray(4, 1, OpenCV.CV_32FC2, [
204
- corners[0].x,
205
- corners[0].y,
206
- corners[1].x,
207
- corners[1].y,
208
- corners[2].x,
209
- corners[2].y,
210
- corners[3].x,
211
- corners[3].y,
212
- ]);
213
-
214
- dstPoints = OpenCV.matFromArray(4, 1, OpenCV.CV_32FC2, [
215
- 0,
216
- 0,
217
- outputSize.width,
218
- 0,
219
- outputSize.width,
220
- outputSize.height,
221
- 0,
222
- outputSize.height,
223
- ]);
224
-
225
- transformMatrix = OpenCV.getPerspectiveTransform(srcPoints, dstPoints);
226
-
227
- warped = OpenCV.warpPerspective(image, transformMatrix, outputSize);
228
-
229
- const outputPath = imagePath.replace(/\.jpg$/i, '_normalized.jpg');
230
- OpenCV.imwrite(outputPath, warped);
231
-
232
- return outputPath;
233
- } catch (error) {
234
- if (__DEV__) {
235
- console.error('[DocumentDetector] perspectiveTransform error', error);
236
- }
237
- return null;
238
- } finally {
239
- safeRelease(warped);
240
- safeRelease(transformMatrix);
241
- safeRelease(dstPoints);
242
- safeRelease(srcPoints);
243
- safeRelease(image);
244
- }
245
- }
246
-
247
- /** Detect document and apply normalization */
248
- static async detectAndNormalize(imagePath: string, outputSize?: Size): Promise<string | null> {
249
- try {
250
- const corners = await DocumentDetector.findDocumentContours(imagePath);
251
- if (!corners) {
252
- if (__DEV__) {
253
- console.log('[DocumentDetector] No document detected');
254
- }
255
- return null;
256
- }
257
-
258
- return DocumentDetector.perspectiveTransform(imagePath, corners, outputSize ?? OUTPUT_SIZE);
259
- } catch (error) {
260
- if (__DEV__) {
261
- console.error('[DocumentDetector] detectAndNormalize error', error);
262
- }
263
- return null;
264
- }
265
- }
266
-
267
- /** Only detect document corners without transforming */
268
- static async getDocumentBounds(imagePath: string): Promise<Quad | null> {
269
- try {
270
- return DocumentDetector.findDocumentContours(imagePath);
271
- } catch (error) {
272
- if (__DEV__) {
273
- console.error('[DocumentDetector] getDocumentBounds error', error);
274
- }
275
- return null;
276
- }
277
- }
278
- }
@@ -1,8 +0,0 @@
1
- import type { DependencyList } from 'react';
2
-
3
- declare module 'react-native-worklets-core' {
4
- export function useRunOnJS<T extends (...args: any[]) => any>(
5
- callback: T,
6
- deps: DependencyList
7
- ): (...args: Parameters<T>) => void;
8
- }