react-native-rectangle-doc-scanner 0.65.0 → 0.69.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 +97 -168
- package/android/build.gradle +55 -0
- package/android/consumer-rules.pro +1 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +11 -0
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerModule.kt +37 -0
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerPackage.kt +16 -0
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerView.kt +129 -0
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerViewManager.kt +50 -0
- package/dist/DocScanner.d.ts +12 -7
- package/dist/DocScanner.js +97 -42
- package/dist/FullDocScanner.d.ts +3 -0
- package/dist/FullDocScanner.js +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/utils/overlay.js +77 -48
- package/docs/native-module-architecture.md +178 -0
- package/ios/RNRDocScannerModule.swift +49 -0
- package/ios/RNRDocScannerView.swift +228 -0
- package/ios/RNRDocScannerViewManager.m +21 -0
- package/ios/RNRDocScannerViewManager.swift +47 -0
- package/package.json +6 -5
- package/react-native-rectangle-doc-scanner.podspec +22 -0
- package/src/DocScanner.tsx +153 -76
- package/src/FullDocScanner.tsx +10 -0
- package/src/external.d.ts +12 -45
- package/src/index.ts +1 -1
- package/src/utils/overlay.tsx +83 -54
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import Foundation
|
|
3
|
+
import React
|
|
4
|
+
import Vision
|
|
5
|
+
|
|
6
|
+
@objc(RNRDocScannerView)
|
|
7
|
+
class RNRDocScannerView: UIView {
|
|
8
|
+
@objc var detectionCountBeforeCapture: NSNumber = 8
|
|
9
|
+
@objc var autoCapture: Bool = true
|
|
10
|
+
@objc var enableTorch: Bool = false {
|
|
11
|
+
didSet {
|
|
12
|
+
updateTorchMode()
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
@objc var quality: NSNumber = 90
|
|
16
|
+
@objc var useBase64: Bool = false
|
|
17
|
+
|
|
18
|
+
@objc var onRectangleDetect: RCTDirectEventBlock?
|
|
19
|
+
@objc var onPictureTaken: RCTDirectEventBlock?
|
|
20
|
+
|
|
21
|
+
private let session = AVCaptureSession()
|
|
22
|
+
private let sessionQueue = DispatchQueue(label: "com.reactnative.rectangledocscanner.session")
|
|
23
|
+
private let analysisQueue = DispatchQueue(label: "com.reactnative.rectangledocscanner.analysis")
|
|
24
|
+
private var previewLayer: AVCaptureVideoPreviewLayer?
|
|
25
|
+
private var photoOutput = AVCapturePhotoOutput()
|
|
26
|
+
|
|
27
|
+
private var currentStableCounter: Int = 0
|
|
28
|
+
private var isCaptureInFlight = false
|
|
29
|
+
|
|
30
|
+
override init(frame: CGRect) {
|
|
31
|
+
super.init(frame: frame)
|
|
32
|
+
commonInit()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
required init?(coder: NSCoder) {
|
|
36
|
+
super.init(coder: coder)
|
|
37
|
+
commonInit()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private func commonInit() {
|
|
41
|
+
backgroundColor = .black
|
|
42
|
+
configurePreviewLayer()
|
|
43
|
+
configureSession()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private func configurePreviewLayer() {
|
|
47
|
+
let layer = AVCaptureVideoPreviewLayer(session: session)
|
|
48
|
+
layer.videoGravity = .resizeAspectFill
|
|
49
|
+
self.layer.insertSublayer(layer, at: 0)
|
|
50
|
+
previewLayer = layer
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private func configureSession() {
|
|
54
|
+
sessionQueue.async { [weak self] in
|
|
55
|
+
guard let self else { return }
|
|
56
|
+
|
|
57
|
+
session.beginConfiguration()
|
|
58
|
+
session.sessionPreset = .photo
|
|
59
|
+
|
|
60
|
+
defer {
|
|
61
|
+
session.commitConfiguration()
|
|
62
|
+
if !session.isRunning {
|
|
63
|
+
session.startRunning()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
guard
|
|
68
|
+
let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
|
|
69
|
+
let videoInput = try? AVCaptureDeviceInput(device: videoDevice),
|
|
70
|
+
session.canAddInput(videoInput)
|
|
71
|
+
else {
|
|
72
|
+
NSLog("[RNRDocScanner] Unable to create AVCaptureDeviceInput")
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
session.addInput(videoInput)
|
|
77
|
+
|
|
78
|
+
if session.canAddOutput(photoOutput) {
|
|
79
|
+
session.addOutput(photoOutput)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// TODO: Wire up AVCaptureVideoDataOutput + rectangle detection pipeline.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override func layoutSubviews() {
|
|
87
|
+
super.layoutSubviews()
|
|
88
|
+
previewLayer?.frame = bounds
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private func updateTorchMode() {
|
|
92
|
+
sessionQueue.async { [weak self] in
|
|
93
|
+
guard
|
|
94
|
+
let self,
|
|
95
|
+
let device = self.videoDevice(for: .back),
|
|
96
|
+
device.hasTorch
|
|
97
|
+
else {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
do {
|
|
102
|
+
try device.lockForConfiguration()
|
|
103
|
+
device.torchMode = self.enableTorch ? .on : .off
|
|
104
|
+
device.unlockForConfiguration()
|
|
105
|
+
} catch {
|
|
106
|
+
NSLog("[RNRDocScanner] Failed to update torch mode: \(error)")
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private func videoDevice(for position: AVCaptureDevice.Position) -> AVCaptureDevice? {
|
|
112
|
+
if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: position) {
|
|
113
|
+
return device
|
|
114
|
+
}
|
|
115
|
+
return AVCaptureDevice.devices(for: .video).first(where: { $0.position == position })
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func handleDetectedRectangle(_ rectangle: VNRectangleObservation?, frameSize: CGSize) {
|
|
119
|
+
guard let onRectangleDetect else { return }
|
|
120
|
+
|
|
121
|
+
let payload: [String: Any?]
|
|
122
|
+
if let rectangle {
|
|
123
|
+
let points = [
|
|
124
|
+
point(from: rectangle.topLeft, frameSize: frameSize),
|
|
125
|
+
point(from: rectangle.topRight, frameSize: frameSize),
|
|
126
|
+
point(from: rectangle.bottomRight, frameSize: frameSize),
|
|
127
|
+
point(from: rectangle.bottomLeft, frameSize: frameSize),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
currentStableCounter = min(currentStableCounter + 1, Int(truncating: detectionCountBeforeCapture))
|
|
131
|
+
payload = [
|
|
132
|
+
"rectangleCoordinates": [
|
|
133
|
+
"topLeft": ["x": points[0].x, "y": points[0].y],
|
|
134
|
+
"topRight": ["x": points[1].x, "y": points[1].y],
|
|
135
|
+
"bottomRight": ["x": points[2].x, "y": points[2].y],
|
|
136
|
+
"bottomLeft": ["x": points[3].x, "y": points[3].y],
|
|
137
|
+
],
|
|
138
|
+
"stableCounter": currentStableCounter,
|
|
139
|
+
"frameWidth": frameSize.width,
|
|
140
|
+
"frameHeight": frameSize.height,
|
|
141
|
+
]
|
|
142
|
+
} else {
|
|
143
|
+
currentStableCounter = 0
|
|
144
|
+
payload = [
|
|
145
|
+
"rectangleCoordinates": NSNull(),
|
|
146
|
+
"stableCounter": currentStableCounter,
|
|
147
|
+
"frameWidth": frameSize.width,
|
|
148
|
+
"frameHeight": frameSize.height,
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
DispatchQueue.main.async {
|
|
153
|
+
onRectangleDetect(payload.compactMapValues { $0 })
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private func point(from normalizedPoint: CGPoint, frameSize: CGSize) -> CGPoint {
|
|
158
|
+
CGPoint(x: normalizedPoint.x * frameSize.width, y: (1 - normalizedPoint.y) * frameSize.height)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func capture(completion: @escaping (Result<RNRDocScannerCaptureResult, Error>) -> Void) {
|
|
162
|
+
sessionQueue.async { [weak self] in
|
|
163
|
+
guard let self else { return }
|
|
164
|
+
|
|
165
|
+
if isCaptureInFlight {
|
|
166
|
+
completion(.failure(RNRDocScannerError.captureInProgress))
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
guard photoOutput.connections.isEmpty == false else {
|
|
171
|
+
completion(.failure(RNRDocScannerError.captureUnavailable))
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
isCaptureInFlight = true
|
|
176
|
+
|
|
177
|
+
// TODO: Implement real capture logic; emit stub callback for now.
|
|
178
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
179
|
+
self.isCaptureInFlight = false
|
|
180
|
+
completion(.failure(RNRDocScannerError.notImplemented))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func resetStability() {
|
|
186
|
+
currentStableCounter = 0
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
struct RNRDocScannerCaptureResult {
|
|
191
|
+
let croppedImage: String?
|
|
192
|
+
let originalImage: String
|
|
193
|
+
let width: CGFloat
|
|
194
|
+
let height: CGFloat
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
enum RNRDocScannerError: Error {
|
|
198
|
+
case captureInProgress
|
|
199
|
+
case captureUnavailable
|
|
200
|
+
case notImplemented
|
|
201
|
+
case viewNotFound
|
|
202
|
+
|
|
203
|
+
var code: String {
|
|
204
|
+
switch self {
|
|
205
|
+
case .captureInProgress:
|
|
206
|
+
return "capture_in_progress"
|
|
207
|
+
case .captureUnavailable:
|
|
208
|
+
return "capture_unavailable"
|
|
209
|
+
case .notImplemented:
|
|
210
|
+
return "not_implemented"
|
|
211
|
+
case .viewNotFound:
|
|
212
|
+
return "view_not_found"
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
var message: String {
|
|
217
|
+
switch self {
|
|
218
|
+
case .captureInProgress:
|
|
219
|
+
return "A capture request is already in flight."
|
|
220
|
+
case .captureUnavailable:
|
|
221
|
+
return "Photo output is not configured yet."
|
|
222
|
+
case .notImplemented:
|
|
223
|
+
return "Native capture is not implemented yet."
|
|
224
|
+
case .viewNotFound:
|
|
225
|
+
return "Unable to locate the native DocScanner view."
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#import <React/RCTBridge.h>
|
|
2
|
+
#import <React/RCTUIManager.h>
|
|
3
|
+
#import <React/RCTViewManager.h>
|
|
4
|
+
|
|
5
|
+
#import "react-native-rectangle-doc-scanner-Swift.h"
|
|
6
|
+
|
|
7
|
+
@interface RCT_EXTERN_MODULE(RNRDocScannerViewManager, RCTViewManager)
|
|
8
|
+
RCT_EXPORT_VIEW_PROPERTY(detectionCountBeforeCapture, NSNumber)
|
|
9
|
+
RCT_EXPORT_VIEW_PROPERTY(autoCapture, BOOL)
|
|
10
|
+
RCT_EXPORT_VIEW_PROPERTY(enableTorch, BOOL)
|
|
11
|
+
RCT_EXPORT_VIEW_PROPERTY(quality, NSNumber)
|
|
12
|
+
RCT_EXPORT_VIEW_PROPERTY(useBase64, BOOL)
|
|
13
|
+
RCT_EXPORT_VIEW_PROPERTY(onRectangleDetect, RCTDirectEventBlock)
|
|
14
|
+
RCT_EXPORT_VIEW_PROPERTY(onPictureTaken, RCTDirectEventBlock)
|
|
15
|
+
|
|
16
|
+
RCT_EXTERN_METHOD(capture:(nonnull NSNumber *)reactTag
|
|
17
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
18
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
19
|
+
|
|
20
|
+
RCT_EXTERN_METHOD(reset:(nonnull NSNumber *)reactTag)
|
|
21
|
+
@end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
3
|
+
|
|
4
|
+
@objc(RNRDocScannerViewManager)
|
|
5
|
+
class RNRDocScannerViewManager: RCTViewManager {
|
|
6
|
+
override static func requiresMainQueueSetup() -> Bool {
|
|
7
|
+
true
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
override func view() -> UIView! {
|
|
11
|
+
RNRDocScannerView()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@objc func capture(_ reactTag: NSNumber, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
15
|
+
bridge.uiManager.addUIBlock { _, viewRegistry in
|
|
16
|
+
guard let view = viewRegistry?[reactTag] as? RNRDocScannerView else {
|
|
17
|
+
reject(RNRDocScannerError.viewNotFound.code, RNRDocScannerError.viewNotFound.message, nil)
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
view.capture { result in
|
|
22
|
+
switch result {
|
|
23
|
+
case let .success(payload):
|
|
24
|
+
resolve([
|
|
25
|
+
"croppedImage": payload.croppedImage as Any,
|
|
26
|
+
"initialImage": payload.originalImage,
|
|
27
|
+
"width": payload.width,
|
|
28
|
+
"height": payload.height,
|
|
29
|
+
])
|
|
30
|
+
case let .failure(error as RNRDocScannerError):
|
|
31
|
+
reject(error.code, error.message, error)
|
|
32
|
+
case let .failure(error):
|
|
33
|
+
reject("capture_failed", error.localizedDescription, error)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@objc func reset(_ reactTag: NSNumber) {
|
|
40
|
+
bridge.uiManager.addUIBlock { _, viewRegistry in
|
|
41
|
+
guard let view = viewRegistry?[reactTag] as? RNRDocScannerView else {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
view.resetStability()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-rectangle-doc-scanner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.69.0",
|
|
4
|
+
"description": "Native-backed document scanner for React Native with customizable overlays.",
|
|
5
|
+
"license": "MIT",
|
|
4
6
|
"main": "dist/index.js",
|
|
5
7
|
"types": "dist/index.d.ts",
|
|
6
8
|
"repository": {
|
|
@@ -18,14 +20,13 @@
|
|
|
18
20
|
"peerDependencies": {
|
|
19
21
|
"@shopify/react-native-skia": "*",
|
|
20
22
|
"react": "*",
|
|
21
|
-
"react-native": "*"
|
|
23
|
+
"react-native": "*",
|
|
24
|
+
"react-native-perspective-image-cropper": "*"
|
|
22
25
|
},
|
|
23
26
|
"devDependencies": {
|
|
24
27
|
"@types/react": "^18.2.41",
|
|
25
28
|
"@types/react-native": "0.73.0",
|
|
26
29
|
"typescript": "^5.3.3"
|
|
27
30
|
},
|
|
28
|
-
"dependencies": {
|
|
29
|
-
"react-native-document-scanner-plugin": "^1.6.3"
|
|
30
|
-
}
|
|
31
|
+
"dependencies": {}
|
|
31
32
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'react-native-rectangle-doc-scanner'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package.fetch('description', 'Document scanner with native camera overlay support for React Native.')
|
|
9
|
+
s.homepage = package['homepage'] || 'https://github.com/danchew90/react-native-rectangle-doc-scanner'
|
|
10
|
+
s.license = package['license'] || { :type => 'MIT' }
|
|
11
|
+
s.author = package['author'] || { 'react-native-rectangle-doc-scanner' => 'opensource@example.com' }
|
|
12
|
+
s.source = { :git => package.dig('repository', 'url') || s.homepage, :tag => "v#{s.version}" }
|
|
13
|
+
|
|
14
|
+
s.platform = :ios, '13.0'
|
|
15
|
+
s.swift_version = '5.0'
|
|
16
|
+
|
|
17
|
+
s.source_files = 'ios/**/*.{h,m,mm,swift}'
|
|
18
|
+
s.public_header_files = 'ios/**/*.h'
|
|
19
|
+
s.requires_arc = true
|
|
20
|
+
|
|
21
|
+
s.dependency 'React-Core'
|
|
22
|
+
end
|
package/src/DocScanner.tsx
CHANGED
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
import React, {
|
|
2
|
-
ComponentType,
|
|
3
2
|
ReactNode,
|
|
3
|
+
forwardRef,
|
|
4
4
|
useCallback,
|
|
5
|
+
useImperativeHandle,
|
|
5
6
|
useMemo,
|
|
6
7
|
useRef,
|
|
7
8
|
useState,
|
|
8
9
|
} from 'react';
|
|
9
10
|
import {
|
|
10
|
-
|
|
11
|
+
findNodeHandle,
|
|
12
|
+
NativeModules,
|
|
13
|
+
requireNativeComponent,
|
|
11
14
|
StyleSheet,
|
|
12
15
|
TouchableOpacity,
|
|
13
16
|
View,
|
|
14
17
|
} from 'react-native';
|
|
15
|
-
import
|
|
18
|
+
import type { NativeSyntheticEvent } from 'react-native';
|
|
16
19
|
import { Overlay } from './utils/overlay';
|
|
17
20
|
import type { Point } from './types';
|
|
18
21
|
|
|
22
|
+
const MODULE_NAME = 'RNRDocScannerModule';
|
|
23
|
+
const VIEW_NAME = 'RNRDocScannerView';
|
|
24
|
+
|
|
25
|
+
const NativeDocScannerModule = NativeModules[MODULE_NAME];
|
|
26
|
+
|
|
27
|
+
if (!NativeDocScannerModule) {
|
|
28
|
+
const fallbackMessage =
|
|
29
|
+
`The native module '${MODULE_NAME}' is not linked. Make sure you have run pod install, ` +
|
|
30
|
+
`synced Gradle, and rebuilt the app after installing 'react-native-rectangle-doc-scanner'.`;
|
|
31
|
+
throw new Error(fallbackMessage);
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
type NativeRectangle = {
|
|
20
35
|
topLeft: Point;
|
|
21
36
|
topRight: Point;
|
|
@@ -23,43 +38,39 @@ type NativeRectangle = {
|
|
|
23
38
|
bottomLeft: Point;
|
|
24
39
|
};
|
|
25
40
|
|
|
26
|
-
type
|
|
27
|
-
rectangleCoordinates
|
|
28
|
-
stableCounter
|
|
41
|
+
type RectangleEvent = {
|
|
42
|
+
rectangleCoordinates: NativeRectangle | null;
|
|
43
|
+
stableCounter: number;
|
|
44
|
+
frameWidth: number;
|
|
45
|
+
frameHeight: number;
|
|
29
46
|
};
|
|
30
47
|
|
|
31
|
-
type
|
|
32
|
-
croppedImage?: string;
|
|
48
|
+
type PictureEvent = {
|
|
49
|
+
croppedImage?: string | null;
|
|
33
50
|
initialImage?: string;
|
|
34
51
|
width?: number;
|
|
35
52
|
height?: number;
|
|
36
53
|
};
|
|
37
54
|
|
|
38
|
-
type
|
|
39
|
-
capture: () => Promise<NativeCaptureResult>;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
type NativeDocumentScannerProps = {
|
|
55
|
+
type NativeDocScannerProps = {
|
|
43
56
|
style?: object;
|
|
44
|
-
overlayColor?: string;
|
|
45
57
|
detectionCountBeforeCapture?: number;
|
|
58
|
+
autoCapture?: boolean;
|
|
46
59
|
enableTorch?: boolean;
|
|
47
|
-
hideControls?: boolean;
|
|
48
|
-
useBase64?: boolean;
|
|
49
60
|
quality?: number;
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
useBase64?: boolean;
|
|
62
|
+
onRectangleDetect?: (event: NativeSyntheticEvent<RectangleEvent>) => void;
|
|
63
|
+
onPictureTaken?: (event: NativeSyntheticEvent<PictureEvent>) => void;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type DocScannerHandle = {
|
|
67
|
+
capture: () => Promise<PictureEvent>;
|
|
68
|
+
reset: () => void;
|
|
52
69
|
};
|
|
53
70
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
>;
|
|
71
|
+
const NativeDocScanner = requireNativeComponent<NativeDocScannerProps>(VIEW_NAME);
|
|
72
|
+
type NativeDocScannerInstance = React.ElementRef<typeof NativeDocScanner>;
|
|
57
73
|
|
|
58
|
-
/**
|
|
59
|
-
* Detection configuration is no longer used now that the native
|
|
60
|
-
* implementation handles edge detection. Keeping it for backwards
|
|
61
|
-
* compatibility with existing consumer code.
|
|
62
|
-
*/
|
|
63
74
|
export interface DetectionConfig {
|
|
64
75
|
processingWidth?: number;
|
|
65
76
|
cannyLowThreshold?: number;
|
|
@@ -87,22 +98,23 @@ interface Props {
|
|
|
87
98
|
const DEFAULT_OVERLAY_COLOR = '#e7a649';
|
|
88
99
|
const GRID_COLOR_FALLBACK = 'rgba(231, 166, 73, 0.35)';
|
|
89
100
|
|
|
90
|
-
export const DocScanner
|
|
101
|
+
export const DocScanner = forwardRef<DocScannerHandle, Props>(({
|
|
91
102
|
onCapture,
|
|
92
103
|
overlayColor = DEFAULT_OVERLAY_COLOR,
|
|
93
104
|
autoCapture = true,
|
|
94
105
|
minStableFrames = 8,
|
|
95
106
|
enableTorch = false,
|
|
96
|
-
quality,
|
|
107
|
+
quality = 90,
|
|
97
108
|
useBase64 = false,
|
|
98
109
|
children,
|
|
99
110
|
showGrid = true,
|
|
100
111
|
gridColor,
|
|
101
112
|
gridLineWidth = 2,
|
|
102
|
-
}) => {
|
|
103
|
-
const
|
|
113
|
+
}, ref) => {
|
|
114
|
+
const viewRef = useRef<NativeDocScannerInstance | null>(null);
|
|
104
115
|
const capturingRef = useRef(false);
|
|
105
116
|
const [quad, setQuad] = useState<Point[] | null>(null);
|
|
117
|
+
const [stable, setStable] = useState(0);
|
|
106
118
|
const [frameSize, setFrameSize] = useState<{ width: number; height: number } | null>(null);
|
|
107
119
|
|
|
108
120
|
const effectiveGridColor = useMemo(
|
|
@@ -110,80 +122,142 @@ export const DocScanner: React.FC<Props> = ({
|
|
|
110
122
|
[gridColor],
|
|
111
123
|
);
|
|
112
124
|
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
116
|
-
|
|
125
|
+
const ensureViewHandle = useCallback(() => {
|
|
126
|
+
const nodeHandle = findNodeHandle(viewRef.current);
|
|
127
|
+
if (!nodeHandle) {
|
|
128
|
+
throw new Error('Unable to obtain native view handle for DocScanner.');
|
|
117
129
|
}
|
|
130
|
+
return nodeHandle;
|
|
118
131
|
}, []);
|
|
119
132
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
const resetNativeStability = useCallback(() => {
|
|
134
|
+
try {
|
|
135
|
+
const handle = ensureViewHandle();
|
|
136
|
+
NativeDocScannerModule.reset(handle);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.warn('[DocScanner] unable to reset native stability', error);
|
|
126
139
|
}
|
|
140
|
+
}, [ensureViewHandle]);
|
|
127
141
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
coordinates.topRight,
|
|
131
|
-
coordinates.bottomRight,
|
|
132
|
-
coordinates.bottomLeft,
|
|
133
|
-
];
|
|
134
|
-
|
|
135
|
-
setQuad(nextQuad);
|
|
136
|
-
}, []);
|
|
137
|
-
|
|
138
|
-
const handlePictureTaken = useCallback(
|
|
139
|
-
(event: NativeCaptureResult) => {
|
|
142
|
+
const emitCaptureResult = useCallback(
|
|
143
|
+
(payload: PictureEvent) => {
|
|
140
144
|
capturingRef.current = false;
|
|
141
145
|
|
|
142
|
-
const path =
|
|
146
|
+
const path = payload.croppedImage ?? payload.initialImage;
|
|
143
147
|
if (!path) {
|
|
144
148
|
return;
|
|
145
149
|
}
|
|
146
150
|
|
|
147
|
-
const width =
|
|
148
|
-
const height =
|
|
149
|
-
|
|
151
|
+
const width = payload.width ?? frameSize?.width ?? 0;
|
|
152
|
+
const height = payload.height ?? frameSize?.height ?? 0;
|
|
150
153
|
onCapture?.({
|
|
151
154
|
path,
|
|
152
155
|
quad,
|
|
153
156
|
width,
|
|
154
157
|
height,
|
|
155
158
|
});
|
|
159
|
+
setStable(0);
|
|
160
|
+
resetNativeStability();
|
|
161
|
+
},
|
|
162
|
+
[frameSize, onCapture, quad, resetNativeStability],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const handleRectangleDetect = useCallback(
|
|
166
|
+
(event: NativeSyntheticEvent<RectangleEvent>) => {
|
|
167
|
+
const { rectangleCoordinates, stableCounter, frameWidth, frameHeight } = event.nativeEvent;
|
|
168
|
+
setStable(stableCounter);
|
|
169
|
+
setFrameSize({ width: frameWidth, height: frameHeight });
|
|
170
|
+
|
|
171
|
+
if (!rectangleCoordinates) {
|
|
172
|
+
setQuad(null);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setQuad([
|
|
177
|
+
rectangleCoordinates.topLeft,
|
|
178
|
+
rectangleCoordinates.topRight,
|
|
179
|
+
rectangleCoordinates.bottomRight,
|
|
180
|
+
rectangleCoordinates.bottomLeft,
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
if (autoCapture && stableCounter >= minStableFrames) {
|
|
184
|
+
triggerCapture();
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
[autoCapture, minStableFrames],
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const handlePictureTaken = useCallback(
|
|
191
|
+
(event: NativeSyntheticEvent<PictureEvent>) => {
|
|
192
|
+
emitCaptureResult(event.nativeEvent);
|
|
156
193
|
},
|
|
157
|
-
[
|
|
194
|
+
[emitCaptureResult],
|
|
158
195
|
);
|
|
159
196
|
|
|
197
|
+
const captureNative = useCallback((): Promise<PictureEvent> => {
|
|
198
|
+
if (capturingRef.current) {
|
|
199
|
+
return Promise.reject(new Error('capture_in_progress'));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const handle = ensureViewHandle();
|
|
204
|
+
capturingRef.current = true;
|
|
205
|
+
return NativeDocScannerModule.capture(handle)
|
|
206
|
+
.then((result: PictureEvent) => {
|
|
207
|
+
emitCaptureResult(result);
|
|
208
|
+
return result;
|
|
209
|
+
})
|
|
210
|
+
.catch((error: Error) => {
|
|
211
|
+
capturingRef.current = false;
|
|
212
|
+
throw error;
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
capturingRef.current = false;
|
|
216
|
+
return Promise.reject(error);
|
|
217
|
+
}
|
|
218
|
+
}, [emitCaptureResult, ensureViewHandle]);
|
|
219
|
+
|
|
220
|
+
const triggerCapture = useCallback(() => {
|
|
221
|
+
if (capturingRef.current) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
captureNative().catch((error: Error) => {
|
|
226
|
+
console.warn('[DocScanner] capture failed', error);
|
|
227
|
+
});
|
|
228
|
+
}, [captureNative]);
|
|
229
|
+
|
|
160
230
|
const handleManualCapture = useCallback(() => {
|
|
161
|
-
if (autoCapture
|
|
231
|
+
if (autoCapture) {
|
|
162
232
|
return;
|
|
163
233
|
}
|
|
234
|
+
captureNative().catch((error: Error) => {
|
|
235
|
+
console.warn('[DocScanner] manual capture failed', error);
|
|
236
|
+
});
|
|
237
|
+
}, [autoCapture, captureNative]);
|
|
164
238
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
239
|
+
useImperativeHandle(
|
|
240
|
+
ref,
|
|
241
|
+
() => ({
|
|
242
|
+
capture: captureNative,
|
|
243
|
+
reset: () => {
|
|
244
|
+
setStable(0);
|
|
245
|
+
resetNativeStability();
|
|
246
|
+
},
|
|
247
|
+
}),
|
|
248
|
+
[captureNative, resetNativeStability],
|
|
249
|
+
);
|
|
173
250
|
|
|
174
251
|
return (
|
|
175
|
-
<View style={styles.container}
|
|
176
|
-
<
|
|
177
|
-
ref={
|
|
178
|
-
scannerRef.current = instance as DocumentScannerHandle | null;
|
|
179
|
-
}}
|
|
252
|
+
<View style={styles.container}>
|
|
253
|
+
<NativeDocScanner
|
|
254
|
+
ref={viewRef}
|
|
180
255
|
style={StyleSheet.absoluteFill}
|
|
181
|
-
|
|
182
|
-
|
|
256
|
+
detectionCountBeforeCapture={minStableFrames}
|
|
257
|
+
autoCapture={autoCapture}
|
|
183
258
|
enableTorch={enableTorch}
|
|
184
|
-
hideControls
|
|
185
|
-
useBase64={useBase64}
|
|
186
259
|
quality={quality}
|
|
260
|
+
useBase64={useBase64}
|
|
187
261
|
onRectangleDetect={handleRectangleDetect}
|
|
188
262
|
onPictureTaken={handlePictureTaken}
|
|
189
263
|
/>
|
|
@@ -201,11 +275,12 @@ export const DocScanner: React.FC<Props> = ({
|
|
|
201
275
|
{children}
|
|
202
276
|
</View>
|
|
203
277
|
);
|
|
204
|
-
};
|
|
278
|
+
});
|
|
205
279
|
|
|
206
280
|
const styles = StyleSheet.create({
|
|
207
281
|
container: {
|
|
208
282
|
flex: 1,
|
|
283
|
+
backgroundColor: '#000',
|
|
209
284
|
},
|
|
210
285
|
button: {
|
|
211
286
|
position: 'absolute',
|
|
@@ -217,3 +292,5 @@ const styles = StyleSheet.create({
|
|
|
217
292
|
backgroundColor: '#fff',
|
|
218
293
|
},
|
|
219
294
|
});
|
|
295
|
+
|
|
296
|
+
export type { DocScannerHandle };
|