Skip to content

Commit

Permalink
feat: MLKit Document Scanner (#121 by @frankcalise)
Browse files Browse the repository at this point in the history
* feat(doc-scanner): initial expo module commit

* package boilerplate updates

* doc scanner dep

* remove core js dep

* docs(changeset): Create MLKit document scanner

* wire up doc scanner to example app

* feat(document-scanner): wiring up the scanner intent

* fix(example): clean up doc scanner screen

* wire up dynamic options

* fix(document-scanner): wire up all option

* fix(document-scanner): WIP wiring up result data object

* fix(document-scanner): parse result uris from native

* test(document-scanner): placeholder test for ci

* chore(document-scanner): remove unused imports

* docs(document-scanner): basic docs

* feat(app): added doc scanner options

* fix(document-scanner): type exports

* docs: readme link to doc scanner and format options table

* fix(app): doc scanner import

* fix(demo-app): document scanner screenshot, locked to Android only demo

* chore(demo-app): lint fix
  • Loading branch information
frankcalise authored Mar 25, 2024
1 parent 0fd9ddb commit d7bf6ff
Show file tree
Hide file tree
Showing 32 changed files with 999 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/eight-books-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@infinitered/react-native-mlkit-document-scanner": patch
---

Create MLKit document scanner
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

RNMLKit provides Expo modules that allow developers to use MLKit native libraries in their Expo apps.

- [Document Scanner](https://https://infinitered.github.io/react-native-mlkit/document-scanner/getting-started/)
- [Face Detection](https://https://infinitered.github.io/react-native-mlkit/face-detection/getting-started/)
- [Image Labeling](https://github.com/infinitered/react-native-mlkit/tree/main/modules/react-native-mlkit-image-labeling)
- [Object Detection](https://github.com/infinitered/react-native-mlkit/tree/main/modules/react-native-mlkit-object-detection)
Expand Down
2 changes: 2 additions & 0 deletions apps/InfiniteRedAI/app/navigators/AppNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type AppStackParamList = {
FaceDetection: Record<string, never>
ImageLabeling: Record<string, never>
ObjectDetection: Record<string, never>
DocumentScanner: Record<string, never>
// IGNITE_GENERATOR_ANCHOR_APP_STACK_PARAM_LIST
}

Expand Down Expand Up @@ -66,6 +67,7 @@ const AppStack = observer(function AppStack() {
<Stack.Screen name="FaceDetection" component={Screens.FaceDetectionScreen} />
<Stack.Screen name="ImageLabeling" component={Screens.ImageLabelingScreen} />
<Stack.Screen name="ObjectDetection" component={Screens.ObjectDetectionScreen} />
<Stack.Screen name="DocumentScanner" component={Screens.DocumentScannerScreen} />
{/* IGNITE_GENERATOR_ANCHOR_APP_STACK_SCREENS */}
</Stack.Navigator>
)
Expand Down
171 changes: 171 additions & 0 deletions apps/InfiniteRedAI/app/screens/DocumentScannerScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import React, { FC } from "react"
import { observer } from "mobx-react-lite"
import { ViewStyle, View, ImageStyle, TextStyle } from "react-native"
import { NativeStackScreenProps } from "@react-navigation/native-stack"
import { AppStackScreenProps } from "../navigators"
import { Screen, Text, Icon, Button, ListItem, TextField, Toggle } from "../components"
import { useTypedNavigation } from "../navigators/useTypedNavigation"
import {
ResultFormatOptions,
ScannerModeOptions,
launchDocumentScannerAsync,
} from "@infinitered/react-native-mlkit-document-scanner"
import { spacing } from "app/theme"

interface DocumentScannerScreenProps
extends NativeStackScreenProps<AppStackScreenProps<"DocumentScanner">> {}

export const DocumentScannerScreen: FC<DocumentScannerScreenProps> = observer(
function DocumentScannerScreen() {
const navigation = useTypedNavigation<"ImageLabeling">()
const [result, setResult] = React.useState<string>("")
const [allowGallery, setAllowGallery] = React.useState<boolean>(false)
const [mode, setMode] = React.useState<ScannerModeOptions>(ScannerModeOptions.FULL)
const [resultFormat, setResultFormat] = React.useState<ResultFormatOptions>(
ResultFormatOptions.ALL,
)
const [pageLimit, setPageLimit] = React.useState<number>(1)

return (
<Screen
style={$root}
preset="scroll"
safeAreaEdges={["top", "bottom"]}
contentContainerStyle={{ paddingBottom: spacing.lg }}
>
<View>
<Icon icon={"back"} onPress={() => navigation.navigate("Home")} style={$backIcon} />
<Text preset={"heading"} text="Document Scanner" />
<Text style={$description}>
Configure scanner options and tap `Scan Document` to launch the modal
</Text>

<View style={{ gap: spacing.lg }}>
<ListItem
text="Page Limit"
textStyle={$optionText}
RightComponent={
<TextField
value={pageLimit.toString()}
onChangeText={(text) => {
// parse string to int and make sure if not or less than 1, set to 1
const num = parseInt(text)
setPageLimit(isNaN(num) || num < 1 ? 1 : num)
}}
containerStyle={$pageLimit}
textAlign="right"
/>
}
/>
<ListItem
text="Allow Gallery Import"
textStyle={$optionText}
RightComponent={
<Toggle
variant="switch"
value={allowGallery}
onPress={() => setAllowGallery(!allowGallery)}
containerStyle={$centerSwitch}
/>
}
/>
<ListItem
text="Mode"
textStyle={$optionText}
RightComponent={
<View style={$radioCol}>
<Toggle
variant="radio"
value={mode === ScannerModeOptions.BASE}
label="Base"
onPress={() => setMode(ScannerModeOptions.BASE)}
/>
<Toggle
variant="radio"
value={mode === ScannerModeOptions.BASE_WITH_FILTER}
label="Base w/ Filter"
onPress={() => setMode(ScannerModeOptions.BASE_WITH_FILTER)}
/>
<Toggle
variant="radio"
value={mode === ScannerModeOptions.FULL}
label="Full"
onPress={() => setMode(ScannerModeOptions.FULL)}
/>
</View>
}
/>
<ListItem
text="Result Formats"
textStyle={$optionText}
RightComponent={
<View style={$radioCol}>
<Toggle
variant="radio"
value={resultFormat === ResultFormatOptions.ALL}
label="All"
onPress={() => setResultFormat(ResultFormatOptions.ALL)}
/>
<Toggle
variant="radio"
value={resultFormat === ResultFormatOptions.PDF}
label="PDF"
onPress={() => setResultFormat(ResultFormatOptions.PDF)}
/>
<Toggle
variant="radio"
value={resultFormat === ResultFormatOptions.JPEG}
label="JPEG"
onPress={() => setResultFormat(ResultFormatOptions.JPEG)}
/>
</View>
}
/>

<Button
onPress={async () => {
const result = await launchDocumentScannerAsync({
pageLimit,
galleryImportAllowed: allowGallery,
resultFormats: resultFormat,
scannerMode: mode,
})
setResult(JSON.stringify(result))
}}
text="Scan Document"
/>
</View>
</View>

<Text style={$description}>Result: {result}</Text>
</Screen>
)
},
)

const $root: ViewStyle = {
flex: 1,
padding: 16,
display: "flex",
flexDirection: "column",
}
const $backIcon: ImageStyle = { marginVertical: 8 }

const $description: TextStyle = {
marginVertical: 8,
color: "rgba(0,0,0,0.6)",
}

const $optionText: TextStyle = {
alignSelf: "flex-start",
fontWeight: "bold",
}

const $radioCol: ViewStyle = {
width: "50%",
gap: spacing.md,
}

const $centerSwitch: ViewStyle = { alignSelf: "center" }

const $pageLimit: ViewStyle = { flex: 1 }
17 changes: 17 additions & 0 deletions apps/InfiniteRedAI/app/screens/HomeScreen/demoInfo.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Platform } from "react-native"
import { AppStackParamList } from "../../navigators"

export interface DemoInfo {
Expand All @@ -9,6 +10,21 @@ export interface DemoInfo {

const FACE_DETECTION = require("../../../assets/images/face-detection.jpg")
const FACE_HOLDER = require("../../../assets/images/welcome-face.png")
const DOCUMENT_SCANNER = require("../../../assets/images/doc-scanner.png")

const ANDROID_ONLY_DEMOS: DemoInfo[] = [
{
title: "Document Scanner",
description: "Quickly and easily digitize paper documents on Android",
screen: "DocumentScanner",
image: DOCUMENT_SCANNER,
},
]

const PLATFORM_SPECIFIC_DEMOS: DemoInfo[] = Platform.select({
android: ANDROID_ONLY_DEMOS,
default: [],
})

// List of available demos as a typescript object
export const DEMO_LIST: DemoInfo[] = [
Expand All @@ -30,4 +46,5 @@ export const DEMO_LIST: DemoInfo[] = [
screen: "ImageLabeling",
image: FACE_HOLDER,
},
...PLATFORM_SPECIFIC_DEMOS,
]
1 change: 1 addition & 0 deletions apps/InfiniteRedAI/app/screens/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export * from "./ErrorScreen/ErrorBoundary"
export * from "./HomeScreen"
export * from "./FaceDetectionScreen/FaceDetectionScreen"
export * from "./ImageLabelingScreen"
export * from "./DocumentScannerScreen"
export { BOX_COLORS } from "./FaceDetectionScreen"
export * from "./ObjectDetectionScreen"
Binary file added apps/InfiniteRedAI/assets/images/doc-scanner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/InfiniteRedAI/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@expo-google-fonts/space-grotesk": "^0.2.2",
"@expo/metro-config": "^0.7.1",
"@expo/webpack-config": "^18.0.1",
"@infinitered/react-native-mlkit-document-scanner": "^0.1.0",
"@infinitered/react-native-mlkit-face-detection": "^0.6.7",
"@infinitered/react-native-mlkit-image-labeling": "^0.5.7",
"@infinitered/react-native-mlkit-object-detection": "^0.6.7",
Expand Down
8 changes: 8 additions & 0 deletions docs/document-scanner/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"label": "Document Scanner (Beta)",
"position": 500,
"link": {
"type": "generated-index",
"description": "Scan documents with your camera"
}
}
53 changes: 53 additions & 0 deletions docs/document-scanner/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
sidebar_position: 1
title: Getting Started
---

# Document Scanner

## Getting Started

This is an expo module that lets you use
the [MLKit Document Scanner](https://developers.google.com/ml-kit/vision/doc-scanner) library in your Expo app.

## Installation

Install like any other npm package:

```bash
#yarn
yarn add react-native-mlkit-document-scanner

#npm
npm install react-native-mlkit-document-scanner
```

## Basic Usage

### 1. Launch the document scanner from a button press

Use the `launchDocumentScannerAsync` method to initiate the document scanner modal.

```tsx
// App.tsx
import { View, Button } from "react-native";
import { launchDocumentScannerAsync } from "react-native-mlkit-document-scanner";

function App() {
return (
<View>
<Button
onPress={async () => {
// result will contain an object with the result information
const result = await launchDocumentScannerAsync({
pageLimit: 1,
galleryImportAllowed: false,
resultFormats: ResultFormatOptions.ALL,
});
}}
title="Scan Document"
/>
</View>
);
}
```
31 changes: 31 additions & 0 deletions docs/document-scanner/options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
sidebar_position: 99
title: Options
---

# Document Scanner Options

A `DocumentScannerOptions` object is used to configure the behavior of the document scanner. This interface provides several optional properties that allow you to enable or disable certain features, set the mode of the scanner and choose the format for the results.

| Property | Type | Optional | Description |
|--:---------------------|--:--------------------|----------|--:----------------------------------------------------------------------|
| `pageLimit` | `number` | Yes | The maximum number of pages that can be scanned. |
| `galleryImportAllowed` | `boolean` | Yes | Allow the user to choose images from gallery or force a new photograph. |
| `scannerMode` | `ScannerModeOptions` | Yes | Sets the mode of the scanner. |
| `resultFormats` | `ResultFormatOptions` | Yes | Sets the format of the scanned document results. |



For more information on these options
see [the MLKit Docs](https://developers.google.com/ml-kit/vision/object-detection/ios#1.-configure-the-object-detector)

### Example Usage

```ts
const options: DocumentScannerOptions = {
pageLimit: 1,
galleryImportAllowed: false,
scannerMode: ScannerModeOptions.FULL,
resultFormats: ResultFormatOptions.ALL,
};
```
4 changes: 3 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,20 @@ Currently the following modules are available:
- [Face Detection](./face-detection)
- [Object Detection](./object-detection)
- [Image Labeling](./image-labeling)
- [Document Scanner](./document-scanner)

We will be adding more modules in the future, and especially welcome PRs that add support for new MLKit libraries! Check
out the contributing guide for more information on how to contribute.

## Installation

Each module is published as a separate npm package. For specific installation instructions for a particular module check
the followig pages:
the following pages:

- [Face Detection](./face-detection)
- [Object Detection](./object-detection)
- [Image Labeling](./image-labeling)
- [Document Scanner](./document-scanner)

## FAQ

Expand Down
8 changes: 8 additions & 0 deletions modules/react-native-mlkit-document-scanner/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
root: true,
extends: ["universe/native", "universe/web"],
ignorePatterns: ["build"],
rules: {
"@typescript-eslint/array-type": "off",
},
};
Loading

0 comments on commit d7bf6ff

Please sign in to comment.