VOOZH about

URL: https://dev.to/yushulx/how-to-build-a-react-native-document-scanner-with-auto-detection-crop-and-export-32dd

⇱ How to Build a React Native Document Scanner with Auto-Detection, Crop, and Export - DEV Community


Turning a phone camera into a reliable document scanner requires accurate edge detection, perspective correction, and clean image output — all in real time. The Dynamsoft Capture Vision React Native SDK handles the heavy lifting, letting you ship a cross-platform document scanner for Android and iOS without writing any native code yourself.

What you'll build: A React Native app that auto-detects document edges from the live camera feed, lets users fine-tune the crop with draggable corner handles, apply color modes (full color, grayscale, binary), and export the result as a high-quality PNG.

Demo Video: React Native Document Scanner in Action

Prerequisites

Before starting, make sure you have:

  • Node.js 18+ and npm
  • React Native CLI (not Expo) with React Native 0.79+
  • Android Studio with an emulator or physical device (Android)
  • Xcode 15+ with CocoaPods (iOS)
  • A Dynamsoft Capture Vision trial or full license key

Get a 30-day free trial license at dynamsoft.com/customer/license/trialLicense

Step 1: Create the React Native Project and Install Dependencies

Start by creating a new React Native project (or use an existing one) and installing the required packages:

npx @react-native-community/cli init ScanDocument
cd ScanDocument
npm install dynamsoft-capture-vision-react-native @react-navigation/native @react-navigation/native-stack react-native-safe-area-context react-native-screens react-native-fs

For iOS, install the native pods:

cd ios && pod install && cd ..

The key dependency is dynamsoft-capture-vision-react-native (v3.4.1000), which bundles the camera enhancer, capture vision router, and document normalizer into a single React Native package.

Step 2: Initialize the License and Configure Navigation

The app entry point registers the root component in index.js:

import {AppRegistry} from 'react-native';
import App from './src/App';
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => App);

In App.tsx, set up a stack navigator with four screens and initialize the Dynamsoft license when the home screen mounts:

import {Quadrilateral, ImageData, LicenseManager} from 'dynamsoft-capture-vision-react-native';
import {createNativeStackNavigator, NativeStackScreenProps} from '@react-navigation/native-stack';
import {NavigationContainer} from '@react-navigation/native';

export type ScreenNames = ['Home', 'Scanner', 'Editor', 'NormalizedImage'];
export type RootStackParamList = Record<ScreenNames[number], undefined>;
export type StackNavigation = NativeStackScreenProps<RootStackParamList>;

const Stack = createNativeStackNavigator<RootStackParamList>();

function App(): React.JSX.Element {
 return (
 <SafeAreaProvider>
 <NavigationContainer>
 <Stack.Navigator initialRouteName="Home">
 <Stack.Screen name="Home" component={HomeScreen} options={{headerShown: false}} />
 <Stack.Screen name="Scanner" component={Scanner} options={{headerShown: false}} />
 <Stack.Screen name="Editor" component={Editor}
 options={{title: 'Adjust & Crop', headerStyle: {backgroundColor: '#2563EB'}, headerTintColor: '#fff'}} />
 <Stack.Screen name="NormalizedImage" component={NormalizedImage}
 options={{title: 'Review & Export', headerStyle: {backgroundColor: '#2563EB'}, headerTintColor: '#fff'}} />
 </Stack.Navigator>
 </NavigationContainer>
 </SafeAreaProvider>
 );
}

License initialization happens inside the HomeScreen component. Replace the license string with your own key:

useEffect(() => {
 LicenseManager.initLicense('LICENSE-KEY')
 .then(() => setLicenseReady(true))
 .catch(e => {
 console.error('Init license failed: ' + e.message);
 setError('License initialization failed.\n' + e.message);
 setLicenseReady(true);
 });
}, []);

Step 3: Detect and Capture Documents from the Camera Feed

The Scanner screen opens the camera, runs real-time document detection, and auto-captures when a stable document boundary is confirmed. The SDK's CameraEnhancer, CaptureVisionRouter, and MultiFrameResultCrossFilter work together:

import {
 CameraEnhancer,
 CameraView,
 CaptureVisionRouter,
 EnumCapturedResultItemType,
 EnumCrossVerificationStatus,
 EnumPresetTemplate,
 MultiFrameResultCrossFilter,
} from 'dynamsoft-capture-vision-react-native';

Open the camera when the screen is focused and close it when it loses focus:

const cameraRef = useRef<CameraEnhancer>(CameraEnhancer.getInstance());
const cvrRef = useRef<CaptureVisionRouter>(CaptureVisionRouter.getInstance());

useFocusEffect(
 React.useCallback(() => {
 const camera = cameraRef.current;
 camera.open();
 return () => {
 camera.close();
 };
 }, []),
);

Wire the camera to the capture vision router and enable cross-frame verification to filter out false positives:

