react-native-rectangle-doc-scanner 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,86 +1,52 @@
1
- # React Native Rectangle Doc Scanner
1
+ # React Native Document Scanner Wrapper
2
2
 
3
- > ⚠️ **Native module migration in progress**
4
- >
5
- > A native VisionKit (iOS) + CameraX/ML Kit (Android) implementation is being scaffolded to replace the previous VisionCamera/OpenCV pipeline. The JavaScript API is already aligned with the native contract; the detection/capture engines will be filled in next. See [`docs/native-module-architecture.md`](docs/native-module-architecture.md) for the roadmap.
3
+ React Native-friendly wrapper around [`react-native-document-scanner`](https://github.com/Michaelvilleneuve/react-native-document-scanner). It exposes a declarative `<DocScanner />` component that renders the native document scanner on both iOS and Android while keeping the surface area small enough to plug into custom UIs.
6
4
 
7
- Native-ready document scanner for React Native that keeps your overlay completely customisable. The library renders a native camera preview, streams polygon detections back to JavaScript, and exposes an imperative `capture()` method so you can build the exact UX you need.
8
-
9
- ## Features
10
-
11
- - Native camera preview surfaces on iOS/Android with React overlay support
12
- - Polygon detection events (with stability counter) delivered every frame
13
- - Skia-powered outline + optional 3×3 grid overlay
14
- - Auto-capture and manual capture flows using the same API
15
- - Optional `CropEditor` powered by `react-native-perspective-image-cropper`
16
-
17
- ## Requirements
18
-
19
- - React Native 0.70+
20
- - iOS 13+ (VisionKit availability) / Android 7.0+ (API 24)
21
- - Camera permission strings in your host app (`NSCameraUsageDescription`, Android runtime permission handling)
22
- - Peer dependencies:
23
- - `@shopify/react-native-skia`
24
- - `react-native-perspective-image-cropper`
25
- - `react`
26
- - `react-native`
5
+ > The native implementation lives inside the upstream library (Objective‑C/OpenCV on iOS, Kotlin/OpenCV on Android). This package simply re-exports a type-safe wrapper, optional crop editor helpers, and a full-screen scanner flow.
27
6
 
28
7
  ## Installation
29
8
 
30
- ```sh
9
+ ```bash
31
10
  yarn add react-native-rectangle-doc-scanner \
32
- @shopify/react-native-skia \
11
+ github:Michaelvilleneuve/react-native-document-scanner \
33
12
  react-native-perspective-image-cropper
34
13
 
35
14
  # iOS
36
15
  cd ios && pod install
37
16
  ```
38
17
 
39
- Android will automatically pick up the included Gradle configuration. If you use a custom package list (old architecture), register `new RNRDocScannerPackage()` manually.
18
+ Android automatically links the native module. If you manage packages manually (legacy architecture), register `DocumentScannerPackage()` in your `MainApplication`.
40
19
 
41
20
  ## Usage
42
21
 
43
22
  ```tsx
44
- import React, { useState } from 'react';
45
- import { StyleSheet, Text, View } from 'react-native';
46
- import {
47
- DocScanner,
48
- CropEditor,
49
- type CapturedDocument,
50
- } from 'react-native-rectangle-doc-scanner';
51
-
52
- export const ScanScreen: React.FC = () => {
53
- const [capturedDoc, setCapturedDoc] = useState<CapturedDocument | null>(null);
54
-
55
- if (capturedDoc) {
56
- return (
57
- <CropEditor
58
- document={capturedDoc}
59
- overlayColor="rgba(0,0,0,0.6)"
60
- overlayStrokeColor="#e7a649"
61
- handlerColor="#e7a649"
62
- onCropChange={(rectangle) => {
63
- console.log('Adjusted corners:', rectangle);
64
- }}
65
- />
66
- );
67
- }
23
+ import React, { useRef } from 'react';
24
+ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
25
+ import { DocScanner, type DocScannerHandle } from 'react-native-rectangle-doc-scanner';
26
+
27
+ export const ScanScreen = () => {
28
+ const scannerRef = useRef<DocScannerHandle>(null);
68
29
 
69
30
  return (
70
31
  <View style={styles.container}>
71
32
  <DocScanner
72
- overlayColor="#e7a649"
73
- minStableFrames={8}
33
+ ref={scannerRef}
34
+ overlayColor="rgba(0, 126, 244, 0.35)"
74
35
  autoCapture
75
- onCapture={(doc) => {
76
- console.log('Captured document:', doc);
77
- setCapturedDoc(doc);
36
+ minStableFrames={6}
37
+ onCapture={(result) => {
38
+ console.log('Captured document:', result.path);
78
39
  }}
79
40
  >
80
- <View style={styles.overlay}>
81
- <Text style={styles.hint}>Align the document with the frame</Text>
41
+ <View style={styles.overlay} pointerEvents="none">
42
+ <Text style={styles.hint}>Align the document inside the frame</Text>
82
43
  </View>
83
44
  </DocScanner>
45
+
46
+ <TouchableOpacity
47
+ style={styles.captureButton}
48
+ onPress={() => scannerRef.current?.capture()}
49
+ />
84
50
  </View>
85
51
  );
86
52
  };
@@ -92,78 +58,41 @@ const styles = StyleSheet.create({
92
58
  top: 60,
93
59
  alignSelf: 'center',
94
60
  paddingHorizontal: 20,
95
- paddingVertical: 8,
61
+ paddingVertical: 10,
96
62
  borderRadius: 12,
97
- backgroundColor: 'rgba(0,0,0,0.55)',
63
+ backgroundColor: 'rgba(0,0,0,0.5)',
64
+ },
65
+ hint: { color: '#fff', fontWeight: '600' },
66
+ captureButton: {
67
+ position: 'absolute',
68
+ bottom: 40,
69
+ alignSelf: 'center',
70
+ width: 70,
71
+ height: 70,
72
+ borderRadius: 35,
73
+ backgroundColor: '#fff',
98
74
  },
99
- hint: { color: '#fff', fontSize: 15, fontWeight: '600' },
100
75
  });
101
76
  ```
102
77
 
103
- The native view renders underneath; anything you pass as `children` sits on top, so you can add custom buttons, headers, progress indicators, etc.
104
-
105
- ## API
106
-
107
- ### `<DocScanner />`
108
-
109
- | Prop | Type | Default | Description |
110
- | --- | --- | --- | --- |
111
- | `onCapture` | `(result) => void` | – | Fired when a capture resolves. Returns `path`, `width`, `height`, `quad`. |
112
- | `overlayColor` | `string` | `#e7a649` | Stroke colour for the overlay outline. |
113
- | `autoCapture` | `boolean` | `true` | When `true`, capture is triggered automatically once stability is reached. |
114
- | `minStableFrames` | `number` | `8` | Number of stable frames before auto capture fires. |
115
- | `enableTorch` | `boolean` | `false` | Toggles device torch (if supported). |
116
- | `quality` | `number` | `90` | JPEG quality (0–100). |
117
- | `useBase64` | `boolean` | `false` | Return base64 strings instead of file URIs. |
118
- | `showGrid` | `boolean` | `true` | Show the 3×3 helper grid inside the overlay. |
119
- | `gridColor` | `string` | `rgba(231,166,73,0.35)` | Colour of grid lines. |
120
- | `gridLineWidth` | `number` | `2` | Width of grid lines. |
121
-
122
- Imperative helpers are exposed via `DocScannerHandle`:
123
-
124
- ```ts
125
- import { DocScanner, type DocScannerHandle } from 'react-native-rectangle-doc-scanner';
126
-
127
- const ref = useRef<DocScannerHandle>(null);
128
-
129
- const fireCapture = () => {
130
- ref.current?.capture().then((result) => {
131
- console.log(result);
132
- });
133
- };
134
- ```
135
-
136
- > The native module currently returns a `"not_implemented"` error from `capture()` until the VisionKit / ML Kit integration is finished. The JS surface is ready for when the native pipeline lands.
78
+ `<DocScanner />` passes through the important upstream props:
137
79
 
138
- ### `<CropEditor />`
139
-
140
- | Prop | Type | Default | Description |
80
+ | Prop | Type | Default | Notes |
141
81
  | --- | --- | --- | --- |
142
- | `document` | `CapturedDocument` | | Document produced by `onCapture`. |
143
- | `overlayColor` | `string` | `rgba(0,0,0,0.5)` | Tint outside the crop area. |
144
- | `overlayStrokeColor` | `string` | `#e7a649` | Boundary colour. |
145
- | `handlerColor` | `string` | `#e7a649` | Corner handle colour. |
146
- | `enablePanStrict` | `boolean` | `false` | Enable strict panning behaviour. |
147
- | `onCropChange` | `(rectangle) => void` | | Fires when the user drags handles. |
148
-
149
- ## Native scaffolding status
82
+ | `overlayColor` | `string` | `#0b7ef4` | Native overlay tint. |
83
+ | `autoCapture` | `boolean` | `true` | Maps to `manualOnly` internally. |
84
+ | `minStableFrames` | `number` | `8` | Detection count before auto capture. |
85
+ | `enableTorch` | `boolean` | `false` | Toggle device torch. |
86
+ | `quality` | `number` | `90` | 0–100 (converted for native). |
87
+ | `useBase64` | `boolean` | `false` | Return base64 payloads instead of file URIs. |
88
+ | `onCapture` | `(result) => void` | — | Receives `{ path, quad: null, width, height }`. |
150
89
 
151
- - TypeScript wrapper + overlay grid
152
- - ✅ iOS view manager / module skeleton (Swift)
153
- - ✅ Android view manager / module skeleton (Kotlin)
154
- - ☐ VisionKit rectangle detection & capture pipeline
155
- - ☐ CameraX + ML Kit rectangle detection & capture pipeline
156
- - ☐ Base64 / file output parity tests
90
+ Manual capture exposes an imperative `capture()` method via `ref`. Children render on top of the camera preview so you can build your own buttons, progress indicators, or onboarding tips.
157
91
 
158
- Contributions to the native pipeline are welcome! Start by reading [`docs/native-module-architecture.md`](docs/native-module-architecture.md) for the current plan.
159
-
160
- ## Build
161
-
162
- ```sh
163
- yarn build
164
- ```
92
+ ## Convenience APIs
165
93
 
166
- Generates the `dist/` output via TypeScript.
94
+ - `CropEditor` – wraps `react-native-perspective-image-cropper` for manual corner adjustment.
95
+ - `FullDocScanner` – puts the scanner and crop editor into a single modal-like flow.
167
96
 
168
97
  ## License
169
98
 
@@ -1,15 +1,10 @@
1
1
  import React, { ReactNode } from 'react';
2
- import type { Point } from './types';
3
2
  type PictureEvent = {
4
3
  croppedImage?: string | null;
5
- initialImage?: string;
4
+ initialImage?: string | null;
6
5
  width?: number;
7
6
  height?: number;
8
7
  };
9
- type DocScannerHandle = {
10
- capture: () => Promise<PictureEvent>;
11
- reset: () => void;
12
- };
13
8
  export interface DetectionConfig {
14
9
  processingWidth?: number;
15
10
  cannyLowThreshold?: number;
@@ -21,7 +16,7 @@ export interface DetectionConfig {
21
16
  interface Props {
22
17
  onCapture?: (photo: {
23
18
  path: string;
24
- quad: Point[] | null;
19
+ quad: null;
25
20
  width: number;
26
21
  height: number;
27
22
  }) => void;
@@ -36,7 +31,10 @@ interface Props {
36
31
  gridColor?: string;
37
32
  gridLineWidth?: number;
38
33
  detectionConfig?: DetectionConfig;
39
- useNativeOverlay?: boolean;
40
34
  }
35
+ type DocScannerHandle = {
36
+ capture: () => Promise<PictureEvent>;
37
+ reset: () => void;
38
+ };
41
39
  export declare const DocScanner: React.ForwardRefExoticComponent<Props & React.RefAttributes<DocScannerHandle>>;
42
40
  export type { DocScannerHandle };
@@ -32,138 +32,85 @@ 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 overlay_1 = require("./utils/overlay");
40
- const MODULE_NAME = 'RNRDocScannerModule';
41
- const VIEW_NAME = 'RNRDocScannerView';
42
- const NativeDocScannerModule = react_native_1.NativeModules[MODULE_NAME];
43
- if (!NativeDocScannerModule) {
44
- const fallbackMessage = `The native module '${MODULE_NAME}' is not linked. Make sure you have run pod install, ` +
45
- `synced Gradle, and rebuilt the app after installing 'react-native-rectangle-doc-scanner'.`;
46
- throw new Error(fallbackMessage);
47
- }
48
- const NativeDocScanner = (0, react_native_1.requireNativeComponent)(VIEW_NAME);
49
- const DEFAULT_OVERLAY_COLOR = '#e7a649';
50
- const GRID_COLOR_FALLBACK = 'rgba(231, 166, 73, 0.35)';
51
- 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 = 2, useNativeOverlay, }, ref) => {
52
- const viewRef = (0, react_1.useRef)(null);
53
- const capturingRef = (0, react_1.useRef)(false);
54
- const [quad, setQuad] = (0, react_1.useState)(null);
55
- const [stable, setStable] = (0, react_1.useState)(0);
56
- const [frameSize, setFrameSize] = (0, react_1.useState)(null);
57
- const shouldUseNativeOverlay = (0, react_1.useMemo)(() => {
58
- if (typeof useNativeOverlay === 'boolean') {
59
- return useNativeOverlay;
42
+ const react_native_document_scanner_1 = __importDefault(require("react-native-document-scanner"));
43
+ const DEFAULT_OVERLAY_COLOR = '#0b7ef4';
44
+ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAULT_OVERLAY_COLOR, autoCapture = true, minStableFrames = 8, enableTorch = false, quality = 90, useBase64 = false, children, showGrid = true, }, ref) => {
45
+ const scannerRef = (0, react_1.useRef)(null);
46
+ const captureResolvers = (0, react_1.useRef)(null);
47
+ const normalizedQuality = (0, react_1.useMemo)(() => {
48
+ if (react_native_1.Platform.OS === 'ios') {
49
+ // iOS expects 0-1
50
+ return Math.min(1, Math.max(0, quality / 100));
60
51
  }
61
- return react_native_1.Platform.OS === 'ios';
62
- }, [useNativeOverlay]);
63
- const effectiveGridColor = (0, react_1.useMemo)(() => gridColor ?? GRID_COLOR_FALLBACK, [gridColor]);
64
- const ensureViewHandle = (0, react_1.useCallback)(() => {
65
- const nodeHandle = (0, react_native_1.findNodeHandle)(viewRef.current);
66
- if (!nodeHandle) {
67
- throw new Error('Unable to obtain native view handle for DocScanner.');
52
+ return Math.min(100, Math.max(0, quality));
53
+ }, [quality]);
54
+ const handlePictureTaken = (0, react_1.useCallback)((event) => {
55
+ const path = event.croppedImage ?? event.initialImage;
56
+ if (path) {
57
+ onCapture?.({
58
+ path,
59
+ quad: null,
60
+ width: event.width ?? 0,
61
+ height: event.height ?? 0,
62
+ });
68
63
  }
69
- return nodeHandle;
70
- }, []);
71
- const resetNativeStability = (0, react_1.useCallback)(() => {
72
- try {
73
- const handle = ensureViewHandle();
74
- NativeDocScannerModule.reset(handle);
64
+ if (captureResolvers.current) {
65
+ captureResolvers.current.resolve(event);
66
+ captureResolvers.current = null;
75
67
  }
76
- catch (error) {
77
- console.warn('[DocScanner] unable to reset native stability', error);
68
+ }, [onCapture]);
69
+ const handleError = (0, react_1.useCallback)((error) => {
70
+ if (captureResolvers.current) {
71
+ captureResolvers.current.reject(error);
72
+ captureResolvers.current = null;
78
73
  }
79
- }, [ensureViewHandle]);
80
- const emitCaptureResult = (0, react_1.useCallback)((payload) => {
81
- capturingRef.current = false;
82
- const path = payload.croppedImage ?? payload.initialImage;
83
- if (!path) {
84
- return;
85
- }
86
- const width = payload.width ?? frameSize?.width ?? 0;
87
- const height = payload.height ?? frameSize?.height ?? 0;
88
- onCapture?.({
89
- path,
90
- quad,
91
- width,
92
- height,
93
- });
94
- setStable(0);
95
- resetNativeStability();
96
- }, [frameSize, onCapture, quad, resetNativeStability]);
97
- const handleRectangleDetect = (0, react_1.useCallback)((event) => {
98
- const { rectangleCoordinates, stableCounter, frameWidth, frameHeight } = event.nativeEvent;
99
- setStable(stableCounter);
100
- setFrameSize({ width: frameWidth, height: frameHeight });
101
- if (!rectangleCoordinates) {
102
- setQuad(null);
103
- return;
104
- }
105
- setQuad([
106
- rectangleCoordinates.topLeft,
107
- rectangleCoordinates.topRight,
108
- rectangleCoordinates.bottomRight,
109
- rectangleCoordinates.bottomLeft,
110
- ]);
111
- if (autoCapture && stableCounter >= minStableFrames) {
112
- triggerCapture();
74
+ }, []);
75
+ const capture = (0, react_1.useCallback)(() => {
76
+ const instance = scannerRef.current;
77
+ if (!instance || typeof instance.capture !== 'function') {
78
+ return Promise.reject(new Error('DocumentScanner native instance is not ready'));
113
79
  }
114
- }, [autoCapture, minStableFrames]);
115
- const handlePictureTaken = (0, react_1.useCallback)((event) => {
116
- emitCaptureResult(event.nativeEvent);
117
- }, [emitCaptureResult]);
118
- const captureNative = (0, react_1.useCallback)(() => {
119
- if (capturingRef.current) {
80
+ if (captureResolvers.current) {
120
81
  return Promise.reject(new Error('capture_in_progress'));
121
82
  }
122
- try {
123
- const handle = ensureViewHandle();
124
- capturingRef.current = true;
125
- return NativeDocScannerModule.capture(handle)
126
- .then((result) => {
127
- emitCaptureResult(result);
128
- return result;
129
- })
130
- .catch((error) => {
131
- capturingRef.current = false;
132
- throw error;
83
+ const result = instance.capture();
84
+ if (result && typeof result.then === 'function') {
85
+ return result.then((payload) => {
86
+ handlePictureTaken(payload);
87
+ return payload;
133
88
  });
134
89
  }
135
- catch (error) {
136
- capturingRef.current = false;
137
- return Promise.reject(error);
138
- }
139
- }, [emitCaptureResult, ensureViewHandle]);
140
- const triggerCapture = (0, react_1.useCallback)(() => {
141
- if (capturingRef.current) {
142
- return;
143
- }
144
- captureNative().catch((error) => {
145
- console.warn('[DocScanner] capture failed', error);
90
+ return new Promise((resolve, reject) => {
91
+ captureResolvers.current = { resolve, reject };
146
92
  });
147
- }, [captureNative]);
93
+ }, [handlePictureTaken]);
148
94
  const handleManualCapture = (0, react_1.useCallback)(() => {
149
95
  if (autoCapture) {
150
96
  return;
151
97
  }
152
- captureNative().catch((error) => {
98
+ capture().catch((error) => {
153
99
  console.warn('[DocScanner] manual capture failed', error);
154
100
  });
155
- }, [autoCapture, captureNative]);
101
+ }, [autoCapture, capture]);
156
102
  (0, react_1.useImperativeHandle)(ref, () => ({
157
- capture: captureNative,
103
+ capture,
158
104
  reset: () => {
159
- setStable(0);
160
- resetNativeStability();
105
+ if (captureResolvers.current) {
106
+ captureResolvers.current.reject(new Error('reset'));
107
+ captureResolvers.current = null;
108
+ }
161
109
  },
162
- }), [captureNative, resetNativeStability]);
110
+ }), [capture]);
163
111
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
164
- react_1.default.createElement(NativeDocScanner, { ref: viewRef, style: react_native_1.StyleSheet.absoluteFill, detectionCountBeforeCapture: minStableFrames, autoCapture: autoCapture, enableTorch: enableTorch, quality: quality, useBase64: useBase64, onRectangleDetect: handleRectangleDetect, onPictureTaken: handlePictureTaken }),
165
- !shouldUseNativeOverlay && (react_1.default.createElement(overlay_1.Overlay, { quad: quad, color: overlayColor, frameSize: frameSize, showGrid: showGrid, gridColor: effectiveGridColor, gridLineWidth: gridLineWidth })),
166
- !autoCapture && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: handleManualCapture })),
112
+ react_1.default.createElement(react_native_document_scanner_1.default, { ref: scannerRef, style: react_native_1.StyleSheet.absoluteFillObject, detectionCountBeforeCapture: minStableFrames, overlayColor: overlayColor, enableTorch: enableTorch, quality: normalizedQuality, useBase64: useBase64, manualOnly: !autoCapture, onPictureTaken: handlePictureTaken, onError: handleError }),
113
+ !autoCapture && react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: handleManualCapture }),
167
114
  children));
168
115
  });
169
116
  const styles = react_native_1.StyleSheet.create({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -28,5 +28,7 @@
28
28
  "@types/react-native": "0.73.0",
29
29
  "typescript": "^5.3.3"
30
30
  },
31
- "dependencies": {}
31
+ "dependencies": {
32
+ "react-native-document-scanner": "github:Michaelvilleneuve/react-native-document-scanner"
33
+ }
32
34
  }