react-native-rectangle-doc-scanner 1.0.0 → 1.2.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.
@@ -74,6 +74,8 @@ class RNRDocScannerView @JvmOverloads constructor(
74
74
  private var currentStableCounter: Int = 0
75
75
  private var lastQuad: QuadPoints? = null
76
76
  private var lastFrameSize: AndroidSize? = null
77
+ private var missedDetections: Int = 0
78
+ private val maxMissedDetections = 4
77
79
  private var capturePromise: Promise? = null
78
80
  private var captureInFlight: Boolean = false
79
81
 
@@ -199,25 +201,43 @@ class RNRDocScannerView @JvmOverloads constructor(
199
201
 
200
202
  private fun emitDetectionResult(quad: QuadPoints?, frameSize: AndroidSize) {
201
203
  val reactContext = context as? ReactContext ?: return
204
+
205
+ val effectiveQuad: QuadPoints? = when {
206
+ quad != null -> {
207
+ missedDetections = 0
208
+ lastQuad = quad
209
+ quad
210
+ }
211
+ lastQuad != null && missedDetections < maxMissedDetections -> {
212
+ missedDetections += 1
213
+ lastQuad
214
+ }
215
+ else -> {
216
+ missedDetections = 0
217
+ lastQuad = null
218
+ null
219
+ }
220
+ }
221
+
222
+ val sizeForEvent = if (effectiveQuad === quad) frameSize else lastFrameSize ?: frameSize
223
+
202
224
  val event: WritableMap = Arguments.createMap().apply {
203
- if (quad != null) {
225
+ if (effectiveQuad != null) {
204
226
  val quadMap = Arguments.createMap().apply {
205
- putMap("topLeft", quad.topLeft.toWritable())
206
- putMap("topRight", quad.topRight.toWritable())
207
- putMap("bottomRight", quad.bottomRight.toWritable())
208
- putMap("bottomLeft", quad.bottomLeft.toWritable())
227
+ putMap("topLeft", effectiveQuad.topLeft.toWritable())
228
+ putMap("topRight", effectiveQuad.topRight.toWritable())
229
+ putMap("bottomRight", effectiveQuad.bottomRight.toWritable())
230
+ putMap("bottomLeft", effectiveQuad.bottomLeft.toWritable())
209
231
  }
210
232
  putMap("rectangleCoordinates", quadMap)
211
233
  currentStableCounter = (currentStableCounter + 1).coerceAtMost(detectionCountBeforeCapture)
212
- lastQuad = quad
213
234
  } else {
214
235
  putNull("rectangleCoordinates")
215
236
  currentStableCounter = 0
216
- lastQuad = null
217
237
  }
218
238
  putInt("stableCounter", currentStableCounter)
219
- putDouble("frameWidth", frameSize.width.toDouble())
220
- putDouble("frameHeight", frameSize.height.toDouble())
239
+ putDouble("frameWidth", sizeForEvent.width.toDouble())
240
+ putDouble("frameHeight", sizeForEvent.height.toDouble())
221
241
  }
222
242
 
223
243
  reactContext
@@ -62,6 +62,23 @@ const buildPath = (points) => {
62
62
  path.close();
63
63
  return path;
64
64
  };
65
+ const orderQuad = (points) => {
66
+ if (points.length !== 4) {
67
+ return points;
68
+ }
69
+ const sum = (p) => p.x + p.y;
70
+ const diff = (p) => p.x - p.y;
71
+ const topLeft = points.reduce((prev, curr) => (sum(curr) < sum(prev) ? curr : prev));
72
+ const bottomRight = points.reduce((prev, curr) => (sum(curr) > sum(prev) ? curr : prev));
73
+ const remaining = points.filter((p) => p !== topLeft && p !== bottomRight);
74
+ if (remaining.length !== 2) {
75
+ return [topLeft, bottomRight, ...remaining];
76
+ }
77
+ const [candidate1, candidate2] = remaining;
78
+ const topRight = diff(candidate1) > diff(candidate2) ? candidate1 : candidate2;
79
+ const bottomLeft = topRight === candidate1 ? candidate2 : candidate1;
80
+ return [topLeft, topRight, bottomRight, bottomLeft];
81
+ };
65
82
  const Overlay = ({ quad, color = '#e7a649', frameSize, showGrid = true, gridColor = 'rgba(231, 166, 73, 0.35)', gridLineWidth = 2, }) => {
66
83
  const { width: screenWidth, height: screenHeight } = (0, react_native_1.useWindowDimensions)();
67
84
  const fillColor = (0, react_1.useMemo)(() => withAlpha(color, 0.2), [color]);
@@ -106,10 +123,11 @@ const Overlay = ({ quad, color = '#e7a649', frameSize, showGrid = true, gridColo
106
123
  if (!transformedQuad) {
107
124
  return { outlinePath: null, gridPaths: [] };
108
125
  }
109
- const skPath = buildPath(transformedQuad);
126
+ const normalizedQuad = orderQuad(transformedQuad);
127
+ const skPath = buildPath(normalizedQuad);
110
128
  const grid = [];
111
129
  if (showGrid) {
112
- const [topLeft, topRight, bottomRight, bottomLeft] = transformedQuad;
130
+ const [topLeft, topRight, bottomRight, bottomLeft] = normalizedQuad;
113
131
  const steps = [1 / 3, 2 / 3];
114
132
  steps.forEach((t) => {
115
133
  const start = lerp(topLeft, topRight, t);
@@ -33,6 +33,8 @@ class RNRDocScannerView: UIView, AVCaptureVideoDataOutputSampleBufferDelegate, A
33
33
  private var isProcessingFrame = false
34
34
  private var isCaptureInFlight = false
35
35
  private var lastObservation: VNRectangleObservation?
36
+ private var missedDetectionFrames: Int = 0
37
+ private let maxMissedDetections = 4
36
38
  private var lastFrameSize: CGSize = .zero
37
39
  private var photoCaptureCompletion: ((Result<RNRDocScannerCaptureResult, Error>) -> Void)?
38
40
 
@@ -173,15 +175,18 @@ class RNRDocScannerView: UIView, AVCaptureVideoDataOutputSampleBufferDelegate, A
173
175
  return
174
176
  }
175
177
 
176
- guard let observation = (request.results as? [VNRectangleObservation])?.first else {
177
- self.lastObservation = nil
178
- self.handleDetectedRectangle(nil, frameSize: frameSize)
179
- return
180
- }
178
+ guard let observations = request.results as? [VNRectangleObservation], let observation = observations.max(by: { lhs, rhs in
179
+ lhs.boundingBox.width * lhs.boundingBox.height < rhs.boundingBox.width * rhs.boundingBox.height
180
+ }) else {
181
+ self.lastObservation = nil
182
+ self.handleDetectedRectangle(nil, frameSize: frameSize)
183
+ return
184
+ }
181
185
 
182
- self.lastObservation = observation
183
- self.handleDetectedRectangle(observation, frameSize: frameSize)
184
- }
186
+ self.lastObservation = observation
187
+ self.missedDetectionFrames = 0
188
+ self.handleDetectedRectangle(observation, frameSize: frameSize)
189
+ }
185
190
 
186
191
  request.maximumObservations = 1
187
192
  request.minimumConfidence = 0.5
@@ -205,13 +210,27 @@ class RNRDocScannerView: UIView, AVCaptureVideoDataOutputSampleBufferDelegate, A
205
210
  func handleDetectedRectangle(_ rectangle: VNRectangleObservation?, frameSize: CGSize) {
206
211
  guard let onRectangleDetect else { return }
207
212
 
213
+ let effectiveObservation: VNRectangleObservation?
214
+ if let rect = rectangle {
215
+ effectiveObservation = rect
216
+ lastObservation = rect
217
+ missedDetectionFrames = 0
218
+ } else if missedDetectionFrames < maxMissedDetections, let cached = lastObservation {
219
+ missedDetectionFrames += 1
220
+ effectiveObservation = cached
221
+ } else {
222
+ lastObservation = nil
223
+ missedDetectionFrames = 0
224
+ effectiveObservation = nil
225
+ }
226
+
208
227
  let payload: [String: Any?]
209
- if let rectangle {
228
+ if let observation = effectiveObservation {
210
229
  let points = [
211
- pointForOverlay(from: rectangle.topLeft, frameSize: frameSize),
212
- pointForOverlay(from: rectangle.topRight, frameSize: frameSize),
213
- pointForOverlay(from: rectangle.bottomRight, frameSize: frameSize),
214
- pointForOverlay(from: rectangle.bottomLeft, frameSize: frameSize),
230
+ pointForOverlay(from: observation.topLeft, frameSize: frameSize),
231
+ pointForOverlay(from: observation.topRight, frameSize: frameSize),
232
+ pointForOverlay(from: observation.bottomRight, frameSize: frameSize),
233
+ pointForOverlay(from: observation.bottomLeft, frameSize: frameSize),
215
234
  ]
216
235
 
217
236
  currentStableCounter = min(currentStableCounter + 1, Int(truncating: detectionCountBeforeCapture))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -47,6 +47,29 @@ const buildPath = (points: Point[]) => {
47
47
  return path;
48
48
  };
49
49
 
50
+ const orderQuad = (points: Point[]): Point[] => {
51
+ if (points.length !== 4) {
52
+ return points;
53
+ }
54
+
55
+ const sum = (p: Point) => p.x + p.y;
56
+ const diff = (p: Point) => p.x - p.y;
57
+
58
+ const topLeft = points.reduce((prev, curr) => (sum(curr) < sum(prev) ? curr : prev));
59
+ const bottomRight = points.reduce((prev, curr) => (sum(curr) > sum(prev) ? curr : prev));
60
+
61
+ const remaining = points.filter((p) => p !== topLeft && p !== bottomRight);
62
+ if (remaining.length !== 2) {
63
+ return [topLeft, bottomRight, ...remaining];
64
+ }
65
+
66
+ const [candidate1, candidate2] = remaining;
67
+ const topRight = diff(candidate1) > diff(candidate2) ? candidate1 : candidate2;
68
+ const bottomLeft = topRight === candidate1 ? candidate2 : candidate1;
69
+
70
+ return [topLeft, topRight, bottomRight, bottomLeft];
71
+ };
72
+
50
73
  export const Overlay: React.FC<OverlayProps> = ({
51
74
  quad,
52
75
  color = '#e7a649',
@@ -105,11 +128,12 @@ export const Overlay: React.FC<OverlayProps> = ({
105
128
  return { outlinePath: null, gridPaths: [] };
106
129
  }
107
130
 
108
- const skPath = buildPath(transformedQuad);
131
+ const normalizedQuad = orderQuad(transformedQuad);
132
+ const skPath = buildPath(normalizedQuad);
109
133
  const grid: ReturnType<typeof Skia.Path.Make>[] = [];
110
134
 
111
135
  if (showGrid) {
112
- const [topLeft, topRight, bottomRight, bottomLeft] = transformedQuad;
136
+ const [topLeft, topRight, bottomRight, bottomLeft] = normalizedQuad;
113
137
  const steps = [1 / 3, 2 / 3];
114
138
 
115
139
  steps.forEach((t) => {