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.
@@ -5,73 +5,17 @@ import React, {
5
5
  useImperativeHandle,
6
6
  useMemo,
7
7
  useRef,
8
- useState,
9
8
  } from 'react';
10
- import {
11
- Platform,
12
- findNodeHandle,
13
- NativeModules,
14
- requireNativeComponent,
15
- StyleSheet,
16
- TouchableOpacity,
17
- View,
18
- } from 'react-native';
19
- import type { NativeSyntheticEvent } from 'react-native';
20
- import { Overlay } from './utils/overlay';
21
- import type { Point } from './types';
22
-
23
- const MODULE_NAME = 'RNRDocScannerModule';
24
- const VIEW_NAME = 'RNRDocScannerView';
25
-
26
- const NativeDocScannerModule = NativeModules[MODULE_NAME];
27
-
28
- if (!NativeDocScannerModule) {
29
- const fallbackMessage =
30
- `The native module '${MODULE_NAME}' is not linked. Make sure you have run pod install, ` +
31
- `synced Gradle, and rebuilt the app after installing 'react-native-rectangle-doc-scanner'.`;
32
- throw new Error(fallbackMessage);
33
- }
34
-
35
- type NativeRectangle = {
36
- topLeft: Point;
37
- topRight: Point;
38
- bottomRight: Point;
39
- bottomLeft: Point;
40
- };
41
-
42
- type RectangleEvent = {
43
- rectangleCoordinates: NativeRectangle | null;
44
- stableCounter: number;
45
- frameWidth: number;
46
- frameHeight: number;
47
- };
9
+ import { Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
10
+ import DocumentScanner from 'react-native-document-scanner';
48
11
 
49
12
  type PictureEvent = {
50
13
  croppedImage?: string | null;
51
- initialImage?: string;
14
+ initialImage?: string | null;
52
15
  width?: number;
53
16
  height?: number;
54
17
  };
55
18
 
56
- type NativeDocScannerProps = {
57
- style?: object;
58
- detectionCountBeforeCapture?: number;
59
- autoCapture?: boolean;
60
- enableTorch?: boolean;
61
- quality?: number;
62
- useBase64?: boolean;
63
- onRectangleDetect?: (event: NativeSyntheticEvent<RectangleEvent>) => void;
64
- onPictureTaken?: (event: NativeSyntheticEvent<PictureEvent>) => void;
65
- };
66
-
67
- type DocScannerHandle = {
68
- capture: () => Promise<PictureEvent>;
69
- reset: () => void;
70
- };
71
-
72
- const NativeDocScanner = requireNativeComponent<NativeDocScannerProps>(VIEW_NAME);
73
- type NativeDocScannerInstance = React.ElementRef<typeof NativeDocScanner>;
74
-
75
19
  export interface DetectionConfig {
76
20
  processingWidth?: number;
77
21
  cannyLowThreshold?: number;
@@ -82,7 +26,7 @@ export interface DetectionConfig {
82
26
  }
83
27
 
84
28
  interface Props {
85
- onCapture?: (photo: { path: string; quad: Point[] | null; width: number; height: number }) => void;
29
+ onCapture?: (photo: { path: string; quad: null; width: number; height: number }) => void;
86
30
  overlayColor?: string;
87
31
  autoCapture?: boolean;
88
32
  minStableFrames?: number;
@@ -94,200 +38,136 @@ interface Props {
94
38
  gridColor?: string;
95
39
  gridLineWidth?: number;
96
40
  detectionConfig?: DetectionConfig;
97
- useNativeOverlay?: boolean;
98
41
  }
99
42
 
100
- const DEFAULT_OVERLAY_COLOR = '#e7a649';
101
- const GRID_COLOR_FALLBACK = 'rgba(231, 166, 73, 0.35)';
102
-
103
- export const DocScanner = forwardRef<DocScannerHandle, Props>(({
104
- onCapture,
105
- overlayColor = DEFAULT_OVERLAY_COLOR,
106
- autoCapture = true,
107
- minStableFrames = 8,
108
- enableTorch = false,
109
- quality = 90,
110
- useBase64 = false,
111
- children,
112
- showGrid = true,
113
- gridColor,
114
- gridLineWidth = 2,
115
- useNativeOverlay,
116
- }, ref) => {
117
- const viewRef = useRef<NativeDocScannerInstance | null>(null);
118
- const capturingRef = useRef(false);
119
- const [quad, setQuad] = useState<Point[] | null>(null);
120
- const [stable, setStable] = useState(0);
121
- const [frameSize, setFrameSize] = useState<{ width: number; height: number } | null>(null);
122
-
123
- const shouldUseNativeOverlay = useMemo(() => {
124
- if (typeof useNativeOverlay === 'boolean') {
125
- return useNativeOverlay;
126
- }
127
- return Platform.OS === 'ios';
128
- }, [useNativeOverlay]);
129
-
130
- const effectiveGridColor = useMemo(
131
- () => gridColor ?? GRID_COLOR_FALLBACK,
132
- [gridColor],
133
- );
134
-
135
- const ensureViewHandle = useCallback(() => {
136
- const nodeHandle = findNodeHandle(viewRef.current);
137
- if (!nodeHandle) {
138
- throw new Error('Unable to obtain native view handle for DocScanner.');
139
- }
140
- return nodeHandle;
141
- }, []);
142
-
143
- const resetNativeStability = useCallback(() => {
144
- try {
145
- const handle = ensureViewHandle();
146
- NativeDocScannerModule.reset(handle);
147
- } catch (error) {
148
- console.warn('[DocScanner] unable to reset native stability', error);
149
- }
150
- }, [ensureViewHandle]);
151
-
152
- const emitCaptureResult = useCallback(
153
- (payload: PictureEvent) => {
154
- capturingRef.current = false;
155
-
156
- const path = payload.croppedImage ?? payload.initialImage;
157
- if (!path) {
158
- return;
159
- }
43
+ type DocScannerHandle = {
44
+ capture: () => Promise<PictureEvent>;
45
+ reset: () => void;
46
+ };
160
47
 
161
- const width = payload.width ?? frameSize?.width ?? 0;
162
- const height = payload.height ?? frameSize?.height ?? 0;
163
- onCapture?.({
164
- path,
165
- quad,
166
- width,
167
- height,
168
- });
169
- setStable(0);
170
- resetNativeStability();
48
+ const DEFAULT_OVERLAY_COLOR = '#0b7ef4';
49
+
50
+ export const DocScanner = forwardRef<DocScannerHandle, Props>(
51
+ (
52
+ {
53
+ onCapture,
54
+ overlayColor = DEFAULT_OVERLAY_COLOR,
55
+ autoCapture = true,
56
+ minStableFrames = 8,
57
+ enableTorch = false,
58
+ quality = 90,
59
+ useBase64 = false,
60
+ children,
61
+ showGrid = true,
171
62
  },
172
- [frameSize, onCapture, quad, resetNativeStability],
173
- );
174
-
175
- const handleRectangleDetect = useCallback(
176
- (event: NativeSyntheticEvent<RectangleEvent>) => {
177
- const { rectangleCoordinates, stableCounter, frameWidth, frameHeight } = event.nativeEvent;
178
- setStable(stableCounter);
179
- setFrameSize({ width: frameWidth, height: frameHeight });
180
-
181
- if (!rectangleCoordinates) {
182
- setQuad(null);
183
- return;
63
+ ref,
64
+ ) => {
65
+ const scannerRef = useRef<any>(null);
66
+ const captureResolvers = useRef<{
67
+ resolve: (value: PictureEvent) => void;
68
+ reject: (reason?: unknown) => void;
69
+ } | null>(null);
70
+
71
+ const normalizedQuality = useMemo(() => {
72
+ if (Platform.OS === 'ios') {
73
+ // iOS expects 0-1
74
+ return Math.min(1, Math.max(0, quality / 100));
184
75
  }
76
+ return Math.min(100, Math.max(0, quality));
77
+ }, [quality]);
78
+
79
+ const handlePictureTaken = useCallback(
80
+ (event: PictureEvent) => {
81
+ const path = event.croppedImage ?? event.initialImage;
82
+ if (path) {
83
+ onCapture?.({
84
+ path,
85
+ quad: null,
86
+ width: event.width ?? 0,
87
+ height: event.height ?? 0,
88
+ });
89
+ }
90
+
91
+ if (captureResolvers.current) {
92
+ captureResolvers.current.resolve(event);
93
+ captureResolvers.current = null;
94
+ }
95
+ },
96
+ [onCapture],
97
+ );
185
98
 
186
- setQuad([
187
- rectangleCoordinates.topLeft,
188
- rectangleCoordinates.topRight,
189
- rectangleCoordinates.bottomRight,
190
- rectangleCoordinates.bottomLeft,
191
- ]);
192
-
193
- if (autoCapture && stableCounter >= minStableFrames) {
194
- triggerCapture();
99
+ const handleError = useCallback((error: Error) => {
100
+ if (captureResolvers.current) {
101
+ captureResolvers.current.reject(error);
102
+ captureResolvers.current = null;
195
103
  }
196
- },
197
- [autoCapture, minStableFrames],
198
- );
199
-
200
- const handlePictureTaken = useCallback(
201
- (event: NativeSyntheticEvent<PictureEvent>) => {
202
- emitCaptureResult(event.nativeEvent);
203
- },
204
- [emitCaptureResult],
205
- );
104
+ }, []);
206
105
 
207
- const captureNative = useCallback((): Promise<PictureEvent> => {
208
- if (capturingRef.current) {
209
- return Promise.reject(new Error('capture_in_progress'));
210
- }
106
+ const capture = useCallback((): Promise<PictureEvent> => {
107
+ const instance = scannerRef.current;
108
+ if (!instance || typeof instance.capture !== 'function') {
109
+ return Promise.reject(new Error('DocumentScanner native instance is not ready'));
110
+ }
111
+ if (captureResolvers.current) {
112
+ return Promise.reject(new Error('capture_in_progress'));
113
+ }
211
114
 
212
- try {
213
- const handle = ensureViewHandle();
214
- capturingRef.current = true;
215
- return NativeDocScannerModule.capture(handle)
216
- .then((result: PictureEvent) => {
217
- emitCaptureResult(result);
218
- return result;
219
- })
220
- .catch((error: Error) => {
221
- capturingRef.current = false;
222
- throw error;
115
+ const result = instance.capture();
116
+ if (result && typeof result.then === 'function') {
117
+ return result.then((payload: PictureEvent) => {
118
+ handlePictureTaken(payload);
119
+ return payload;
223
120
  });
224
- } catch (error) {
225
- capturingRef.current = false;
226
- return Promise.reject(error);
227
- }
228
- }, [emitCaptureResult, ensureViewHandle]);
229
-
230
- const triggerCapture = useCallback(() => {
231
- if (capturingRef.current) {
232
- return;
233
- }
234
-
235
- captureNative().catch((error: Error) => {
236
- console.warn('[DocScanner] capture failed', error);
237
- });
238
- }, [captureNative]);
239
-
240
- const handleManualCapture = useCallback(() => {
241
- if (autoCapture) {
242
- return;
243
- }
244
- captureNative().catch((error: Error) => {
245
- console.warn('[DocScanner] manual capture failed', error);
246
- });
247
- }, [autoCapture, captureNative]);
121
+ }
248
122
 
249
- useImperativeHandle(
250
- ref,
251
- () => ({
252
- capture: captureNative,
253
- reset: () => {
254
- setStable(0);
255
- resetNativeStability();
256
- },
257
- }),
258
- [captureNative, resetNativeStability],
259
- );
123
+ return new Promise<PictureEvent>((resolve, reject) => {
124
+ captureResolvers.current = { resolve, reject };
125
+ });
126
+ }, [handlePictureTaken]);
260
127
 
261
- return (
262
- <View style={styles.container}>
263
- <NativeDocScanner
264
- ref={viewRef}
265
- style={StyleSheet.absoluteFill}
266
- detectionCountBeforeCapture={minStableFrames}
267
- autoCapture={autoCapture}
268
- enableTorch={enableTorch}
269
- quality={quality}
270
- useBase64={useBase64}
271
- onRectangleDetect={handleRectangleDetect}
272
- onPictureTaken={handlePictureTaken}
273
- />
274
- {!shouldUseNativeOverlay && (
275
- <Overlay
276
- quad={quad}
277
- color={overlayColor}
278
- frameSize={frameSize}
279
- showGrid={showGrid}
280
- gridColor={effectiveGridColor}
281
- gridLineWidth={gridLineWidth}
128
+ const handleManualCapture = useCallback(() => {
129
+ if (autoCapture) {
130
+ return;
131
+ }
132
+ capture().catch((error) => {
133
+ console.warn('[DocScanner] manual capture failed', error);
134
+ });
135
+ }, [autoCapture, capture]);
136
+
137
+ useImperativeHandle(
138
+ ref,
139
+ () => ({
140
+ capture,
141
+ reset: () => {
142
+ if (captureResolvers.current) {
143
+ captureResolvers.current.reject(new Error('reset'));
144
+ captureResolvers.current = null;
145
+ }
146
+ },
147
+ }),
148
+ [capture],
149
+ );
150
+
151
+ return (
152
+ <View style={styles.container}>
153
+ <DocumentScanner
154
+ ref={scannerRef}
155
+ style={StyleSheet.absoluteFillObject}
156
+ detectionCountBeforeCapture={minStableFrames}
157
+ overlayColor={overlayColor}
158
+ enableTorch={enableTorch}
159
+ quality={normalizedQuality}
160
+ useBase64={useBase64}
161
+ manualOnly={!autoCapture}
162
+ onPictureTaken={handlePictureTaken}
163
+ onError={handleError}
282
164
  />
283
- )}
284
- {!autoCapture && (
285
- <TouchableOpacity style={styles.button} onPress={handleManualCapture} />
286
- )}
287
- {children}
288
- </View>
289
- );
290
- });
165
+ {!autoCapture && <TouchableOpacity style={styles.button} onPress={handleManualCapture} />}
166
+ {children}
167
+ </View>
168
+ );
169
+ },
170
+ );
291
171
 
292
172
  const styles = StyleSheet.create({
293
173
  container: {
package/src/external.d.ts CHANGED
@@ -31,18 +31,6 @@ declare module '@shopify/react-native-skia' {
31
31
  export const Path: ComponentType<PathProps>;
32
32
  }
33
33
 
34
- declare module 'react-native-rectangle-doc-scanner/RNRDocScannerModule' {
35
- export type NativeCaptureResult = {
36
- croppedImage?: string | null;
37
- initialImage?: string;
38
- width?: number;
39
- height?: number;
40
- };
41
-
42
- export function capture(viewTag: number): Promise<NativeCaptureResult>;
43
- export function reset(viewTag: number): void;
44
- }
45
-
46
34
  declare module 'react-native-perspective-image-cropper' {
47
35
  import type { ComponentType } from 'react';
48
36
 
@@ -69,3 +57,31 @@ declare module 'react-native-perspective-image-cropper' {
69
57
  const CustomImageCropperDefault: ComponentType<CustomImageCropperProps>;
70
58
  export default CustomImageCropperDefault;
71
59
  }
60
+
61
+ declare module 'react-native-document-scanner' {
62
+ import type { Component } from 'react';
63
+ import type { ViewStyle } from 'react-native';
64
+
65
+ export type DocumentScannerResult = {
66
+ croppedImage?: string | null;
67
+ initialImage?: string | null;
68
+ width?: number;
69
+ height?: number;
70
+ };
71
+
72
+ export interface DocumentScannerProps {
73
+ style?: ViewStyle;
74
+ detectionCountBeforeCapture?: number;
75
+ overlayColor?: string;
76
+ enableTorch?: boolean;
77
+ useBase64?: boolean;
78
+ quality?: number;
79
+ manualOnly?: boolean;
80
+ onPictureTaken?: (event: DocumentScannerResult) => void;
81
+ onError?: (error: Error) => void;
82
+ }
83
+
84
+ export default class DocumentScanner extends Component<DocumentScannerProps> {
85
+ capture(): Promise<DocumentScannerResult>;
86
+ }
87
+ }
@@ -1,55 +0,0 @@
1
- buildscript {
2
- repositories {
3
- google()
4
- mavenCentral()
5
- }
6
- dependencies {
7
- classpath("com.android.tools.build:gradle:8.1.1")
8
- classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10")
9
- }
10
- }
11
-
12
- apply plugin: "com.android.library"
13
- apply plugin: "org.jetbrains.kotlin.android"
14
-
15
- android {
16
- namespace "com.reactnativerectangledocscanner"
17
- compileSdkVersion 34
18
-
19
- defaultConfig {
20
- minSdkVersion 24
21
- targetSdkVersion 34
22
- consumerProguardFiles "consumer-rules.pro"
23
- }
24
-
25
- buildTypes {
26
- release {
27
- minifyEnabled false
28
- proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
29
- }
30
- }
31
-
32
- compileOptions {
33
- sourceCompatibility JavaVersion.VERSION_11
34
- targetCompatibility JavaVersion.VERSION_11
35
- }
36
-
37
- kotlinOptions {
38
- jvmTarget = "11"
39
- }
40
- }
41
-
42
- repositories {
43
- google()
44
- mavenCentral()
45
- }
46
-
47
- dependencies {
48
- implementation "com.facebook.react:react-native:+"
49
- implementation "androidx.camera:camera-core:1.3.1"
50
- implementation "androidx.camera:camera-camera2:1.3.1"
51
- implementation "androidx.camera:camera-lifecycle:1.3.1"
52
- implementation "androidx.camera:camera-view:1.3.1"
53
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
54
- implementation "org.opencv:opencv-android:4.9.0"
55
- }
@@ -1 +0,0 @@
1
- # Consumer ProGuard rules will be added once native implementation is complete.
@@ -1 +0,0 @@
1
- # Keep default empty; add rules when native implementation is added.
@@ -1,37 +0,0 @@
1
- package com.reactnativerectangledocscanner
2
-
3
- import com.facebook.react.bridge.Arguments
4
- import com.facebook.react.bridge.Promise
5
- import com.facebook.react.bridge.ReactApplicationContext
6
- import com.facebook.react.bridge.ReactContextBaseJavaModule
7
- import com.facebook.react.bridge.ReactMethod
8
- import com.facebook.react.bridge.UiThreadUtil
9
- import com.facebook.react.uimanager.UIManagerHelper
10
- import com.facebook.react.uimanager.events.EventDispatcher
11
-
12
- class RNRDocScannerModule(
13
- private val reactContext: ReactApplicationContext,
14
- ) : ReactContextBaseJavaModule(reactContext) {
15
-
16
- override fun getName() = "RNRDocScannerModule"
17
-
18
- @ReactMethod
19
- fun capture(viewTag: Int, promise: Promise) {
20
- UiThreadUtil.runOnUiThread {
21
- val view = UIManagerHelper.getView(reactContext, viewTag) as? RNRDocScannerView
22
- if (view == null) {
23
- promise.reject("view_not_found", "Unable to locate DocScanner view.")
24
- return@runOnUiThread
25
- }
26
- view.capture(promise)
27
- }
28
- }
29
-
30
- @ReactMethod
31
- fun reset(viewTag: Int) {
32
- UiThreadUtil.runOnUiThread {
33
- val view = UIManagerHelper.getView(reactContext, viewTag) as? RNRDocScannerView
34
- view?.reset()
35
- }
36
- }
37
- }
@@ -1,16 +0,0 @@
1
- package com.reactnativerectangledocscanner
2
-
3
- import com.facebook.react.bridge.ReactApplicationContext
4
- import com.facebook.react.bridge.ReactPackage
5
- import com.facebook.react.bridge.NativeModule
6
- import com.facebook.react.uimanager.ViewManager
7
-
8
- class RNRDocScannerPackage : ReactPackage {
9
- override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
10
- return listOf(RNRDocScannerModule(reactContext))
11
- }
12
-
13
- override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
14
- return listOf(RNRDocScannerViewManager(reactContext))
15
- }
16
- }