react-native-rectangle-doc-scanner 12.0.0 → 13.0.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.
@@ -86,7 +86,6 @@ dependencies {
86
86
 
87
87
  // ML Kit object detection for live rectangle hints (Camera2 mode)
88
88
  implementation 'com.google.mlkit:object-detection:17.0.1'
89
- implementation 'com.google.android.gms:play-services-mlkit-document-scanner:16.0.0-beta1'
90
89
 
91
90
  if (hasVisionCamera) {
92
91
  // VisionCamera mode - include VisionCamera dependency
@@ -486,32 +486,11 @@ class CameraController(
486
486
 
487
487
  if (viewWidth <= 0 || viewHeight <= 0) return null
488
488
 
489
- // The image coordinates are in camera sensor space. We need to transform them
490
- // to match how the TextureView displays the image (after rotation/scaling).
491
- val sensorOrientation = getCameraSensorOrientation()
492
- val displayRotationDegrees = when (textureView.display?.rotation ?: Surface.ROTATION_0) {
493
- Surface.ROTATION_0 -> 0
494
- Surface.ROTATION_90 -> 90
495
- Surface.ROTATION_180 -> 180
496
- Surface.ROTATION_270 -> 270
497
- else -> 0
498
- }
499
-
500
- fun rotatePoint(point: org.opencv.core.Point): org.opencv.core.Point {
501
- return if (sensorOrientation == 90) {
502
- org.opencv.core.Point(
503
- point.y,
504
- imageWidth - point.x
505
- )
506
- } else {
507
- point
508
- }
509
- }
510
-
511
- val finalWidth = if (sensorOrientation == 90) imageHeight else imageWidth
512
- val finalHeight = if (sensorOrientation == 90) imageWidth else imageHeight
489
+ // Rectangle coordinates are already in the rotated image space (effective rotation applied).
490
+ val finalWidth = imageWidth
491
+ val finalHeight = imageHeight
513
492
 
514
- // Then apply fit-center scaling
493
+ // Apply fit-center scaling to match TextureView display.
515
494
  val scaleX = viewWidth / finalWidth.toFloat()
516
495
  val scaleY = viewHeight / finalHeight.toFloat()
517
496
  val scale = scaleX.coerceAtMost(scaleY)
@@ -522,10 +501,9 @@ class CameraController(
522
501
  val offsetY = (viewHeight - scaledHeight) / 2f
523
502
 
524
503
  fun transformPoint(point: org.opencv.core.Point): org.opencv.core.Point {
525
- val rotated = rotatePoint(point)
526
504
  return org.opencv.core.Point(
527
- rotated.x * scale + offsetX,
528
- rotated.y * scale + offsetY
505
+ point.x * scale + offsetX,
506
+ point.y * scale + offsetY
529
507
  )
530
508
  }
531
509
 
@@ -536,10 +514,9 @@ class CameraController(
536
514
  transformPoint(rectangle.bottomRight)
537
515
  )
538
516
 
539
- Log.d(TAG, "[MAPPING] Sensor: ${sensorOrientation}°, Image: ${imageWidth}x${imageHeight} → Final: ${finalWidth}x${finalHeight}")
517
+ Log.d(TAG, "[MAPPING] Image: ${imageWidth}x${imageHeight} → Final: ${finalWidth}x${finalHeight}")
540
518
  Log.d(TAG, "[MAPPING] View: ${viewWidth.toInt()}x${viewHeight.toInt()}, Scale: $scale, Offset: ($offsetX, $offsetY)")
541
519
  Log.d(TAG, "[MAPPING] TL: (${rectangle.topLeft.x}, ${rectangle.topLeft.y}) → " +
542
- "Rotated: (${rotatePoint(rectangle.topLeft).x}, ${rotatePoint(rectangle.topLeft).y}) → " +
543
520
  "Final: (${result.topLeft.x}, ${result.topLeft.y})")
544
521
 
545
522
  return result
@@ -49,7 +49,6 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
49
49
  var brightness: Float = 0f
50
50
  var contrast: Float = 1f
51
51
  var saturation: Float = 1f
52
- var useExternalScanner: Boolean = false
53
52
 
54
53
  // State
55
54
  private var stableCounter = 0
@@ -61,6 +60,7 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
61
60
  private var lastDetectedImageHeight = 0
62
61
  private var lastRectangleOnScreen: Rectangle? = null
63
62
  private var lastSmoothedRectangleOnScreen: Rectangle? = null
63
+ private val iouHistory = ArrayDeque<Rectangle>()
64
64
 
65
65
  // Coroutine scope for async operations
66
66
  private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
@@ -143,10 +143,6 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
143
143
  }
144
144
 
145
145
  private fun initializeCameraWhenReady() {
146
- if (useExternalScanner) {
147
- Log.d(TAG, "[INIT] External scanner enabled - skipping camera startup")
148
- return
149
- }
150
146
  // If view is already laid out, start camera immediately
151
147
  if (width > 0 && height > 0) {
152
148
  Log.d(TAG, "[INIT] View already laid out, starting camera immediately")
@@ -180,20 +176,6 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
180
176
  }
181
177
  }
182
178
 
183
- fun setUseExternalScanner(enabled: Boolean) {
184
- if (useExternalScanner == enabled) {
185
- return
186
- }
187
- useExternalScanner = enabled
188
- Log.d(TAG, "[SET] useExternalScanner: $enabled")
189
- if (enabled) {
190
- stopCamera()
191
- overlayView.setRectangle(null, overlayColor)
192
- } else if (width > 0 && height > 0 && cameraController == null) {
193
- setupCamera()
194
- startCamera()
195
- }
196
- }
197
179
 
198
180
  private fun setupCamera() {
199
181
  try {
@@ -325,23 +307,33 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
325
307
  overlayView.setRectangle(rectangleOnScreen, overlayColor)
326
308
  }
327
309
 
328
- // Update stable counter based on quality
310
+ // Update stable counter based on quality + IOU stability
329
311
  if (rectangleCoordinates == null) {
330
312
  if (stableCounter != 0) {
331
313
  Log.d(TAG, "Rectangle lost, resetting stableCounter")
332
314
  }
333
315
  stableCounter = 0
316
+ clearIouHistory()
334
317
  } else {
335
318
  when (quality) {
336
319
  RectangleQuality.GOOD -> {
337
- stableCounter = min(stableCounter + 1, detectionCountBeforeCapture)
338
- Log.d(TAG, "Good rectangle detected, stableCounter: $stableCounter/$detectionCountBeforeCapture")
320
+ val isStable = rectangleOnScreen?.let { updateIouHistory(it) } ?: false
321
+ if (isStable) {
322
+ stableCounter = min(stableCounter + 1, detectionCountBeforeCapture)
323
+ Log.d(TAG, "Good rectangle detected, stableCounter: $stableCounter/$detectionCountBeforeCapture")
324
+ } else {
325
+ if (stableCounter > 0) {
326
+ stableCounter--
327
+ }
328
+ Log.d(TAG, "Rectangle unstable (IOU), stableCounter: $stableCounter")
329
+ }
339
330
  }
340
331
  RectangleQuality.BAD_ANGLE, RectangleQuality.TOO_FAR -> {
341
332
  if (stableCounter > 0) {
342
333
  stableCounter--
343
334
  }
344
335
  Log.d(TAG, "Bad rectangle detected (type: $quality), stableCounter: $stableCounter")
336
+ clearIouHistory()
345
337
  }
346
338
  }
347
339
  }
@@ -357,6 +349,52 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
357
349
  }
358
350
  }
359
351
 
352
+ private fun updateIouHistory(rectangle: Rectangle): Boolean {
353
+ if (iouHistory.size >= 3) {
354
+ iouHistory.removeFirst()
355
+ }
356
+ iouHistory.addLast(rectangle)
357
+ if (iouHistory.size < 3) {
358
+ return false
359
+ }
360
+ val r0 = iouHistory.elementAt(0)
361
+ val r1 = iouHistory.elementAt(1)
362
+ val r2 = iouHistory.elementAt(2)
363
+ val iou01 = rectangleIou(r0, r1)
364
+ val iou12 = rectangleIou(r1, r2)
365
+ val iou02 = rectangleIou(r0, r2)
366
+ return iou01 >= 0.85 && iou12 >= 0.85 && iou02 >= 0.85
367
+ }
368
+
369
+ private fun clearIouHistory() {
370
+ iouHistory.clear()
371
+ }
372
+
373
+ private fun rectangleIou(a: Rectangle, b: Rectangle): Double {
374
+ fun bounds(r: Rectangle): DoubleArray {
375
+ val minX = min(min(r.topLeft.x, r.topRight.x), min(r.bottomLeft.x, r.bottomRight.x))
376
+ val maxX = max(max(r.topLeft.x, r.topRight.x), max(r.bottomLeft.x, r.bottomRight.x))
377
+ val minY = min(min(r.topLeft.y, r.topRight.y), min(r.bottomLeft.y, r.bottomRight.y))
378
+ val maxY = max(max(r.topLeft.y, r.topRight.y), max(r.bottomLeft.y, r.bottomRight.y))
379
+ return doubleArrayOf(minX, minY, maxX, maxY)
380
+ }
381
+
382
+ val ab = bounds(a)
383
+ val bb = bounds(b)
384
+ val interLeft = max(ab[0], bb[0])
385
+ val interTop = max(ab[1], bb[1])
386
+ val interRight = min(ab[2], bb[2])
387
+ val interBottom = min(ab[3], bb[3])
388
+ val interW = max(0.0, interRight - interLeft)
389
+ val interH = max(0.0, interBottom - interTop)
390
+ val interArea = interW * interH
391
+ val areaA = max(0.0, (ab[2] - ab[0])) * max(0.0, (ab[3] - ab[1]))
392
+ val areaB = max(0.0, (bb[2] - bb[0])) * max(0.0, (bb[3] - bb[1]))
393
+ val union = areaA + areaB - interArea
394
+ if (union <= 0.0) return 0.0
395
+ return interArea / union
396
+ }
397
+
360
398
  fun capture() {
361
399
  captureWithPromise(null)
362
400
  }
@@ -94,11 +94,6 @@ class DocumentScannerViewManager : SimpleViewManager<DocumentScannerView>() {
94
94
  view.saturation = saturation
95
95
  }
96
96
 
97
- @ReactProp(name = "useExternalScanner")
98
- fun setUseExternalScanner(view: DocumentScannerView, enabled: Boolean) {
99
- view.setUseExternalScanner(enabled)
100
- }
101
-
102
97
  override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
103
98
  return MapBuilder.of(
104
99
  "onPictureTaken",
@@ -1,49 +1,24 @@
1
1
  package com.reactnativerectangledocscanner
2
2
 
3
- import android.app.Activity
4
- import android.content.Intent
5
3
  import android.graphics.BitmapFactory
6
- import android.net.Uri
7
4
  import android.util.Log
8
5
  import com.facebook.react.bridge.*
9
6
  import com.facebook.react.uimanager.UIManagerModule
10
- import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions
11
- import com.google.mlkit.vision.documentscanner.GmsDocumentScanning
12
- import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult
13
7
  import kotlinx.coroutines.*
14
8
  import org.opencv.core.Point
15
- import java.io.File
16
- import java.io.FileOutputStream
17
9
 
18
10
  class DocumentScannerModule(reactContext: ReactApplicationContext) :
19
- ReactContextBaseJavaModule(reactContext), ActivityEventListener {
11
+ ReactContextBaseJavaModule(reactContext) {
20
12
 
21
13
  private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
22
14
 
23
15
  companion object {
24
16
  const val NAME = "RNPdfScannerManager"
25
17
  private const val TAG = "DocumentScannerModule"
26
- private const val EXTERNAL_SCAN_REQUEST = 9401
27
18
  }
28
19
 
29
20
  override fun getName() = NAME
30
21
 
31
- private data class PendingScanConfig(
32
- val useBase64: Boolean,
33
- val saveInAppDocument: Boolean,
34
- val quality: Float,
35
- val brightness: Float,
36
- val contrast: Float,
37
- val saturation: Float
38
- )
39
-
40
- private var pendingScanPromise: Promise? = null
41
- private var pendingScanConfig: PendingScanConfig? = null
42
-
43
- init {
44
- reactContext.addActivityEventListener(this)
45
- }
46
-
47
22
  /**
48
23
  * Capture image from the document scanner view
49
24
  * Matches iOS signature: capture(reactTag, resolver, rejecter)
@@ -71,13 +46,9 @@ class DocumentScannerModule(reactContext: ReactApplicationContext) :
71
46
  if (view is DocumentScannerView) {
72
47
  Log.d(TAG, "Found DocumentScannerView, triggering capture with promise")
73
48
 
74
- if (view.useExternalScanner) {
75
- startExternalScan(view, promise)
76
- } else {
77
- // Pass promise to view so it can be resolved when capture completes
78
- // This matches iOS behavior where promise is resolved with actual image data
79
- view.captureWithPromise(promise)
80
- }
49
+ // Pass promise to view so it can be resolved when capture completes
50
+ // This matches iOS behavior where promise is resolved with actual image data
51
+ view.captureWithPromise(promise)
81
52
  } else {
82
53
  Log.e(TAG, "View with tag $tag is not DocumentScannerView: ${view?.javaClass?.simpleName}")
83
54
  promise.reject("INVALID_VIEW", "View is not a DocumentScannerView")
@@ -93,160 +64,6 @@ class DocumentScannerModule(reactContext: ReactApplicationContext) :
93
64
  }
94
65
  }
95
66
 
96
- private fun startExternalScan(view: DocumentScannerView, promise: Promise) {
97
- if (pendingScanPromise != null) {
98
- promise.reject("SCAN_IN_PROGRESS", "Another scan is already in progress")
99
- return
100
- }
101
-
102
- val activity = currentActivity ?: run {
103
- promise.reject("NO_ACTIVITY", "Activity not available")
104
- return
105
- }
106
-
107
- val options = GmsDocumentScannerOptions.Builder()
108
- .setScannerMode(GmsDocumentScannerOptions.SCANNER_MODE_FULL)
109
- .setGalleryImportAllowed(true)
110
- .setPageLimit(1)
111
- .setResultFormats(GmsDocumentScannerOptions.RESULT_FORMAT_JPEG)
112
- .build()
113
-
114
- val scanner = GmsDocumentScanning.getClient(options)
115
-
116
- pendingScanPromise = promise
117
- pendingScanConfig = PendingScanConfig(
118
- useBase64 = view.useBase64,
119
- saveInAppDocument = view.saveInAppDocument,
120
- quality = view.quality,
121
- brightness = view.brightness,
122
- contrast = view.contrast,
123
- saturation = view.saturation
124
- )
125
-
126
- scanner.getStartScanIntent(activity)
127
- .addOnSuccessListener { intentSender ->
128
- try {
129
- activity.startIntentSenderForResult(
130
- intentSender,
131
- EXTERNAL_SCAN_REQUEST,
132
- null,
133
- 0,
134
- 0,
135
- 0
136
- )
137
- } catch (e: Exception) {
138
- Log.e(TAG, "Failed to launch ML Kit scanner", e)
139
- cleanupPendingScan()
140
- promise.reject("SCAN_LAUNCH_FAILED", "Failed to launch scanner: ${e.message}", e)
141
- }
142
- }
143
- .addOnFailureListener { e ->
144
- Log.e(TAG, "Failed to get ML Kit scan intent", e)
145
- cleanupPendingScan()
146
- promise.reject("SCAN_INTENT_FAILED", "Failed to start scanner: ${e.message}", e)
147
- }
148
- }
149
-
150
- override fun onActivityResult(activity: Activity?, requestCode: Int, resultCode: Int, data: Intent?) {
151
- if (requestCode != EXTERNAL_SCAN_REQUEST) {
152
- return
153
- }
154
-
155
- val promise = pendingScanPromise ?: return
156
- val config = pendingScanConfig
157
- cleanupPendingScan()
158
-
159
- if (resultCode != Activity.RESULT_OK || data == null) {
160
- promise.reject("SCAN_CANCELLED", "Scan cancelled or failed")
161
- return
162
- }
163
-
164
- val result = GmsDocumentScanningResult.fromActivityResultIntent(data)
165
- val page = result?.pages?.firstOrNull()
166
- val imageUri = page?.imageUri
167
-
168
- if (imageUri == null || config == null) {
169
- promise.reject("SCAN_NO_RESULT", "No scanned image returned")
170
- return
171
- }
172
-
173
- scope.launch {
174
- try {
175
- val outputDir = if (config.saveInAppDocument) {
176
- reactApplicationContext.filesDir
177
- } else {
178
- reactApplicationContext.cacheDir
179
- }
180
- val timestamp = System.currentTimeMillis()
181
- val initialPath = copyUriToFile(imageUri, outputDir, "doc_scan_initial_$timestamp.jpg")
182
-
183
- val processed = withContext(Dispatchers.IO) {
184
- ImageProcessor.processImage(
185
- imagePath = initialPath,
186
- rectangle = null,
187
- brightness = config.brightness,
188
- contrast = config.contrast,
189
- saturation = config.saturation,
190
- shouldCrop = false
191
- )
192
- }
193
-
194
- val resultMap = Arguments.createMap()
195
- if (config.useBase64) {
196
- val croppedBase64 = ImageProcessor.bitmapToBase64(processed.croppedImage, config.quality)
197
- val initialBase64 = ImageProcessor.bitmapToBase64(processed.initialImage, config.quality)
198
- resultMap.putString("croppedImage", croppedBase64)
199
- resultMap.putString("initialImage", initialBase64)
200
- } else {
201
- val croppedPath = ImageProcessor.saveBitmapToFile(
202
- processed.croppedImage,
203
- outputDir,
204
- "doc_scan_cropped_$timestamp.jpg",
205
- config.quality
206
- )
207
- resultMap.putString("croppedImage", croppedPath)
208
- resultMap.putString("initialImage", initialPath)
209
- }
210
-
211
- resultMap.putMap("rectangleCoordinates", null)
212
- resultMap.putInt("width", processed.croppedImage.width)
213
- resultMap.putInt("height", processed.croppedImage.height)
214
-
215
- // Cleanup bitmaps to avoid leaks.
216
- if (processed.croppedImage !== processed.initialImage) {
217
- processed.croppedImage.recycle()
218
- processed.initialImage.recycle()
219
- } else {
220
- processed.croppedImage.recycle()
221
- }
222
-
223
- promise.resolve(resultMap)
224
- } catch (e: Exception) {
225
- Log.e(TAG, "Failed to process scan result", e)
226
- promise.reject("SCAN_PROCESS_FAILED", "Failed to process scan result: ${e.message}", e)
227
- }
228
- }
229
- }
230
-
231
- override fun onNewIntent(intent: Intent?) {
232
- // No-op
233
- }
234
-
235
- private fun copyUriToFile(uri: Uri, outputDir: File, fileName: String): String {
236
- val outputFile = File(outputDir, fileName)
237
- reactApplicationContext.contentResolver.openInputStream(uri)?.use { input ->
238
- FileOutputStream(outputFile).use { output ->
239
- input.copyTo(output)
240
- }
241
- } ?: throw IllegalStateException("Failed to open input stream for URI: $uri")
242
- return outputFile.absolutePath
243
- }
244
-
245
- private fun cleanupPendingScan() {
246
- pendingScanPromise = null
247
- pendingScanConfig = null
248
- }
249
-
250
67
  /**
251
68
  * Apply color controls to an image
252
69
  * Matches iOS: applyColorControls(imagePath, brightness, contrast, saturation, resolver, rejecter)
@@ -715,28 +715,19 @@ const NativeScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAU
715
715
  const overlayIsActive = autoCapture ? isAutoCapturing : (detectedRectangle?.stableCounter ?? 0) > 0;
716
716
  const detectionThreshold = autoCapture ? minStableFrames : 99999;
717
717
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
718
- react_1.default.createElement(react_native_document_scanner_1.default, { ref: scannerRef, style: styles.scanner, detectionCountBeforeCapture: detectionThreshold, overlayColor: overlayColor, enableTorch: enableTorch, quality: normalizedQuality, useBase64: useBase64, manualOnly: react_native_1.Platform.OS === 'android', detectionConfig: detectionConfig, useExternalScanner: react_native_1.Platform.OS === 'android', onPictureTaken: handlePictureTaken, onError: handleError, onRectangleDetect: handleRectangleDetect }),
718
+ react_1.default.createElement(react_native_document_scanner_1.default, { ref: scannerRef, style: styles.scanner, detectionCountBeforeCapture: detectionThreshold, overlayColor: overlayColor, enableTorch: enableTorch, quality: normalizedQuality, useBase64: useBase64, manualOnly: react_native_1.Platform.OS === 'android', detectionConfig: detectionConfig, onPictureTaken: handlePictureTaken, onError: handleError, onRectangleDetect: handleRectangleDetect }),
719
719
  showGrid && overlayPolygon && (react_1.default.createElement(overlay_1.ScannerOverlay, { active: overlayIsActive, color: gridColor ?? overlayColor, lineWidth: gridLineWidth, polygon: overlayPolygon, clipRect: react_native_1.Platform.OS === 'android' ? null : (detectedRectangle?.previewViewport ?? null) })),
720
720
  showManualCaptureButton && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: handleManualCapture })),
721
721
  children));
722
722
  });
723
723
  exports.DocScanner = (0, react_1.forwardRef)((props, ref) => {
724
- const useExternalScanner = react_native_1.Platform.OS === 'android';
725
724
  (0, react_1.useEffect)(() => {
726
- if (react_native_1.Platform.OS !== 'android' || useExternalScanner) {
725
+ if (react_native_1.Platform.OS !== 'android') {
727
726
  return;
728
727
  }
729
- if (hasVisionCamera) {
730
- console.log('[DocScanner] Using VisionCamera pipeline');
731
- }
732
- else {
733
- console.warn('[DocScanner] VisionCamera pipeline unavailable, falling back to native view.', {
734
- hasVisionCameraModule: Boolean(visionCameraModule),
735
- hasReanimated: Boolean(reanimatedModule),
736
- });
737
- }
728
+ console.log('[DocScanner] Using native CameraX pipeline on Android');
738
729
  }, []);
739
- if (useExternalScanner) {
730
+ if (react_native_1.Platform.OS === 'android') {
740
731
  return react_1.default.createElement(NativeScanner, { ref: ref, ...props });
741
732
  }
742
733
  if (hasVisionCamera) {
@@ -124,7 +124,6 @@ const normalizeCapturedDocument = (document) => {
124
124
  };
125
125
  };
126
126
  const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3170f3', gridColor, gridLineWidth, showGrid, strings, minStableFrames, onError, enableGallery = true, cropWidth = 1200, cropHeight = 1600, type, }) => {
127
- const useExternalScanner = react_native_1.Platform.OS === 'android';
128
127
  const [processing, setProcessing] = (0, react_1.useState)(false);
129
128
  const [croppedImageData, setCroppedImageData] = (0, react_1.useState)(null);
130
129
  const [isGalleryOpen, setIsGalleryOpen] = (0, react_1.useState)(false);
@@ -144,11 +143,6 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
144
143
  const rectangleHintTimeoutRef = (0, react_1.useRef)(null);
145
144
  const captureReadyTimeoutRef = (0, react_1.useRef)(null);
146
145
  const isBusinessMode = type === 'business';
147
- (0, react_1.useEffect)(() => {
148
- if (useExternalScanner) {
149
- setCaptureReady(true);
150
- }
151
- }, [useExternalScanner]);
152
146
  const resetScannerView = (0, react_1.useCallback)((options) => {
153
147
  setProcessing(false);
154
148
  setCroppedImageData(null);
@@ -394,7 +388,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
394
388
  return;
395
389
  }
396
390
  console.log('[FullDocScanner] Starting manual capture, grid detected:', rectangleDetected);
397
- const captureMode = useExternalScanner ? 'grid' : (rectangleDetected ? 'grid' : 'no-grid');
391
+ const captureMode = rectangleDetected ? 'grid' : 'no-grid';
398
392
  captureModeRef.current = captureMode;
399
393
  captureInProgressRef.current = true;
400
394
  // Add timeout to reset state if capture hangs
@@ -542,9 +536,6 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
542
536
  resetScannerView({ remount: true });
543
537
  }, [capturedPhotos.length, isBusinessMode, resetScannerView]);
544
538
  const handleRectangleDetect = (0, react_1.useCallback)((event) => {
545
- if (useExternalScanner) {
546
- return;
547
- }
548
539
  const stableCounter = event.stableCounter ?? 0;
549
540
  const rectangleCoordinates = event.rectangleOnScreen ?? event.rectangleCoordinates;
550
541
  const hasRectangle = Boolean(rectangleCoordinates);
@@ -598,7 +589,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
598
589
  }
599
590
  setRectangleDetected(false);
600
591
  }
601
- }, [rectangleDetected, useExternalScanner]);
592
+ }, [rectangleDetected]);
602
593
  (0, react_1.useEffect)(() => () => {
603
594
  if (rectangleCaptureTimeoutRef.current) {
604
595
  clearTimeout(rectangleCaptureTimeoutRef.current);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "12.0.0",
3
+ "version": "13.0.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -1008,7 +1008,6 @@ const NativeScanner = forwardRef<DocScannerHandle, Props>(
1008
1008
  useBase64={useBase64}
1009
1009
  manualOnly={Platform.OS === 'android'}
1010
1010
  detectionConfig={detectionConfig}
1011
- useExternalScanner={Platform.OS === 'android'}
1012
1011
  onPictureTaken={handlePictureTaken}
1013
1012
  onError={handleError}
1014
1013
  onRectangleDetect={handleRectangleDetect}
@@ -1032,23 +1031,14 @@ const NativeScanner = forwardRef<DocScannerHandle, Props>(
1032
1031
  );
1033
1032
 
1034
1033
  export const DocScanner = forwardRef<DocScannerHandle, Props>((props, ref) => {
1035
- const useExternalScanner = Platform.OS === 'android';
1036
-
1037
1034
  useEffect(() => {
1038
- if (Platform.OS !== 'android' || useExternalScanner) {
1035
+ if (Platform.OS !== 'android') {
1039
1036
  return;
1040
1037
  }
1041
- if (hasVisionCamera) {
1042
- console.log('[DocScanner] Using VisionCamera pipeline');
1043
- } else {
1044
- console.warn('[DocScanner] VisionCamera pipeline unavailable, falling back to native view.', {
1045
- hasVisionCameraModule: Boolean(visionCameraModule),
1046
- hasReanimated: Boolean(reanimatedModule),
1047
- });
1048
- }
1038
+ console.log('[DocScanner] Using native CameraX pipeline on Android');
1049
1039
  }, []);
1050
1040
 
1051
- if (useExternalScanner) {
1041
+ if (Platform.OS === 'android') {
1052
1042
  return <NativeScanner ref={ref} {...props} />;
1053
1043
  }
1054
1044
 
@@ -187,7 +187,6 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
187
187
  cropHeight = 1600,
188
188
  type,
189
189
  }) => {
190
- const useExternalScanner = Platform.OS === 'android';
191
190
  const [processing, setProcessing] = useState(false);
192
191
  const [croppedImageData, setCroppedImageData] = useState<PreviewImageData | null>(null);
193
192
  const [isGalleryOpen, setIsGalleryOpen] = useState(false);
@@ -209,12 +208,6 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
209
208
 
210
209
  const isBusinessMode = type === 'business';
211
210
 
212
- useEffect(() => {
213
- if (useExternalScanner) {
214
- setCaptureReady(true);
215
- }
216
- }, [useExternalScanner]);
217
-
218
211
  const resetScannerView = useCallback(
219
212
  (options?: { remount?: boolean }) => {
220
213
  setProcessing(false);
@@ -546,7 +539,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
546
539
 
547
540
  console.log('[FullDocScanner] Starting manual capture, grid detected:', rectangleDetected);
548
541
 
549
- const captureMode = useExternalScanner ? 'grid' : (rectangleDetected ? 'grid' : 'no-grid');
542
+ const captureMode = rectangleDetected ? 'grid' : 'no-grid';
550
543
  captureModeRef.current = captureMode;
551
544
  captureInProgressRef.current = true;
552
545
 
@@ -736,9 +729,6 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
736
729
  }, [capturedPhotos.length, isBusinessMode, resetScannerView]);
737
730
 
738
731
  const handleRectangleDetect = useCallback((event: RectangleDetectEvent) => {
739
- if (useExternalScanner) {
740
- return;
741
- }
742
732
  const stableCounter = event.stableCounter ?? 0;
743
733
  const rectangleCoordinates = event.rectangleOnScreen ?? event.rectangleCoordinates;
744
734
  const hasRectangle = Boolean(rectangleCoordinates);
@@ -797,7 +787,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
797
787
  }
798
788
  setRectangleDetected(false);
799
789
  }
800
- }, [rectangleDetected, useExternalScanner]);
790
+ }, [rectangleDetected]);
801
791
 
802
792
  useEffect(
803
793
  () => () => {
package/src/external.d.ts CHANGED
@@ -75,7 +75,6 @@ declare module 'react-native-document-scanner' {
75
75
  maxAnchorMisses?: number;
76
76
  maxCenterDelta?: number;
77
77
  };
78
- useExternalScanner?: boolean;
79
78
  onPictureTaken?: (event: DocumentScannerResult) => void;
80
79
  onError?: (error: Error) => void;
81
80
  onRectangleDetect?: (event: RectangleEventPayload) => void;
@@ -46,7 +46,6 @@ export interface DocumentScannerProps {
46
46
  maxAnchorMisses?: number;
47
47
  maxCenterDelta?: number;
48
48
  };
49
- useExternalScanner?: boolean;
50
49
  onPictureTaken?: (event: DocumentScannerResult) => void;
51
50
  onError?: (error: Error) => void;
52
51
  onRectangleDetect?: (event: RectangleEventPayload) => void;