react-native-rectangle-doc-scanner 11.2.0 → 12.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,6 +86,7 @@ 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'
89
90
 
90
91
  if (hasVisionCamera) {
91
92
  // VisionCamera mode - include VisionCamera dependency
@@ -280,10 +280,11 @@ class CameraController(
280
280
  }
281
281
 
282
282
  // Use the same rotation logic as updateTextureViewTransform
283
+ val tabletUpsideDownFix = if (sensorOrientation == 0 && displayRotationDegrees == 90) 180 else 0
283
284
  val effectiveRotation = if (sensorOrientation == 0) {
284
- displayRotationDegrees // Tablet: use display rotation (90°)
285
+ (displayRotationDegrees + tabletUpsideDownFix) % 360
285
286
  } else {
286
- sensorOrientation // Phone: use sensor orientation (90°)
287
+ sensorOrientation
287
288
  }
288
289
 
289
290
  Log.d(TAG, "[ANALYZE] Sensor: $sensorOrientation°, Display: $displayRotationDegrees°, Effective: $effectiveRotation°")
@@ -49,6 +49,7 @@ 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
52
53
 
53
54
  // State
54
55
  private var stableCounter = 0
@@ -142,6 +143,10 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
142
143
  }
143
144
 
144
145
  private fun initializeCameraWhenReady() {
146
+ if (useExternalScanner) {
147
+ Log.d(TAG, "[INIT] External scanner enabled - skipping camera startup")
148
+ return
149
+ }
145
150
  // If view is already laid out, start camera immediately
146
151
  if (width > 0 && height > 0) {
147
152
  Log.d(TAG, "[INIT] View already laid out, starting camera immediately")
@@ -175,6 +180,21 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
175
180
  }
176
181
  }
