diff --git a/.changeset/sour-wombats-invite.md b/.changeset/sour-wombats-invite.md new file mode 100644 index 00000000..741091d7 --- /dev/null +++ b/.changeset/sour-wombats-invite.md @@ -0,0 +1,6 @@ +--- +"@infinitered/react-native-mlkit-barcode-scanning": minor +"example-app": minor +--- + +add react native mlkit barcode scanning module diff --git a/apps/ExampleApp/app/navigators/AppNavigator.tsx b/apps/ExampleApp/app/navigators/AppNavigator.tsx index 1e355b3c..98405560 100644 --- a/apps/ExampleApp/app/navigators/AppNavigator.tsx +++ b/apps/ExampleApp/app/navigators/AppNavigator.tsx @@ -35,6 +35,7 @@ export type AppStackParamList = { ObjectDetection: Record DocumentScanner: Record TextRecognition: Record + BarcodeScanning: Record // IGNITE_GENERATOR_ANCHOR_APP_STACK_PARAM_LIST } @@ -63,6 +64,7 @@ const AppStack = observer(function AppStack() { + {/* IGNITE_GENERATOR_ANCHOR_APP_STACK_SCREENS */} ) diff --git a/apps/ExampleApp/app/screens/BarcodeScanningScreen.tsx b/apps/ExampleApp/app/screens/BarcodeScanningScreen.tsx new file mode 100644 index 00000000..3bd42249 --- /dev/null +++ b/apps/ExampleApp/app/screens/BarcodeScanningScreen.tsx @@ -0,0 +1,241 @@ +import React, { FC, useState, useEffect, useCallback } from "react" +import { observer } from "mobx-react-lite" +import { ViewStyle, View, ImageStyle, TextStyle, ScrollView, Pressable } from "react-native" +import { NativeStackScreenProps } from "@react-navigation/native-stack" +import { AppStackScreenProps } from "../navigators" +import { Text, Icon, ImageSelector, Screen } from "../components" +import { useTypedNavigation } from "../navigators/useTypedNavigation" + +import RNMLKitBarcodeScanningModule, { + BarcodeFormat, +} from "@infinitered/react-native-mlkit-barcode-scanning" +import type { + BarcodeScannerOptions, + BarcodeScannerResult, +} from "@infinitered/react-native-mlkit-barcode-scanning" +import { UseExampleImageStatus, SelectedImage } from "../utils/useExampleImage" + +type BarcodeScanningScreenProps = NativeStackScreenProps> + +function DebugOutput({ data }: { data: unknown }) { + const [expanded, setExpanded] = useState(false) + + return ( + + setExpanded(!expanded)} style={$debugHeader}> + {expanded ? "▼" : "▶"} Debug Output + + {expanded && ( + + + {JSON.stringify(data, null, 2)} + + + )} + + ) +} + +const DEFAULT_OPTIONS: BarcodeScannerOptions = { + barcodeFormats: [BarcodeFormat.ALL], + enableAllPotentialBarcodes: false, +} + +export const BarcodeScanningScreen: FC = observer( + function BarcodeScanningScreen() { + const navigation = useTypedNavigation<"BarcodeScanning">() + + const [image, setImage] = useState(null) + + const handleImageChange = useCallback((nextImage: SelectedImage) => { + setImage(nextImage) + }, []) + + const [result, setResult] = useState(null) + const [status, setStatus] = useState< + "init" | "noPermissions" | "done" | "error" | "loading" | UseExampleImageStatus + >("init") + + const onStatusChange = React.useCallback( + (status: "init" | "noPermissions" | "done" | "error" | "loading" | UseExampleImageStatus) => { + setStatus(status) + }, + [], + ) + + const [isScanning, setIsScanning] = useState(false) + + useEffect(() => { + const scanBarcode = async () => { + if (!image?.uri) return + setIsScanning(true) + try { + await RNMLKitBarcodeScanningModule.initialize(DEFAULT_OPTIONS) + const scanResult = await RNMLKitBarcodeScanningModule.process(image.uri) + setResult(scanResult) + setStatus("done") + } catch (error) { + console.error("Error scanning barcode:", error) + setStatus("error") + } finally { + setIsScanning(false) + } + } + + scanBarcode().then(() => null) + }, [image]) + + const statusMessage = React.useMemo(() => { + if (!image && status !== "init") { + setStatus("init") + } + if (isScanning) { + return "Scanning for barcodes..." + } + switch (status) { + case "init": + return "Take a photo or select one from your camera roll" + case "noPermissions": + return "You need to grant camera permissions to take a photo" + case "takingPhoto": + return "Taking photo..." + case "selectingPhoto": + return "Selecting photo..." + case "done": + return result?.barcodes.length + ? `Found ${result.barcodes.length} barcode${result.barcodes.length > 1 ? "s" : ""}!` + : "No barcodes found" + case "error": + return "Error during scanning!" + case "loading": + return "Loading Example Images..." + default: + return "" + } + }, [result, image, status, isScanning]) + + const clearResults = useCallback(() => { + setResult(null) + }, []) + + return ( + + + navigation.navigate("Home")} style={$backIcon} /> + + Take a photo of a barcode and scan it. + + + + {result && result.barcodes.length > 0 && ( + <> + + Scanned Barcodes + {result.barcodes.map((barcode, index) => ( + + Format: {barcode.format} + Type: {barcode.valueType} + {barcode.displayValue && ( + Value: {barcode.displayValue} + )} + {barcode.rawValue && barcode.rawValue !== barcode.displayValue && ( + Raw: {barcode.rawValue} + )} + + ))} + + + + )} + + ) + }, +) + +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 $resultContainer: ViewStyle = { + width: "100%", + borderWidth: 1, + borderColor: "rgba(0,0,0,0.2)", + borderRadius: 8, + padding: 12, + marginVertical: 16, +} + +const $barcodeItem: ViewStyle = { + marginTop: 12, + paddingTop: 12, + borderTopWidth: 1, + borderTopColor: "rgba(0,0,0,0.1)", +} + +const $barcodeLabel: TextStyle = { + fontSize: 12, + color: "rgba(0,0,0,0.6)", + marginBottom: 4, +} + +const $barcodeValue: TextStyle = { + fontSize: 16, + fontWeight: "600", + marginTop: 4, +} + +const $barcodeRaw: TextStyle = { + fontSize: 12, + fontFamily: "monospace", + marginTop: 4, + color: "rgba(0,0,0,0.5)", +} + +const $debugContainer: ViewStyle = { + width: "100%", + borderWidth: 1, + borderColor: "rgba(0,0,0,0.2)", + borderRadius: 8, + marginBottom: 24, + overflow: "hidden", +} + +const $debugHeader: ViewStyle = { + padding: 12, + backgroundColor: "rgba(0,0,0,0.05)", +} + +const $debugTitle: TextStyle = { + fontWeight: "bold", +} + +const $debugContent: ViewStyle = { + maxHeight: 300, + padding: 12, + backgroundColor: "rgba(0,0,0,0.02)", +} + +const $debugText: TextStyle = { + fontFamily: "monospace", + fontSize: 12, +} diff --git a/apps/ExampleApp/app/screens/HomeScreen/demoInfo.ts b/apps/ExampleApp/app/screens/HomeScreen/demoInfo.ts index 331dc0f2..eb4ca8f5 100644 --- a/apps/ExampleApp/app/screens/HomeScreen/demoInfo.ts +++ b/apps/ExampleApp/app/screens/HomeScreen/demoInfo.ts @@ -53,5 +53,11 @@ export const DEMO_LIST: DemoInfo[] = [ screen: "TextRecognition", image: TEXT_RECOGNITION, }, + { + title: "Barcode Scanning", + description: "Scan barcodes and QR codes from images", + screen: "BarcodeScanning", + image: FACE_HOLDER, + }, ...PLATFORM_SPECIFIC_DEMOS, ] diff --git a/apps/ExampleApp/app/screens/index.ts b/apps/ExampleApp/app/screens/index.ts index 52f2fb39..2f8d0276 100644 --- a/apps/ExampleApp/app/screens/index.ts +++ b/apps/ExampleApp/app/screens/index.ts @@ -7,4 +7,5 @@ export * from "./ImageLabelingScreen" export * from "./DocumentScannerScreen" export { BOX_COLORS } from "./FaceDetectionScreen" export * from "./ObjectDetectionScreen" -export * from "./TextRecognitionScreen" \ No newline at end of file +export * from "./TextRecognitionScreen" +export * from "./BarcodeScanningScreen" \ No newline at end of file diff --git a/apps/ExampleApp/package.json b/apps/ExampleApp/package.json index 0783ef09..a8688736 100644 --- a/apps/ExampleApp/package.json +++ b/apps/ExampleApp/package.json @@ -32,6 +32,7 @@ "dependencies": { "@expo-google-fonts/space-grotesk": "^0.2.2", "@expo/metro-runtime": "~6.1.2", + "@infinitered/react-native-mlkit-barcode-scanning": "workspace:^5.0.0", "@infinitered/react-native-mlkit-document-scanner": "workspace:^5.0.0", "@infinitered/react-native-mlkit-face-detection": "workspace:^5.0.0", "@infinitered/react-native-mlkit-image-labeling": "workspace:^5.0.0", diff --git a/docs/barcode-scanning/category.json b/docs/barcode-scanning/category.json new file mode 100644 index 00000000..333c8e38 --- /dev/null +++ b/docs/barcode-scanning/category.json @@ -0,0 +1,4 @@ +{ + "label": "Barcode Scanner", + "position": 100 + } \ No newline at end of file diff --git a/docs/barcode-scanning/index.md b/docs/barcode-scanning/index.md new file mode 100644 index 00000000..fb19c9ad --- /dev/null +++ b/docs/barcode-scanning/index.md @@ -0,0 +1,250 @@ +--- +sidebar_position: 1 +title: Getting Started +--- + +# Barcode Scanner + +## Getting Started + +This is an expo module that lets you use the [MLKit Barcode Scanning](https://developers.google.com/ml-kit/vision/barcode-scanning) library in your Expo app. It supports both Android and iOS platforms. + +## Installation + +Install like any other npm package: + +```bash +#yarn +yarn add @infinitered/react-native-mlkit-barcode-scanning + +#npm +npm install @infinitered/react-native-mlkit-barcode-scanning +``` + +## Basic Usage + +### 1. Initialize the barcode scanner + +Before scanning, you need to initialize the scanner with your desired [options](./options). + +```tsx +import RNMLKitBarcodeScanningModule, { + BarcodeFormat, +} from "@infinitered/react-native-mlkit-barcode-scanning"; +import type { BarcodeScannerOptions } from "@infinitered/react-native-mlkit-barcode-scanning"; + +const options: BarcodeScannerOptions = { + barcodeFormats: [BarcodeFormat.ALL], +}; + +await RNMLKitBarcodeScanningModule.initialize(options); +``` + +### 2. Process an image for barcodes + +Once initialized, pass an image path to the `process` method to scan for barcodes. + +```tsx +const result = await RNMLKitBarcodeScanningModule.process(imagePath); + +// result.barcodes contains an array of detected barcodes +for (const barcode of result.barcodes) { + console.log("Format:", barcode.format); + console.log("Value:", barcode.displayValue); + console.log("Type:", barcode.valueType); +} +``` + +### 3. Full example + +Here's a complete example showing how to scan barcodes from an image: + +```tsx +import React, { useState } from "react"; +import { View, Button, Text, Image } from "react-native"; +import * as ImagePicker from "expo-image-picker"; +import RNMLKitBarcodeScanningModule, { + BarcodeFormat, +} from "@infinitered/react-native-mlkit-barcode-scanning"; +import type { + BarcodeScannerOptions, + BarcodeScannerResult, +} from "@infinitered/react-native-mlkit-barcode-scanning"; + +const DEFAULT_OPTIONS: BarcodeScannerOptions = { + barcodeFormats: [BarcodeFormat.ALL], +}; + +function BarcodeScannerExample() { + const [result, setResult] = useState(null); + const [imageUri, setImageUri] = useState(null); + + const scanBarcode = async () => { + // Pick an image + const pickerResult = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + }); + + if (pickerResult.canceled) return; + + const uri = pickerResult.assets[0].uri; + setImageUri(uri); + + // Initialize and scan + await RNMLKitBarcodeScanningModule.initialize(DEFAULT_OPTIONS); + const scanResult = await RNMLKitBarcodeScanningModule.process(uri); + setResult(scanResult); + }; + + return ( + +