From bc029190bfffc5f591d6fb65e5d44f976183c69e Mon Sep 17 00:00:00 2001 From: Bhavya Jain Date: Thu, 20 Aug 2026 12:37:17 +0530 Subject: [PATCH 1/2] feat: expose Android scanner mode --- README.md | 21 +++++++++++++++- .../documentscanner/DocumentScannerModule.kt | 19 +++++++++++++- src/NativeDocumentScanner.ts | 25 +++++++++++++++++++ src/__tests__/index.test.tsx | 25 ++++++++++++++++++- src/index.tsx | 1 + 5 files changed, 88 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 19906f8..929d8a8 100644 --- a/README.md +++ b/README.md @@ -183,11 +183,30 @@ Opens the camera, and starts the document scan | ----------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | | **`croppedImageQuality`** | number | The quality of the cropped image from 0 - 100. 100 is the best quality. | : 100 | | **`maxNumDocuments`** | number | Android only: The maximum number of photos an user can take (not counting photo retakes) | : undefined | +| **`scannerMode`** | ScannerMode | Android only: The scanner feature set to enable. `ScannerMode.BASE` provides basic scanning and editing features, `ScannerMode.BASE_WITH_FILTER` adds image filters, and `ScannerMode.FULL` adds ML-enabled image cleaning. | : ScannerMode.FULL | | **`responseType`** | ResponseType | The response comes back in this format on success. It can be the document scan image file paths or base64 images. | : ResponseType.ImageFilePath | ### Enums +#### ScannerMode + +Android-only scanner modes. `ScannerMode.FULL` remains the default for backwards compatibility. + +| Member | Value | Description | +| --- | --- | --- | +| **`BASE`** | 'base' | Basic scanning features such as capture, auto rotation, crop, and page reordering. | +| **`BASE_WITH_FILTER`** | 'base_with_filter' | Basic scanning features plus image filters. | +| **`FULL`** | 'full' | The full feature set, including ML-enabled image cleaning. | + +```tsx +import DocumentScanner, { ScannerMode } from 'react-native-document-scanner-plugin' + +const { scannedImages } = await DocumentScanner.scanDocument({ + scannerMode: ScannerMode.BASE, +}) +``` + #### ScanDocumentResponseStatus @@ -291,4 +310,4 @@ export default () => { ) } ``` - \ No newline at end of file + diff --git a/android/src/main/java/com/documentscanner/DocumentScannerModule.kt b/android/src/main/java/com/documentscanner/DocumentScannerModule.kt index fbb6836..bc79804 100644 --- a/android/src/main/java/com/documentscanner/DocumentScannerModule.kt +++ b/android/src/main/java/com/documentscanner/DocumentScannerModule.kt @@ -51,7 +51,7 @@ class DocumentScannerModule(reactContext: ReactApplicationContext) : val documentScannerOptionsBuilder = GmsDocumentScannerOptions.Builder() .setResultFormats(GmsDocumentScannerOptions.RESULT_FORMAT_JPEG) - .setScannerMode(GmsDocumentScannerOptions.SCANNER_MODE_FULL) + .setScannerMode(getScannerMode(options)) if (options.hasKey("maxNumDocuments")) { documentScannerOptionsBuilder.setPageLimit( @@ -132,6 +132,23 @@ class DocumentScannerModule(reactContext: ReactApplicationContext) : }) } + private fun getScannerMode(options: ReadableMap): Int { + return try { + if (!options.hasKey("scannerMode") || options.isNull("scannerMode")) { + return GmsDocumentScannerOptions.SCANNER_MODE_FULL + } + + when (options.getString("scannerMode")) { + "base" -> GmsDocumentScannerOptions.SCANNER_MODE_BASE + "base_with_filter" -> GmsDocumentScannerOptions.SCANNER_MODE_BASE_WITH_FILTER + "full" -> GmsDocumentScannerOptions.SCANNER_MODE_FULL + else -> GmsDocumentScannerOptions.SCANNER_MODE_FULL + } + } catch (_: RuntimeException) { + GmsDocumentScannerOptions.SCANNER_MODE_FULL + } + } + companion object { const val NAME = "DocumentScanner" } diff --git a/src/NativeDocumentScanner.ts b/src/NativeDocumentScanner.ts index 449eeb0..4f6a66e 100644 --- a/src/NativeDocumentScanner.ts +++ b/src/NativeDocumentScanner.ts @@ -4,6 +4,25 @@ export interface Spec extends TurboModule { scanDocument(options: ScanDocumentOptions): Promise; } +export enum ScannerMode { + /** + * Basic scanning features such as capture, auto rotation, crop, and page + * reordering. Android only. + */ + BASE = 'base', + + /** + * Basic scanning features plus image filters. Android only. + */ + BASE_WITH_FILTER = 'base_with_filter', + + /** + * The full scanner feature set, including ML-enabled image cleaning. + * Android only and the default mode. + */ + FULL = 'full', +} + export interface ScanDocumentOptions { /** * The quality of the cropped image from 0 - 100. 100 is the best quality. @@ -17,6 +36,12 @@ export interface ScanDocumentOptions { */ maxNumDocuments?: number; + /** + * Android only: The scanner feature set to enable. + * @default: ScannerMode.FULL + */ + scannerMode?: ScannerMode; + /** * The response comes back in this format on success. It can be the document * scan image file paths or base64 images. diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx index bf84291..cded953 100644 --- a/src/__tests__/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -1 +1,24 @@ -it.todo('write a test'); +jest.mock('react-native', () => ({ + TurboModuleRegistry: { + getEnforcing: jest.fn(), + }, +})); + +import { + ResponseType, + ScannerMode, + ScanDocumentResponseStatus, +} from '../index'; + +describe('public API constants', () => { + it('exposes the supported scanner modes', () => { + expect(ScannerMode.BASE).toBe('base'); + expect(ScannerMode.BASE_WITH_FILTER).toBe('base_with_filter'); + expect(ScannerMode.FULL).toBe('full'); + }); + + it('preserves existing exported constants', () => { + expect(ResponseType.ImageFilePath).toBe('imageFilePath'); + expect(ScanDocumentResponseStatus.Success).toBe('success'); + }); +}); diff --git a/src/index.tsx b/src/index.tsx index d7176a2..2a043af 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -10,6 +10,7 @@ export type { export { ResponseType, + ScannerMode, ScanDocumentResponseStatus, } from './NativeDocumentScanner'; From c91bbf739c52b5166161040d9781b6704705f47d Mon Sep 17 00:00:00 2001 From: Bhavya Jain Date: Thu, 20 Aug 2026 13:18:26 +0530 Subject: [PATCH 2/2] feat: add scanner mode selector to example --- example/src/App.tsx | 140 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 117 insertions(+), 23 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 84ba72e..5dab220 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,33 +1,127 @@ -import { useState, useEffect } from 'react'; -import { Image } from 'react-native'; -import DocumentScanner from 'react-native-document-scanner-plugin'; +import { useState } from 'react'; +import { + Image, + Platform, + Pressable, + StyleSheet, + Text, + View, +} from 'react-native'; +import DocumentScanner, { + ScannerMode, +} from 'react-native-document-scanner-plugin'; + +const scannerModes = [ + { mode: ScannerMode.BASE, label: 'Base' }, + { mode: ScannerMode.BASE_WITH_FILTER, label: 'Base with filter' }, + { mode: ScannerMode.FULL, label: 'Full' }, +]; export default () => { - const [scannedImage, setScannedImage] = useState(); + const [scannedImage, setScannedImage] = useState(); + const [scannerMode, setScannerMode] = useState(ScannerMode.FULL); const scanDocument = async () => { - // start the document scanner - const { scannedImages } = await DocumentScanner.scanDocument(); - - // check if undefined - if (scannedImages) { - // get back an array with scanned image file paths - if (scannedImages.length > 0) { - // set the img src, so we can view the first scanned image - setScannedImage(scannedImages[0]); - } + const options = Platform.OS === 'android' ? { scannerMode } : undefined; + const { scannedImages } = await DocumentScanner.scanDocument(options); + + if (scannedImages && scannedImages.length > 0) { + setScannedImage(scannedImages[0]); } }; - useEffect(() => { - // call scanDocument on load - scanDocument(); - }, []); - return ( - + + {Platform.OS === 'android' && ( + + Scanner mode + + {scannerModes.map(({ mode, label }) => ( + setScannerMode(mode)} + style={[ + styles.modeButton, + scannerMode === mode && styles.selectedModeButton, + ]} + > + + {label} + + + ))} + + + )} + + + Scan document + + + {scannedImage && ( + + )} + ); }; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 24, + backgroundColor: '#fff', + }, + controls: { + marginTop: 24, + }, + title: { + marginBottom: 8, + fontSize: 18, + fontWeight: '600', + }, + modeSelector: { + gap: 8, + }, + modeButton: { + padding: 12, + borderWidth: 1, + borderColor: '#777', + borderRadius: 6, + }, + selectedModeButton: { + backgroundColor: '#222', + borderColor: '#222', + }, + modeButtonText: { + textAlign: 'center', + color: '#222', + }, + selectedModeButtonText: { + color: '#fff', + }, + scanButton: { + marginTop: 16, + padding: 14, + borderRadius: 6, + backgroundColor: '#1976d2', + }, + scanButtonText: { + textAlign: 'center', + color: '#fff', + fontWeight: '600', + }, + image: { + flex: 1, + width: '100%', + marginTop: 24, + }, +});