177
182
 
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
+
178
198
  private fun setupCamera() {
179
199
  try {
180
200
  Log.d(TAG, "[SETUP] Creating CameraController...")
@@ -94,6 +94,11 @@ 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
+
97
102
  override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
98
103
  return MapBuilder.of(
99
104
  "onPictureTaken",
@@ -259,7 +259,7 @@ class DocumentDetector {
259
259
 
260
260
  var largestRectangle: Rectangle? = null
261
261
  var bestScore = 0.0
262
- val minArea = max(450.0, (srcMat.rows() * srcMat.cols()) * 0.0007)
262
+ val minArea = max(350.0, (srcMat.rows() * srcMat.cols()) * 0.0005)
263
263
 
264
264
  debugStats.contours = contours.size
265
265
 
@@ -289,7 +289,7 @@ class DocumentDetector {
289
289
  val rect = Imgproc.minAreaRect(MatOfPoint2f(*points))
290
290
  val rectArea = rect.size.area()
291
291
  val rectangularity = if (rectArea > 1.0) contourArea / rectArea else 0.0
292
- if (rectangularity >= 0.6 && isCandidateValid(ordered, srcMat)) {
292
+ if (rectangularity >= 0.5 && isCandidateValid(ordered, srcMat)) {
293
293
  debugStats.candidates += 1
294
294
  val score = contourArea * rectangularity
295
295
  if (score > bestScore) {
@@ -313,7 +313,7 @@ class DocumentDetector {
313
313
  val rectArea = rotated.size.area()
314
314
  if (rectArea > 1.0) {
315
315
  val rectangularity = contourArea / rectArea
316
- if (rectangularity >= 0.6) {
316
+ if (rectangularity >= 0.5) {
317
317
  debugStats.candidates += 1
318
318
  val boxPoints = Array(4) { Point() }
319
319
  rotated.points(boxPoints)
@@ -497,16 +497,16 @@ class DocumentDetector {
497
497
  val rectHeight = max(leftEdgeLength, rightEdgeLength)
498
498
  val rectArea = rectWidth * rectHeight
499
499
 
500
- // Check if rectangle is too small (less than 15% of view area)
501
- // or too large (more than 85% - likely detecting screen instead of document)
500
+ // Check if rectangle is too small (less than 6% of view area)
501
+ // or too large (more than 95% - likely detecting screen instead of document)
502
502
  val areaRatio = rectArea / viewArea
503
- if (areaRatio < 0.15) {
503
+ if (areaRatio < 0.06) {
504
504
  if (BuildConfig.DEBUG) {
505
505
  Log.d(TAG, "[QUALITY] TOO_FAR (small): area=${String.format("%.1f", rectArea)}, ratio=${String.format("%.2f", areaRatio)}")
506
506
  }
507
507
  return RectangleQuality.TOO_FAR
508
508
  }
509
- if (areaRatio > 0.85) {
509
+ if (areaRatio > 0.95) {
510
510
  if (BuildConfig.DEBUG) {
511
511
  Log.d(TAG, "[QUALITY] TOO_FAR (large): area=${String.format("%.1f", rectArea)}, ratio=${String.format("%.2f", areaRatio)} - likely detecting screen")
512
512
  }
@@ -1,24 +1,49 @@
1
1
  package com.reactnativerectangledocscanner
2
2
 
3
+ import android.app.Activity
4
+ import android.content.Intent
3
5
  import android.graphics.BitmapFactory
6
+ import android.net.Uri
4
7
  import android.util.Log
5
8
  import com.facebook.react.bridge.*
6
9
  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
7
13
  import kotlinx.coroutines.*
8
14
  import org.opencv.core.Point
15
+ import java.io.File
16
+ import java.io.FileOutputStream
9
17
 
10
18
  class DocumentScannerModule(reactContext: ReactApplicationContext) :
11
- ReactContextBaseJavaModule(reactContext) {
19
+ ReactContextBaseJavaModule(reactContext), ActivityEventListener {
12
20
 
13
21
  private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
14
22
 
15
23
  companion object {
16
24
  const val NAME = "RNPdfScannerManager"
17
25
  private const val TAG = "DocumentScannerModule"
26
+ private const val EXTERNAL_SCAN_REQUEST = 9401
18
27
  }
19
28
 
20
29
  override fun getName() = NAME
21
30
 
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
+
22
47
  /**
23
48
  * Capture image from the document scanner view
24
49
  * Matches iOS signature: capture(reactTag, resolver, rejecter)
@@ -46,9 +71,13 @@ class DocumentScannerModule(reactContext: ReactApplicationContext) :
46
71
  if (view is DocumentScannerView) {
47
72
  Log.d(TAG, "Found DocumentScannerView, triggering capture with promise")
48
73
 
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)
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
+ }
52
81
  } else {
53
82
  Log.e(TAG, "View with tag $tag is not DocumentScannerView: ${view?.javaClass?.simpleName}")
54
83
  promise.reject("INVALID_VIEW", "View is not a DocumentScannerView")
@@ -64,6 +93,160 @@ class DocumentScannerModule(reactContext: ReactApplicationContext) :
64
93
  }
65
94
  }
66
95
 
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
+
67
250
  /**
68
251
  * Apply color controls to an image
69
252
  * Matches iOS: applyColorControls(imagePath, brightness, contrast, saturation, resolver, rejecter)
@@ -715,14 +715,15 @@ 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: false, detectionConfig: detectionConfig, 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, useExternalScanner: react_native_1.Platform.OS === 'android', 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';
724
725
  (0, react_1.useEffect)(() => {
725
- if (react_native_1.Platform.OS !== 'android') {
726
+ if (react_native_1.Platform.OS !== 'android' || useExternalScanner) {
726
727
  return;
727
728
  }
728
729
  if (hasVisionCamera) {
@@ -735,6 +736,9 @@ exports.DocScanner = (0, react_1.forwardRef)((props, ref) => {
735
736
  });
736
737
  }
737
738
  }, []);
739
+ if (useExternalScanner) {
740
+ return react_1.default.createElement(NativeScanner, { ref: ref, ...props });
741
+ }
738
742
  if (hasVisionCamera) {
739
743
  return react_1.default.createElement(VisionCameraScanner, { ref: ref, ...props });
740
744
  }
@@ -124,6 +124,7 @@ 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';
127
128
  const [processing, setProcessing] = (0, react_1.useState)(false);
128
129
  const [croppedImageData, setCroppedImageData] = (0, react_1.useState)(null);
129
130
  const [isGalleryOpen, setIsGalleryOpen] = (0, react_1.useState)(false);
@@ -143,6 +144,11 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
143
144
  const rectangleHintTimeoutRef = (0, react_1.useRef)(null);
144
145
  const captureReadyTimeoutRef = (0, react_1.useRef)(null);
145
146
  const isBusinessMode = type === 'business';
147
+ (0, react_1.useEffect)(() => {
148
+ if (useExternalScanner) {
149
+ setCaptureReady(true);
150
+ }
151
+ }, [useExternalScanner]);
146
152
  const resetScannerView = (0, react_1.useCallback)((options) => {
147
153
  setProcessing(false);
148
154
  setCroppedImageData(null);
@@ -388,7 +394,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
388
394
  return;
389
395
  }
390
396
  console.log('[FullDocScanner] Starting manual capture, grid detected:', rectangleDetected);
391
- const captureMode = rectangleDetected ? 'grid' : 'no-grid';
397
+ const captureMode = useExternalScanner ? 'grid' : (rectangleDetected ? 'grid' : 'no-grid');
392
398
  captureModeRef.current = captureMode;
393
399
  captureInProgressRef.current = true;
394
400
  // Add timeout to reset state if capture hangs
@@ -536,6 +542,9 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
536
542
  resetScannerView({ remount: true });
537
543
  }, [capturedPhotos.length, isBusinessMode, resetScannerView]);
538
544
  const handleRectangleDetect = (0, react_1.useCallback)((event) => {
545
+ if (useExternalScanner) {
546
+ return;
547
+ }
539
548
  const stableCounter = event.stableCounter ?? 0;
540
549
  const rectangleCoordinates = event.rectangleOnScreen ?? event.rectangleCoordinates;
541
550
  const hasRectangle = Boolean(rectangleCoordinates);
@@ -589,7 +598,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
589
598
  }
590
599
  setRectangleDetected(false);
591
600
  }
592
- }, [rectangleDetected]);
601
+ }, [rectangleDetected, useExternalScanner]);
593
602
  (0, react_1.useEffect)(() => () => {
594
603
  if (rectangleCaptureTimeoutRef.current) {
595
604
  clearTimeout(rectangleCaptureTimeoutRef.current);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "11.2.0",
3
+ "version": "12.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",
@@ -994,7 +994,7 @@ const NativeScanner = forwardRef<DocScannerHandle, Props>(
994
994
  : detectedRectangle?.rectangleOnScreen ?? detectedRectangle?.rectangleCoordinates ?? null;
995
995
  const overlayIsActive = autoCapture ? isAutoCapturing : (detectedRectangle?.stableCounter ?? 0) > 0;
996
996
 
997
- const detectionThreshold = autoCapture ? minStableFrames : 99999;
997
+ const detectionThreshold = autoCapture ? minStableFrames : 99999;
998
998
 
999
999
  return (
1000
1000
  <View style={styles.container}>
@@ -1006,8 +1006,9 @@ const NativeScanner = forwardRef<DocScannerHandle, Props>(
1006
1006
  enableTorch={enableTorch}
1007
1007
  quality={normalizedQuality}
1008
1008
  useBase64={useBase64}
1009
- manualOnly={false}
1009
+ manualOnly={Platform.OS === 'android'}
1010
1010
  detectionConfig={detectionConfig}
1011
+ useExternalScanner={Platform.OS === 'android'}
1011
1012
  onPictureTaken={handlePictureTaken}
1012
1013
  onError={handleError}
1013
1014
  onRectangleDetect={handleRectangleDetect}
@@ -1031,8 +1032,10 @@ const NativeScanner = forwardRef<DocScannerHandle, Props>(
1031
1032
  );
1032
1033
 
1033
1034
  export const DocScanner = forwardRef<DocScannerHandle, Props>((props, ref) => {
1035
+ const useExternalScanner = Platform.OS === 'android';
1036
+
1034
1037
  useEffect(() => {
1035
- if (Platform.OS !== 'android') {
1038
+ if (Platform.OS !== 'android' || useExternalScanner) {
1036
1039
  return;
1037
1040
  }
1038
1041
  if (hasVisionCamera) {
@@ -1045,6 +1048,10 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>((props, ref) => {
1045
1048
  }
1046
1049
  }, []);
1047
1050
 
1051
+ if (useExternalScanner) {
1052
+ return <NativeScanner ref={ref} {...props} />;
1053
+ }
1054
+
1048
1055
  if (hasVisionCamera) {
1049
1056
  return <VisionCameraScanner ref={ref} {...props} />;
1050
1057
  }
@@ -187,6 +187,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
187
187
  cropHeight = 1600,
188
188
  type,
189
189
  }) => {
190
+ const useExternalScanner = Platform.OS === 'android';
190
191
  const [processing, setProcessing] = useState(false);
191
192
  const [croppedImageData, setCroppedImageData] = useState<PreviewImageData | null>(null);
192
193
  const [isGalleryOpen, setIsGalleryOpen] = useState(false);
@@ -208,6 +209,12 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
208
209
 
209
210
  const isBusinessMode = type === 'business';
210
211
 
212
+ useEffect(() => {
213
+ if (useExternalScanner) {
214
+ setCaptureReady(true);
215
+ }
216
+ }, [useExternalScanner]);
217
+
211
218
  const resetScannerView = useCallback(
212
219
  (options?: { remount?: boolean }) => {
213
220
  setProcessing(false);
@@ -539,7 +546,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
539
546
 
540
547
  console.log('[FullDocScanner] Starting manual capture, grid detected:', rectangleDetected);
541
548
 
542
- const captureMode = rectangleDetected ? 'grid' : 'no-grid';
549
+ const captureMode = useExternalScanner ? 'grid' : (rectangleDetected ? 'grid' : 'no-grid');
543
550
  captureModeRef.current = captureMode;
544
551
  captureInProgressRef.current = true;
545
552
 
@@ -729,6 +736,9 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
729
736
  }, [capturedPhotos.length, isBusinessMode, resetScannerView]);
730
737
 
731
738
  const handleRectangleDetect = useCallback((event: RectangleDetectEvent) => {
739
+ if (useExternalScanner) {
740
+ return;
741
+ }
732
742
  const stableCounter = event.stableCounter ?? 0;
733
743
  const rectangleCoordinates = event.rectangleOnScreen ?? event.rectangleCoordinates;
734
744
  const hasRectangle = Boolean(rectangleCoordinates);
@@ -787,7 +797,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
787
797
  }
788
798
  setRectangleDetected(false);
789
799
  }
790
- }, [rectangleDetected]);
800
+ }, [rectangleDetected, useExternalScanner]);
791
801
 
792
802
  useEffect(
793
803
  () => () => {
package/src/external.d.ts CHANGED
@@ -75,6 +75,7 @@ declare module 'react-native-document-scanner' {
75
75
  maxAnchorMisses?: number;
76
76
  maxCenterDelta?: number;
77
77
  };
78
+ useExternalScanner?: boolean;
78
79
  onPictureTaken?: (event: DocumentScannerResult) => void;
79
80
  onError?: (error: Error) => void;
80
81
  onRectangleDetect?: (event: RectangleEventPayload) => void;
@@ -46,6 +46,7 @@ export interface DocumentScannerProps {
46
46
  maxAnchorMisses?: number;
47
47
  maxCenterDelta?: number;
48
48
  };
49
+ useExternalScanner?: boolean;
49
50
  onPictureTaken?: (event: DocumentScannerResult) => void;
50
51
  onError?: (error: Error) => void;
51
52
  onRectangleDetect?: (event: RectangleEventPayload) => void;