if (!sdkInitialized) {
 cvr.setInput(camera);
 const filter = new MultiFrameResultCrossFilter();
 filter.enableResultCrossVerification(EnumCapturedResultItemType.CRIT_DESKEWED_IMAGE, true);
 cvr.addFilter(filter);
 sdkInitialized = true;
}

Register a result receiver that fires when a deskewed document image is ready. The capture triggers either through cross-verification passing or a manual shutter tap:

receiverRef.current = cvr.addResultReceiver({
 onProcessedDocumentResultReceived: result => {
 if (
 result.deskewedImageResultItems &&
 result.deskewedImageResultItems.length > 0 &&
 (ifBtnClick.current || result.deskewedImageResultItems[0].crossVerificationStatus === EnumCrossVerificationStatus.CVS_PASSED)
 ) {
 ifBtnClick.current = false;
 global.originalImage = cvr.getIntermediateResultManager().getOriginalImage(result.originalImageHashId) as ImageData;
 global.deskewedImage = result.deskewedImageResultItems[0].imageData;
 global.sourceDeskewQuad = result.deskewedImageResultItems[0].sourceDeskewQuad;
 if (global.originalImage.width > 0 && global.originalImage.height > 0) {
 navigation.navigate('NormalizedImage');
 }
 }
 },
});

cvr.startCapturing(EnumPresetTemplate.PT_DETECT_AND_NORMALIZE_DOCUMENT);

Start capturing with the built-in PT_DETECT_AND_NORMALIZE_DOCUMENT template — no custom template configuration needed.

Step 4: Fine-Tune the Document Crop with Draggable Corners

The Editor screen uses ImageEditorView to display the original image with a draggable quadrilateral overlay. Users drag the corner handles to fine-tune the document boundary before confirming:

import {
 EnumDrawingLayerId,
 ImageData,
 ImageEditorView,
 ImageProcessor,
} from 'dynamsoft-capture-vision-react-native';

export function Editor({navigation}: StackNavigation) {
 const editorView = useRef<ImageEditorView>(null);

 useEffect(() => {
 if (editorView.current) {
 editorView.current.setOriginalImage(global.originalImage);
 editorView.current.setQuads([global.sourceDeskewQuad], EnumDrawingLayerId.DDN_LAYER_ID);
 }
 }, []);

When the user confirms, extract the selected quad and re-deskew the image:

const getSelectedQuadAndDeskew = async (): Promise<ImageData | null | undefined> => {
 if (!editorView.current) {
 return null;
 }
 const quad = await editorView.current.getSelectedQuad().catch(e => {
 console.error('getSelectedQuad error: ' + e.message);
 return null;
 });
 if (quad) {
 global.sourceDeskewQuad = quad;
 return new ImageProcessor().cropAndDeskewImage(global.originalImage, quad);
 } else {
 Alert.alert('No selection', 'Please select a quad to confirm.');
 return null;
 }
};

The ImageProcessor.cropAndDeskewImage() method applies perspective correction based on the four-corner quadrilateral, producing a clean, rectangular document image.

Step 5: Apply Color Modes and Export the Document as PNG

👁 React Native Document Scanner

The NormalizedImage screen displays the deskewed result and provides three actions: edit (re-open the quad editor), change color mode, and export.

Convert between color, grayscale, and binary outputs using ImageProcessor:

import {
 ImageIO,
 ImageProcessor,
 imageDataToBase64,
} from 'dynamsoft-capture-vision-react-native';

const changeColorMode = (mode: string) => {
 if (global.showingImage && global.showingImage !== global.deskewedImage) {
 global.showingImage.release();
 }
 switch (mode) {
 case ColorMode.color:
 global.showingImage = global.deskewedImage;
 break;
 case ColorMode.grayscale:
 global.showingImage = new ImageProcessor().convertToGray(global.deskewedImage) ?? global.deskewedImage;
 break;
 case ColorMode.binary:
 global.showingImage =
 new ImageProcessor().convertToBinaryLocal(
 global.deskewedImage,
 /*blockSize = */ 0,
 /*compensation = */ 10,
 /*invert = */ false,
 ) ?? global.deskewedImage;
 break;
 }
 setBase64(imageDataToBase64(global.showingImage) ?? '');
};

Export the current image as a PNG using ImageIO.saveToFile(), writing to the platform-appropriate directory:

import {
 ExternalCachesDirectoryPath,
 TemporaryDirectoryPath,
} from 'react-native-fs';

const imageIO = new ImageIO();
const savedPath =
 (Platform.OS === 'ios'
 ? TemporaryDirectoryPath
 : ExternalCachesDirectoryPath) + `/document_${Date.now()}.png`;
imageIO.saveToFile(global.showingImage, savedPath, true);
Alert.alert('Saved ✓', 'Image saved to:\n' + savedPath);

Source Code

https://github.com/yushulx/android-camera-barcode-mrz-document-scanner/tree/main/examples/react-native-document-scanner