diff --git a/.gitignore b/.gitignore index 9fbaafb..07789d5 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ android/keystores/debug.keystore !.yarn/releases !.yarn/sdks !.yarn/versions +yarn.lock # Expo .expo/ diff --git a/CHANGELOG.md b/CHANGELOG.md index ff5f20d..f37c8f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,18 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.1.0] - 2025-08-26 +## [0.1.0] - 2025-08-27 ### Added - Initial release of React Native Image Code Scanner - Native implementation for iOS using Vision Framework - Native implementation for Android using ML Kit - Support for 13 barcode formats (QR Code, Code 128, Code 39, etc.) -- Advanced image preprocessing options: +- **Automatic image preprocessing** for optimal recognition: - Contrast enhancement - Grayscale conversion - - Automatic rotation attempts -- Platform-specific preprocessing overrides + - Multiple rotation attempts (0°, 90°, 180°, 270°) +- **Ultra-simple API** - just pass image path and formats, preprocessing is always automatic - Full support for React Native's New Architecture (Turbo Modules) - TypeScript support with complete type definitions - Comprehensive documentation and examples @@ -28,18 +28,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features - Lightweight and performant native implementation -- Smart retry logic with image preprocessing +- **Automatic preprocessing** enabled by default for best results +- Smart retry logic with multiple image enhancement techniques - No additional setup required for Android - Minimal iOS setup with just pod install - **Expo integration** with proper prebuild workflow - **Modern example app** using Expo Image Picker and StatusBar -- **Real-time configuration** of preprocessing options +- **Simplified API** - just pass image path and formats - **Performance metrics** and timing measurements ### Example App Features - Modern Expo-based example application - Barcode format selection UI with real-time toggles -- Enhanced preprocessing options with visual controls +- Automatic preprocessing info with optional disable switch - Improved error handling and user feedback - Comprehensive setup documentation - Support for both Expo Go (UI testing) and prebuild (full functionality) diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md new file mode 100644 index 0000000..b36fdb8 --- /dev/null +++ b/COMPATIBILITY.md @@ -0,0 +1,206 @@ +# React Native Image Code Scanner - Compatibility Guide + +## Version Compatibility Matrix + +### React Native Versions + +| React Native | Package Version | Architecture Support | Notes | +|-------------|-----------------|---------------------|--------| +| 0.70.x | ✅ 0.1.x | Old Architecture | Full support | +| 0.71.x | ✅ 0.1.x | Old Architecture | Full support | +| 0.72.x | ✅ 0.1.x | Old Architecture | Full support | +| 0.73.x | ✅ 0.1.x | Old & New Architecture | Full support | +| 0.74.x | ✅ 0.1.x | Old & New Architecture | Full support | +| 0.75.x | ✅ 0.1.x | Old & New Architecture | Full support | +| 0.76.x | ✅ 0.1.x | New Architecture (default) | Full support | +| 0.77.x | ✅ 0.1.x | New Architecture (default) | Full support | +| 0.78.x | ✅ 0.1.x | New Architecture (default) | Full support | +| **0.79.x** | ✅ **0.1.x** | **New Architecture (default)** | **Latest - Full support** | +| 0.80.x+ | 🔜 0.1.x | New Architecture | Ready when released | + +### React Versions + +| React Version | Compatibility | +|--------------|---------------| +| 17.x | ✅ Supported | +| 18.x | ✅ Supported (Recommended) | +| 19.x | ✅ Supported (Beta) | + +### Expo SDK Compatibility + +| Expo SDK | React Native | Package Version | Status | +|----------|-------------|-----------------|--------| +| SDK 49 | 0.72.x | ✅ 0.1.x | Supported (requires prebuild) | +| SDK 50 | 0.73.x | ✅ 0.1.x | Supported (requires prebuild) | +| SDK 51 | 0.74.x | ✅ 0.1.x | Supported (requires prebuild) | +| **SDK 52** | **0.79.x** | ✅ **0.1.x** | **Latest - Full support (requires prebuild)** | + +**Note**: This library requires prebuild for Expo projects as it uses native modules. + +## Platform Requirements + +### iOS Requirements + +- **Minimum iOS Version**: 13.4 +- **Xcode**: 14.0 or higher +- **Swift**: 5.0 or higher +- **Frameworks Required**: + - Vision Framework (iOS 11+) + - Core Image (iOS 5+) + - UIKit (iOS 2+) + - Core Graphics (iOS 2+) + +### Android Requirements + +- **Minimum SDK**: 21 (Android 5.0 Lollipop) +- **Target SDK**: 33+ (Android 13+) +- **Compile SDK**: 33+ +- **Kotlin**: 1.6.0+ +- **Gradle**: 7.0+ +- **Android Gradle Plugin**: 7.0+ +- **Dependencies**: + - Google ML Kit Barcode Scanning: 17.3.0+ + - AndroidX Camera Core: 1.3.1+ + +### Node.js Requirements + +- **Node.js**: >=18.0.0 (LTS recommended) +- **npm**: >=8.0.0 +- **Yarn**: >=1.22.0 or >=3.0.0 (Berry) + +## New Architecture Support + +### Turbo Modules (React Native 0.68+) + +This library is built as a Turbo Module and fully supports the New Architecture: + +```typescript +// Automatically uses Turbo Module when New Architecture is enabled +import ImageCodeScanner from 'react-native-image-code-scanner'; +``` + +### Fabric (React Native 0.68+) + +The library is compatible with Fabric renderer but doesn't include UI components, so Fabric configuration is not required. + +### Enabling New Architecture + +#### For React Native 0.70-0.75 + +```bash +# iOS +cd ios && RCT_NEW_ARCH_ENABLED=1 pod install + +# Android - in gradle.properties +newArchEnabled=true +``` + +#### For React Native 0.76+ + +New Architecture is enabled by default. To disable: + +```bash +# iOS +cd ios && RCT_NEW_ARCH_ENABLED=0 pod install + +# Android - in gradle.properties +newArchEnabled=false +``` + +## Migration Guide + +### From React Native 0.6x to 0.7x + +If you're upgrading from React Native 0.6x: + +1. Update peer dependencies: + ```json + { + "react": ">=17.0.0", + "react-native": ">=0.70.0" + } + ``` + +2. Update iOS deployment target to 13.4 or higher + +3. Update Android minSdkVersion to 21 or higher + +### From Old to New Architecture + +No code changes required! The library automatically detects and uses the appropriate architecture: + +```typescript +// Same API for both architectures +const results = await ImageCodeScanner.scan({ + path: imagePath, + formats: [BarcodeFormat.QR_CODE] +}); +``` + +## Testing Compatibility + +### Test Matrix + +Run tests across different React Native versions: + +```bash +# Test with specific React Native version +npx react-native init TestApp --version 0.79.2 +cd TestApp +npm install react-native-image-code-scanner +``` + +### Automated Testing + +The library is tested against: +- React Native 0.70.x (Old Architecture) +- React Native 0.75.x (Both Architectures) +- React Native 0.79.x (New Architecture) +- Latest React Native release + +## Known Issues and Workarounds + +### Issue: Build fails on React Native 0.70.x with New Architecture + +**Solution**: New Architecture requires React Native 0.68+. For 0.70.x, ensure Old Architecture is used. + +### Issue: Expo Go doesn't work + +**Solution**: This is expected. The library uses native modules that require prebuild: +```bash +npx expo prebuild +npx expo run:ios # or run:android +``` + +### Issue: TypeScript errors with React Native 0.7x + +**Solution**: Ensure you have the correct @types/react-native: +```bash +npm install --save-dev @types/react@^18.0.0 @types/react-native@^0.72.0 +``` + +## Support Policy + +- **Active Support**: Latest 3 minor versions of React Native +- **Security Updates**: Latest 6 minor versions of React Native +- **Best Effort**: Older versions on case-by-case basis + +## Reporting Compatibility Issues + +If you encounter compatibility issues: + +1. Check this compatibility guide +2. Search [existing issues](https://github.com/nguyenthanhan/react-native-image-code-scanner/issues) +3. Create a new issue with: + - React Native version + - Package version + - Platform (iOS/Android) + - Architecture (Old/New) + - Error messages/logs + +## Resources + +- [React Native Releases](https://github.com/facebook/react-native/releases) +- [React Native New Architecture](https://reactnative.dev/docs/new-architecture-intro) +- [Expo SDK Versions](https://docs.expo.dev/versions/latest/) +- [Package Changelog](./CHANGELOG.md) diff --git a/README.md b/README.md index b24f2bc..b276f26 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,17 @@

-A lightweight, high-performance React Native library for scanning QR codes and barcodes from images with advanced preprocessing options. Built with native performance in mind using iOS Vision Framework and Android ML Kit. +A lightweight, high-performance React Native library for scanning QR codes and barcodes from images with automatic preprocessing for optimal recognition. Built with native performance in mind using iOS Vision Framework and Android ML Kit. ## ✨ Features - **🚀 High Performance**: Native implementation using iOS Vision Framework and Google ML Kit - **📱 Cross-Platform**: Support for both iOS and Android - **🎯 Multiple Formats**: Supports 13+ barcode formats including QR, Code128, EAN, UPC, and more -- **🔧 Advanced Preprocessing**: Built-in image enhancement for better recognition rates +- **🔧 Automatic Preprocessing**: Built-in image enhancement automatically applied for better recognition - **⚡ New Architecture Ready**: Full support for React Native's New Architecture (Turbo Modules) - **📦 Lightweight**: Minimal dependencies, optimized bundle size -- **🔄 Smart Retry Logic**: Intelligent preprocessing with rotation, grayscale, and contrast enhancement +- **🔄 Smart Recognition**: Automatic rotation, grayscale conversion, and contrast enhancement - **🔒 Type Safe**: Full TypeScript support with comprehensive type definitions - **🛠️ Expo Compatible**: Works with Expo (requires prebuild for native functionality) @@ -50,7 +50,22 @@ A lightweight, high-performance React Native library for scanning QR codes and b | ITF | ✅ | ✅ | | Codabar | ✅ | ✅ | -## 🚀 Installation +## Compatibility + +| React Native Version | Package Version | Status | +|---------------------|-----------------|--------| +| 0.70.x - 0.74.x | ✅ 0.1.x | Fully Supported | +| 0.75.x - 0.79.x | ✅ 0.1.x | Fully Supported (including New Architecture) | +| 0.80.x+ | ✅ 0.1.x | Fully Supported (when released) | + +### Requirements +- **React Native**: >=0.70.0 +- **React**: >=17.0.0 +- **iOS**: 13.4+ +- **Android**: minSdkVersion 21+ +- **Node**: >=18 + +## Installation ```bash npm install react-native-image-code-scanner @@ -126,12 +141,12 @@ const scanQRCode = async (imagePath: string) => { }; ``` -### Advanced Usage with Preprocessing +### Advanced Usage with Multiple Formats ```typescript import ImageCodeScanner, { BarcodeFormat } from 'react-native-image-code-scanner'; -const scanWithPreprocessing = async (imagePath: string) => { +const scanMultipleFormats = async (imagePath: string) => { try { const results = await ImageCodeScanner.scan({ path: imagePath, @@ -140,12 +155,7 @@ const scanWithPreprocessing = async (imagePath: string) => { BarcodeFormat.CODE_128, BarcodeFormat.EAN_13, ], - // Enable all preprocessing options - preprocessing: { - enhanceContrast: true, // Try contrast enhancement - grayscale: true, // Try grayscale conversion - rotations: true, // Try multiple rotations (0°, 90°, 180°, 270°) - }, + // Automatic preprocessing is enabled by default for optimal recognition }); console.log('Found barcodes:', results); @@ -155,29 +165,16 @@ const scanWithPreprocessing = async (imagePath: string) => { }; ``` -### Platform-Specific Preprocessing +### How It Works -```typescript -const scanWithPlatformOverrides = async (imagePath: string) => { - const results = await ImageCodeScanner.scan({ - path: imagePath, - formats: [BarcodeFormat.QR_CODE], - preprocessing: { - enhanceContrast: true, - grayscale: true, - rotations: false, // Default for both platforms - }, - platformOverrides: { - ios: { - rotations: true, // Enable rotations only on iOS - }, - android: { - enhanceContrast: false, // Disable contrast enhancement on Android - }, - }, - }); -}; -``` +The scanner automatically applies multiple preprocessing techniques to maximize recognition: + +1. **Original Image**: First scan attempt +2. **Grayscale Conversion**: Improves detection in colored backgrounds +3. **Contrast Enhancement**: Better recognition in low-contrast images +4. **Rotation Attempts**: Tries 0°, 90°, 180°, and 270° rotations + +As soon as a barcode is detected with any technique, the result is returned immediately for optimal performance. ### With Image Picker @@ -197,7 +194,7 @@ const scanFromGallery = async () => { const scanResults = await ImageCodeScanner.scan({ path: imagePath, formats: [ImageCodeScanner.BarcodeFormat.QR_CODE], - preprocessing: true, // Enable all preprocessing + // Automatic preprocessing is enabled by default }); if (scanResults.length > 0) { @@ -226,7 +223,7 @@ const scanFromGallery = async () => { const scanResults = await ImageCodeScanner.scan({ path: imagePath, formats: [ImageCodeScanner.BarcodeFormat.QR_CODE], - preprocessing: true, // Enable all preprocessing + // Automatic preprocessing is enabled by default }); if (scanResults.length > 0) { @@ -241,13 +238,13 @@ const scanFromGallery = async () => { ```typescript import ImageCodeScanner, { BarcodeFormat } from 'react-native-image-code-scanner'; -// Scan with all preprocessing enabled and multiple formats +// Scan with automatic preprocessing and multiple formats const scanEverything = async (imagePath: string) => { try { const results = await ImageCodeScanner.scan({ path: imagePath, formats: Object.values(BarcodeFormat), // All supported formats - preprocessing: true, // Enable all preprocessing options + // Automatic preprocessing is enabled by default }); console.log(`Found ${results.length} barcodes:`, results); @@ -263,7 +260,7 @@ const scanEverything = async (imagePath: string) => { ### `ImageCodeScanner.scan(options)` -Scans an image for barcodes. +Scans an image for barcodes with automatic preprocessing enabled. #### Parameters @@ -276,22 +273,7 @@ Scans an image for barcodes. ```typescript interface ScanOptions { path: string; // Path to the image file - formats?: BarcodeFormat[]; // Array of barcode formats to detect - preprocessing?: PreprocessingOptions | boolean; // Preprocessing configuration - platformOverrides?: { // Platform-specific preprocessing overrides - android?: PreprocessingOptions | boolean; - ios?: PreprocessingOptions | boolean; - }; -} -``` - -#### PreprocessingOptions - -```typescript -interface PreprocessingOptions { - enhanceContrast?: boolean; // Enhance image contrast - grayscale?: boolean; // Convert to grayscale - rotations?: boolean; // Try multiple rotations + formats?: BarcodeFormat[]; // Array of barcode formats to detect (default: QR_CODE) } ``` @@ -321,13 +303,22 @@ enum BarcodeFormat { } ``` +### Automatic Preprocessing + +The library automatically applies the following preprocessing techniques for better recognition: + +- **Grayscale Conversion**: Converts images to grayscale for improved barcode detection +- **Contrast Enhancement**: Enhances image contrast to make barcodes more readable +- **Rotation Detection**: Tries multiple orientations (0°, 90°, 180°, 270°) automatically +- **Smart Retry Logic**: If initial scan fails, automatically tries with different preprocessing techniques + ## ⚡ Performance Tips 1. **🖼️ Image Size**: Large images may take longer to process. Consider resizing images before scanning if performance is critical. 2. **🎯 Format Selection**: Specify only the formats you need rather than scanning for all formats. -3. **🔧 Preprocessing**: While preprocessing can improve recognition rates, it also increases processing time. Use selectively based on your needs. +3. **🔧 Preprocessing**: Automatic preprocessing improves recognition rates but may increase processing time. The library optimizes this automatically. 4. **📱 Platform Differences**: iOS Vision Framework and Android ML Kit may have slight differences in recognition capabilities. Test on both platforms for critical use cases. diff --git a/eslint.config.mjs b/eslint.config.mjs index 16b00bb..0a6255b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,7 +2,6 @@ import { fixupConfigRules } from '@eslint/compat'; import { FlatCompat } from '@eslint/eslintrc'; import js from '@eslint/js'; import prettier from 'eslint-plugin-prettier'; -import { defineConfig } from 'eslint/config'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -14,16 +13,24 @@ const compat = new FlatCompat({ allConfig: js.configs.all, }); -export default defineConfig([ +export default [ { - extends: fixupConfigRules(compat.extends('@react-native', 'prettier')), - plugins: { prettier }, + ignores: ['node_modules/**', 'lib/**', 'example/**', 'coverage/**', 'build/**', '.yarn/**', '*.config.js', '*.config.mjs'], + }, + ...fixupConfigRules(compat.extends('@react-native')), + { + plugins: { + prettier, + }, rules: { 'react/react-in-jsx-scope': 'off', - 'prettier/prettier': 'error', + 'prettier/prettier': ['error', { + quoteProps: 'consistent', + singleQuote: true, + tabWidth: 2, + trailingComma: 'es5', + useTabs: false, + }], }, }, - { - ignores: ['node_modules/', 'lib/'], - }, -]); +]; diff --git a/example/App.tsx b/example/App.tsx index c16a61e..ed43b9d 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -1,4 +1,493 @@ -import React from 'react'; -import App from './src/App'; +import { useState } from 'react'; +import { + Text, + View, + StyleSheet, + ScrollView, + TouchableOpacity, + Alert, + ActivityIndicator, + Image, + SafeAreaView, + Platform, +} from 'react-native'; +import { StatusBar } from 'expo-status-bar'; +import * as ImagePicker from 'expo-image-picker'; +import ImageCodeScanner, { + BarcodeFormat, +} from 'react-native-image-code-scanner'; -export default App; +interface ScanResult { + data: string[]; + time: number; +} + +const BARCODE_FORMATS = [ + { key: BarcodeFormat.QR_CODE, label: 'QR Code', enabled: true }, + { key: BarcodeFormat.CODE_128, label: 'Code 128', enabled: false }, + { key: BarcodeFormat.CODE_39, label: 'Code 39', enabled: false }, + { key: BarcodeFormat.EAN_13, label: 'EAN-13', enabled: false }, + { key: BarcodeFormat.PDF_417, label: 'PDF417', enabled: false }, + { key: BarcodeFormat.DATA_MATRIX, label: 'Data Matrix', enabled: false }, +]; + +export default function App() { + const [selectedImage, setSelectedImage] = useState(null); + const [scanResult, setScanResult] = useState(null); + const [isScanning, setIsScanning] = useState(false); + const [selectedFormats, setSelectedFormats] = useState([ + BarcodeFormat.QR_CODE, + ]); + + const pickImageFromCamera = async () => { + const { status } = await ImagePicker.requestCameraPermissionsAsync(); + if (status !== 'granted') { + Alert.alert( + 'Permission Denied', + 'Camera permission is required to take photos' + ); + return; + } + + const result = await ImagePicker.launchCameraAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: false, + quality: 1, + }); + + if (!result.canceled && result.assets[0]) { + setSelectedImage(result.assets[0].uri); + setScanResult(null); + } + }; + + const pickImageFromGallery = async () => { + const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (status !== 'granted') { + Alert.alert( + 'Permission Denied', + 'Gallery permission is required to select photos' + ); + return; + } + + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: false, + quality: 1, + }); + + if (!result.canceled && result.assets[0]) { + setSelectedImage(result.assets[0].uri); + setScanResult(null); + } + }; + + const scanImage = async () => { + if (!selectedImage) { + Alert.alert('No Image', 'Please select an image first'); + return; + } + + setIsScanning(true); + setScanResult(null); + + try { + const startTime = Date.now(); + + const results = await ImageCodeScanner.scan({ + path: selectedImage, + formats: selectedFormats, + }); + + const endTime = Date.now(); + const scanTime = endTime - startTime; + + setScanResult({ + data: results, + time: scanTime, + }); + + if (results.length === 0) { + Alert.alert( + 'No Codes Found', + 'No barcodes were detected in the image. Try using a clearer image or different barcode formats.' + ); + } + } catch (err) { + Alert.alert( + 'Scan Error', + err instanceof Error ? err.message : 'Unknown error occurred' + ); + } finally { + setIsScanning(false); + } + }; + + const toggleFormat = (format: BarcodeFormat) => { + setSelectedFormats((prev) => { + if (prev.includes(format)) { + if (prev.length === 1) { + Alert.alert( + 'Format Required', + 'At least one format must be selected' + ); + return prev; + } + return prev.filter((f) => f !== format); + } else { + return [...prev, format]; + } + }); + }; + + return ( + + + + + 📷 Image Code Scanner + Expo Example App + + + {/* Image Selection */} + + 1. Select Image Source + + + 📸 Camera + + + 🖼️ Gallery + + + + {selectedImage && ( + + + Image loaded + + )} + + + {/* Format Selection */} + + 2. Select Barcode Formats + + {BARCODE_FORMATS.map((format) => ( + toggleFormat(format.key)} + > + + {format.label} + + + ))} + + + + {/* Preprocessing Info */} + + 3. Automatic Preprocessing + + The scanner automatically applies image enhancements for optimal recognition: + {"\n"}• Contrast enhancement + {"\n"}• Grayscale conversion + {"\n"}• Multiple rotation attempts (0°, 90°, 180°, 270°) + {"\n"}\nThese optimizations ensure the best possible barcode detection rates. + + + + {/* Scan Button */} + + {isScanning ? ( + + ) : ( + 🔍 Scan Image + )} + + + {/* Results */} + {scanResult && ( + + Results + + + ⏱️ Scan time: {scanResult.time}ms + + + Found: {scanResult.data.length} code + {scanResult.data.length !== 1 ? 's' : ''} + + + + {scanResult.data.length > 0 ? ( + + {scanResult.data.map((code, index) => ( + { + Alert.alert('Code Content', code); + }} + > + + Code {index + 1} + Tap to view + + + {code} + + + ))} + + ) : ( + + No codes detected + + Try using a clearer image or selecting different formats + + + )} + + )} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f5f5', + }, + scrollContent: { + padding: 16, + paddingBottom: 32, + }, + header: { + alignItems: 'center', + marginBottom: 24, + paddingTop: 8, + }, + title: { + fontSize: 32, + fontWeight: 'bold', + color: '#333', + }, + subtitle: { + fontSize: 16, + color: '#666', + marginTop: 4, + }, + section: { + backgroundColor: '#fff', + borderRadius: 16, + padding: 16, + marginBottom: 16, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 8, + elevation: 2, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '600', + marginBottom: 12, + color: '#333', + }, + buttonRow: { + flexDirection: 'row', + justifyContent: 'space-between', + gap: 12, + }, + button: { + flex: 1, + backgroundColor: '#007AFF', + padding: 14, + borderRadius: 12, + alignItems: 'center', + }, + buttonText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, + imageContainer: { + marginTop: 16, + alignItems: 'center', + }, + previewImage: { + width: '100%', + height: 200, + borderRadius: 12, + resizeMode: 'contain', + backgroundColor: '#f8f8f8', + }, + imagePath: { + fontSize: 12, + color: '#999', + marginTop: 8, + }, + formatGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 8, + }, + formatChip: { + paddingHorizontal: 14, + paddingVertical: 8, + borderRadius: 20, + backgroundColor: '#f0f0f0', + borderWidth: 2, + borderColor: 'transparent', + }, + formatChipSelected: { + backgroundColor: '#E3F2FD', + borderColor: '#007AFF', + }, + formatChipText: { + fontSize: 14, + color: '#666', + fontWeight: '500', + }, + formatChipTextSelected: { + color: '#007AFF', + }, + optionRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 10, + borderBottomWidth: 1, + borderBottomColor: '#f0f0f0', + }, + optionLabel: { + fontSize: 16, + color: '#333', + }, + preprocessingInfo: { + fontSize: 14, + color: '#666', + lineHeight: 20, + marginBottom: 12, + }, + warningText: { + fontSize: 13, + color: '#ff6b6b', + fontStyle: 'italic', + marginTop: 8, + }, + scanButton: { + backgroundColor: '#4CAF50', + padding: 18, + borderRadius: 14, + alignItems: 'center', + marginVertical: 8, + shadowColor: '#4CAF50', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 4, + }, + disabledButton: { + backgroundColor: '#ccc', + shadowOpacity: 0, + }, + scanButtonText: { + color: '#fff', + fontSize: 18, + fontWeight: 'bold', + }, + resultsSection: { + borderWidth: 2, + borderColor: '#4CAF50', + }, + resultMeta: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 12, + paddingBottom: 8, + borderBottomWidth: 1, + borderBottomColor: '#f0f0f0', + }, + scanTime: { + fontSize: 14, + color: '#666', + }, + resultCount: { + fontSize: 14, + color: '#666', + fontWeight: '600', + }, + resultItem: { + backgroundColor: '#f8f8f8', + padding: 14, + borderRadius: 10, + marginBottom: 10, + borderLeftWidth: 4, + borderLeftColor: '#007AFF', + }, + resultHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 6, + }, + resultLabel: { + fontSize: 12, + color: '#999', + fontWeight: '600', + textTransform: 'uppercase', + }, + tapHint: { + fontSize: 11, + color: '#007AFF', + }, + resultText: { + fontSize: 15, + color: '#333', + fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', + lineHeight: 20, + }, + noResultsContainer: { + alignItems: 'center', + padding: 20, + }, + noResults: { + fontSize: 16, + color: '#999', + textAlign: 'center', + marginBottom: 8, + }, + noResultsHint: { + fontSize: 14, + color: '#bbb', + textAlign: 'center', + }, +}); diff --git a/example/README.md b/example/README.md index 20edbf3..c4072ce 100644 --- a/example/README.md +++ b/example/README.md @@ -94,9 +94,9 @@ npm run build:android ## Preprocessing Options -- **Enhance Contrast**: Improves image contrast for better recognition -- **Grayscale**: Converts image to grayscale (often improves barcode detection) -- **Rotations**: Tries multiple orientations (0°, 90°, 180°, 270°) +- **Enhance Contrast**: Automatically improves image contrast for better recognition +- **Grayscale**: Automatically converts image to grayscale (often improves barcode detection) +- **Auto Rotations**: Automatically tries multiple orientations (0°, 90°, 180°, 270°) ## Supported Barcode Formats diff --git a/example/babel.config.js b/example/babel.config.js index 900d974..9d89e13 100644 --- a/example/babel.config.js +++ b/example/babel.config.js @@ -1,10 +1,4 @@ -const path = require('path'); -const { getConfig } = require('react-native-builder-bob/babel-config'); -const pkg = require('../package.json'); - -const root = path.resolve(__dirname, '..'); - -module.exports = function(api) { +module.exports = function (api) { api.cache(true); return { presets: ['babel-preset-expo'], diff --git a/example/package.json b/example/package.json index 187349c..577f7a9 100644 --- a/example/package.json +++ b/example/package.json @@ -17,17 +17,15 @@ }, "dependencies": { "expo": "~52.0.0", - "expo-status-bar": "~2.0.0", - "expo-image-picker": "~16.0.0", - "expo-permissions": "~18.0.0", + "expo-image-picker": "~16.0.3", + "expo-status-bar": "~2.0.1", "react": "18.3.1", - "react-native": "0.76.3", + "react-native": "0.79.2", "react-native-image-code-scanner": "file:../" }, "devDependencies": { "@babel/core": "^7.20.0", "@types/react": "~18.3.12", - "@types/react-native": "~0.76.0", "eslint": "^8.56.0", "typescript": "^5.3.0" }, diff --git a/example/src/App.tsx b/example/src/App.tsx index 27e2184..61d01d2 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { Text, View, @@ -12,7 +12,9 @@ import { Platform, Switch, } from 'react-native'; -import ImageCodeScanner, { BarcodeFormat } from 'react-native-image-code-scanner'; +import ImageCodeScanner, { + BarcodeFormat, +} from 'react-native-image-code-scanner'; import * as ImagePicker from 'expo-image-picker'; import { StatusBar } from 'expo-status-bar'; @@ -35,17 +37,16 @@ export default function App() { const [selectedImage, setSelectedImage] = useState(null); const [scanResult, setScanResult] = useState(null); const [isScanning, setIsScanning] = useState(false); - const [selectedFormats, setSelectedFormats] = useState([BarcodeFormat.QR_CODE]); - const [preprocessingOptions, setPreprocessingOptions] = useState({ - enhanceContrast: true, - grayscale: true, - rotations: true, - }); + const [selectedFormats, setSelectedFormats] = useState([ + BarcodeFormat.QR_CODE, + ]); const requestPermissions = async () => { - const { status: cameraStatus } = await ImagePicker.requestCameraPermissionsAsync(); - const { status: libraryStatus } = await ImagePicker.requestMediaLibraryPermissionsAsync(); - + const { status: cameraStatus } = + await ImagePicker.requestCameraPermissionsAsync(); + const { status: libraryStatus } = + await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (cameraStatus !== 'granted' || libraryStatus !== 'granted') { Alert.alert( 'Permissions Required', @@ -100,11 +101,11 @@ export default function App() { try { const startTime = Date.now(); - + const results = await ImageCodeScanner.scan({ path: selectedImage, formats: selectedFormats, - preprocessing: preprocessingOptions, + // Preprocessing is always enabled automatically for optimal results }); const endTime = Date.now(); @@ -119,7 +120,10 @@ export default function App() { Alert.alert('No Codes Found', 'No barcodes were detected in the image'); } } catch (err) { - Alert.alert('Scan Error', err instanceof Error ? err.message : 'Unknown error'); + Alert.alert( + 'Scan Error', + err instanceof Error ? err.message : 'Unknown error' + ); } finally { setIsScanning(false); } @@ -138,13 +142,13 @@ export default function App() { Select Image - handleImagePicker('camera')} > 📷 Camera - handleImagePicker('gallery')} > @@ -154,7 +158,10 @@ export default function App() { {selectedImage && ( - + )} @@ -162,7 +169,9 @@ export default function App() { {/* Barcode Format Selection */} Barcode Formats - Select formats to scan for: + + Select formats to scan for: + {BARCODE_FORMATS.map((format) => ( {format.label} @@ -170,9 +179,11 @@ export default function App() { value={selectedFormats.includes(format.key)} onValueChange={(value) => { if (value) { - setSelectedFormats(prev => [...prev, format.key]); + setSelectedFormats((prev) => [...prev, format.key]); } else { - setSelectedFormats(prev => prev.filter(f => f !== format.key)); + setSelectedFormats((prev) => + prev.filter((f) => f !== format.key) + ); } }} /> @@ -180,44 +191,26 @@ export default function App() { ))} - {/* Preprocessing Options */} + {/* Automatic Preprocessing Info */} - Preprocessing - Enable image enhancement options: - - - Enhance Contrast - - setPreprocessingOptions(prev => ({ ...prev, enhanceContrast: value })) - } - /> - + Automatic Preprocessing + + The following enhancements are automatically applied: + - - Grayscale - - setPreprocessingOptions(prev => ({ ...prev, grayscale: value })) - } - /> + + • Grayscale conversion for better barcode detection - - - Try Rotations - - setPreprocessingOptions(prev => ({ ...prev, rotations: value })) - } - /> + + • Contrast enhancement for improved readability + + + • Automatic rotation detection (0°, 90°, 180°, 270°) {/* Scan Button */} - Results Scan time: {scanResult.time}ms - + {scanResult.data.length > 0 ? ( {scanResult.data.map((code, index) => ( @@ -332,6 +325,9 @@ const styles = StyleSheet.create({ alignItems: 'center', paddingVertical: 8, }, + infoRow: { + paddingVertical: 4, + }, scanButton: { backgroundColor: '#4CAF50', padding: 16, diff --git a/ios/ImageCodeScanner.swift b/ios/ImageCodeScanner.swift index 3410062..d7b27c9 100644 --- a/ios/ImageCodeScanner.swift +++ b/ios/ImageCodeScanner.swift @@ -1 +1,286 @@ -c \ No newline at end of file +import Foundation +import React +import Vision +import UIKit +import CoreImage +import CoreGraphics + +@objc(ImageCodeScanner) +class ImageCodeScanner: NSObject { + + // MARK: - Image Preprocessing Methods + + private func convertToGrayscale(_ image: UIImage) -> UIImage? { + guard let currentCGImage = image.cgImage else { return nil } + let currentCIImage = CIImage(cgImage: currentCGImage) + + let filter = CIFilter(name: "CIColorMonochrome") + filter?.setValue(currentCIImage, forKey: "inputImage") + filter?.setValue(CIColor(red: 0.7, green: 0.7, blue: 0.7), forKey: "inputColor") + filter?.setValue(1.0, forKey: "inputIntensity") + + guard let outputImage = filter?.outputImage else { return nil } + + let context = CIContext(options: nil) + guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else { return nil } + + return UIImage(cgImage: cgImage) + } + + private func enhanceContrast(_ image: UIImage) -> UIImage? { + guard let currentCGImage = image.cgImage else { return nil } + let currentCIImage = CIImage(cgImage: currentCGImage) + + let filter = CIFilter(name: "CIColorControls") + filter?.setValue(currentCIImage, forKey: "inputImage") + filter?.setValue(2.0, forKey: "inputContrast") // Increase contrast + filter?.setValue(-0.2, forKey: "inputBrightness") // Slightly decrease brightness + filter?.setValue(1.0, forKey: "inputSaturation") + + guard let outputImage = filter?.outputImage else { return nil } + + let context = CIContext(options: nil) + guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else { return nil } + + return UIImage(cgImage: cgImage) + } + + private func rotateImage(_ image: UIImage, degrees: CGFloat) -> UIImage? { + let radians = degrees * CGFloat.pi / 180 + + var newSize = CGRect(origin: CGPoint.zero, size: image.size) + .applying(CGAffineTransform(rotationAngle: radians)) + .size + + newSize.width = floor(newSize.width) + newSize.height = floor(newSize.height) + + UIGraphicsBeginImageContextWithOptions(newSize, false, image.scale) + defer { UIGraphicsEndImageContext() } + + guard let context = UIGraphicsGetCurrentContext() else { return nil } + + context.translateBy(x: newSize.width / 2, y: newSize.height / 2) + context.rotate(by: radians) + + image.draw(in: CGRect( + x: -image.size.width / 2, + y: -image.size.height / 2, + width: image.size.width, + height: image.size.height + )) + + return UIGraphicsGetImageFromCurrentImageContext() + } + + @objc(scanFromPath:formats:options:resolver:rejecter:) + func scanFromPath(_ path: String, + formats: [String], + options: NSDictionary, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock) { + + print("ImageCodeScanner iOS - Starting scan with all preprocessing options enabled") + + // Use atomic flag to prevent multiple promise calls + var hasResolved = false + + func safeResolve(_ result: Any) { + if !hasResolved { + hasResolved = true + resolver(result) + } + } + + func safeReject(_ code: String, _ message: String, _ error: Error?) { + if !hasResolved { + hasResolved = true + rejecter(code, message, error) + } + } + + // Remove file:// prefix if present + let cleanPath = path.replacingOccurrences(of: "file://", with: "") + + guard let originalImage = UIImage(contentsOfFile: cleanPath) else { + safeReject("INVALID_IMAGE", "Cannot load image from path: \(path)", nil) + return + } + + // Prepare images to try - always try all preprocessing options + var imagesToTry: [(String, UIImage)] = [("Original", originalImage)] + + // Always add grayscale version + if let grayscaleImage = convertToGrayscale(originalImage) { + imagesToTry.append(("Grayscale", grayscaleImage)) + print("ImageCodeScanner iOS - Added grayscale version") + } + + // Always add contrast enhanced version + if let contrastImage = enhanceContrast(originalImage) { + imagesToTry.append(("Enhanced contrast", contrastImage)) + print("ImageCodeScanner iOS - Added contrast enhanced version") + } + + // Always add rotated versions + if let rotated90 = rotateImage(originalImage, degrees: 90) { + imagesToTry.append(("Rotated 90°", rotated90)) + } + if let rotated180 = rotateImage(originalImage, degrees: 180) { + imagesToTry.append(("Rotated 180°", rotated180)) + } + if let rotated270 = rotateImage(originalImage, degrees: 270) { + imagesToTry.append(("Rotated 270°", rotated270)) + } + print("ImageCodeScanner iOS - Added rotated versions") + + // Convert formats array to Vision symbologies + var symbologies: [VNBarcodeSymbology] = [] + + for format in formats { + switch format { + case "QR_CODE": + symbologies.append(.qr) + case "CODE_128": + symbologies.append(.code128) + case "CODE_39": + symbologies.append(.code39) + case "CODE_93": + symbologies.append(.code93) + case "EAN_13": + symbologies.append(.ean13) + case "EAN_8": + symbologies.append(.ean8) + case "UPC_A": + symbologies.append(.upce) // Vision uses UPCE for UPC-A + case "UPC_E": + symbologies.append(.upce) + case "PDF_417": + symbologies.append(.pdf417) + case "DATA_MATRIX": + symbologies.append(.dataMatrix) + case "AZTEC": + symbologies.append(.aztec) + case "ITF": + symbologies.append(.itf14) // ITF14 format + case "CODABAR": + symbologies.append(.codabar) + default: + break + } + } + + // If no formats specified, default to QR + if symbologies.isEmpty { + symbologies.append(.qr) + } + + // Function to try scanning with a specific image + func tryScanning(images: [(String, UIImage)], index: Int) { + guard index < images.count else { + // No more images to try, return empty result + print("ImageCodeScanner iOS - No barcodes found after trying all preprocessing options") + safeResolve([]) + return + } + + let (description, currentImage) = images[index] + print("ImageCodeScanner iOS - Attempting scan with: \(description)") + + guard let cgImage = currentImage.cgImage else { + // Try next image if this one fails + tryScanning(images: images, index: index + 1) + return + } + + // Create Vision request for barcode detection + let request = VNDetectBarcodesRequest { request, error in + DispatchQueue.main.async { + if let error = error { + print("ImageCodeScanner iOS - \(description) failed: \(error.localizedDescription)") + // Try next image + tryScanning(images: images, index: index + 1) + return + } + + guard let results = request.results as? [VNBarcodeObservation] else { + // Try next image + tryScanning(images: images, index: index + 1) + return + } + + // Extract barcode payload strings + let barcodeStrings = results.compactMap { observation in + observation.payloadStringValue + } + + if barcodeStrings.isEmpty { + print("ImageCodeScanner iOS - \(description): No barcodes found") + // Try next image + tryScanning(images: images, index: index + 1) + } else { + print("ImageCodeScanner iOS - Success with \(description)! Found \(barcodeStrings.count) codes") + safeResolve(barcodeStrings) + } + } + } + + #if targetEnvironment(simulator) + request.revision = VNDetectBarcodesRequestRevision1 + #endif + + // Configure supported symbologies + request.symbologies = symbologies + + // Create handler and perform request + let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) + + DispatchQueue.global(qos: .userInitiated).async { + do { + try handler.perform([request]) + } catch { + DispatchQueue.main.async { + print("ImageCodeScanner iOS - \(description) perform error: \(error.localizedDescription)") + // Try next image + tryScanning(images: images, index: index + 1) + } + } + } + } + + // Start scanning with the first image + tryScanning(images: imagesToTry, index: 0) + } + + // Fallback method for QR code detection using Core Image + private func tryQRCodeFallback(image: UIImage, + safeResolve: @escaping (Any) -> Void, + safeReject: @escaping (String, String, Error?) -> Void) { + + guard let ciImage = CIImage(image: image) else { + safeReject("INVALID_IMAGE", "Cannot create CIImage for fallback", nil) + return + } + + // Create QR code detector + let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [ + CIDetectorAccuracy: CIDetectorAccuracyHigh + ]) + + guard let detector = detector else { + safeReject("DETECTOR_ERROR", "Cannot create QR code detector", nil) + return + } + + // Detect QR codes + let features = detector.features(in: ciImage) + + // Extract QR code strings + let qrStrings = features.compactMap { feature -> String? in + guard let qrFeature = feature as? CIQRCodeFeature else { return nil } + return qrFeature.messageString + } + + safeResolve(qrStrings) + } +} diff --git a/package.json b/package.json index dcb705a..26e102c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-native-image-code-scanner", "version": "0.1.0", - "description": "A lightweight, high-performance React Native library for scanning QR codes and barcodes from images with advanced preprocessing options.", + "description": "A lightweight, high-performance React Native library for scanning QR codes and barcodes from images with automatic preprocessing for optimal recognition.", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", "exports": { @@ -82,29 +82,29 @@ "@eslint/eslintrc": "^3.3.0", "@eslint/js": "^9.22.0", "@evilmartians/lefthook": "^1.5.0", - "@react-native-community/cli": "15.0.0-alpha.2", + "@react-native-community/cli": "15.0.0", "@react-native/babel-preset": "0.79.2", - "@react-native/eslint-config": "^0.78.0", + "@react-native/eslint-config": "^0.79.2", "@release-it/conventional-changelog": "^9.0.2", "@types/jest": "^29.5.5", - "@types/react": "^19.0.0", + "@types/react": "^18.3.12", "commitlint": "^19.6.1", "del-cli": "^5.1.0", - "eslint": "^9.22.0", - "eslint-config-prettier": "^10.1.1", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.3", "jest": "^29.7.0", "prettier": "^3.0.3", - "react": "19.0.0", + "react": "18.3.1", "react-native": "0.79.2", "react-native-builder-bob": "^0.40.13", "release-it": "^17.10.0", "turbo": "^1.10.7", - "typescript": "^5.8.3" + "typescript": "^5.3.0" }, "peerDependencies": { - "react": "*", - "react-native": "*" + "react": ">=17.0.0", + "react-native": ">=0.70.0" }, "workspaces": [ "example" diff --git a/scripts/validate-package.js b/scripts/validate-package.js index 7afca94..270838f 100755 --- a/scripts/validate-package.js +++ b/scripts/validate-package.js @@ -3,7 +3,9 @@ const fs = require('fs'); const path = require('path'); -console.log('🔍 Validating React Native Image Code Scanner package structure...\n'); +console.log( + '🔍 Validating React Native Image Code Scanner package structure...\n' +); const requiredFiles = [ 'package.json', @@ -33,7 +35,7 @@ const errors = []; const warnings = []; // Check required files -requiredFiles.forEach(file => { +requiredFiles.forEach((file) => { const filePath = path.join(__dirname, '..', file); if (!fs.existsSync(filePath)) { errors.push(`❌ Missing required file: ${file}`); @@ -44,17 +46,37 @@ requiredFiles.forEach(file => { // Check package.json configuration try { - const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')); - - if (!packageJson.name) errors.push('❌ package.json: missing "name" field'); - if (!packageJson.version) errors.push('❌ package.json: missing "version" field'); - if (!packageJson.main) errors.push('❌ package.json: missing "main" field'); - if (!packageJson.types) errors.push('❌ package.json: missing "types" field'); - if (!packageJson.repository) warnings.push('⚠️ package.json: missing "repository" field'); - if (!packageJson.keywords || packageJson.keywords.length === 0) warnings.push('⚠️ package.json: missing or empty "keywords" field'); - if (!packageJson.peerDependencies) errors.push('❌ package.json: missing "peerDependencies" field'); - if (!packageJson.codegenConfig) warnings.push('⚠️ package.json: missing "codegenConfig" for New Architecture support'); - + const packageJson = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8') + ); + + if (!packageJson.name) { + errors.push('❌ package.json: missing "name" field'); + } + if (!packageJson.version) { + errors.push('❌ package.json: missing "version" field'); + } + if (!packageJson.main) { + errors.push('❌ package.json: missing "main" field'); + } + if (!packageJson.types) { + errors.push('❌ package.json: missing "types" field'); + } + if (!packageJson.repository) { + warnings.push('⚠️ package.json: missing "repository" field'); + } + if (!packageJson.keywords || packageJson.keywords.length === 0) { + warnings.push('⚠️ package.json: missing or empty "keywords" field'); + } + if (!packageJson.peerDependencies) { + errors.push('❌ package.json: missing "peerDependencies" field'); + } + if (!packageJson.codegenConfig) { + warnings.push( + '⚠️ package.json: missing "codegenConfig" for New Architecture support' + ); + } + console.log('✅ package.json structure validated'); } catch (error) { errors.push(`❌ Error reading package.json: ${error.message}`); @@ -62,7 +84,7 @@ try { // Check for unwanted files const unwantedDirs = ['modules', 'build', 'lib', '.turbo']; -unwantedDirs.forEach(dir => { +unwantedDirs.forEach((dir) => { if (fs.existsSync(path.join(__dirname, '..', dir))) { warnings.push(`⚠️ Found directory that should be cleaned: ${dir}`); } @@ -78,12 +100,12 @@ if (errors.length === 0 && warnings.length === 0) { } else { if (errors.length > 0) { console.log('\n❌ ERRORS (must fix before publishing):'); - errors.forEach(error => console.log(' ' + error)); + errors.forEach((error) => console.log(' ' + error)); } - + if (warnings.length > 0) { console.log('\n⚠️ WARNINGS (recommended to fix):'); - warnings.forEach(warning => console.log(' ' + warning)); + warnings.forEach((warning) => console.log(' ' + warning)); } } diff --git a/src/NativeImageCodeScanner.ts b/src/NativeImageCodeScanner.ts index bbffd3d..00ffab6 100644 --- a/src/NativeImageCodeScanner.ts +++ b/src/NativeImageCodeScanner.ts @@ -1,8 +1,16 @@ -import type {TurboModule} from 'react-native'; -import {TurboModuleRegistry} from 'react-native'; +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; export interface Spec extends TurboModule { - scanFromPath(path: string, formats: string[], options: {enhanceContrast: boolean, convertToGrayscale: boolean, tryRotations: boolean}): Promise; + scanFromPath( + path: string, + formats: string[], + options: { + enhanceContrast: boolean; + convertToGrayscale: boolean; + tryRotations: boolean; + } + ): Promise; } export default TurboModuleRegistry.getEnforcing('ImageCodeScanner'); diff --git a/src/index.tsx b/src/index.tsx index ae98371..a3848e4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,3 @@ -import {Platform} from 'react-native'; - import ImageCodeScanner from './NativeImageCodeScanner'; // Common formats supported by both iOS Vision and Android ML Kit @@ -19,146 +17,27 @@ export enum BarcodeFormat { CODABAR = 'CODABAR', } -/** - * Scan options for the image code scanner - * @param {string} path - The path to the image to scan - * @param {BarcodeFormat[]} formats - The formats to scan for - * @param {AndroidOptions} android - Android-specific options - * @param {IosOptions} ios - iOS-specific options - */ -/** - * Preprocessing options for image enhancement - */ -export interface PreprocessingOptions { - /** - * Try scanning with contrast-enhanced version if initial scan fails - * Android: Uses Bitmap manipulation | iOS: Uses Core Image filters - */ - enhanceContrast?: boolean; - /** - * Try scanning with grayscale version if initial scan fails - * Android: Uses Bitmap conversion | iOS: Uses Core Image filters - */ - grayscale?: boolean; - /** - * Try scanning with rotated versions (90°, 180°, 270°) if initial scan fails - * Android: Uses Matrix rotation | iOS: Uses Core Graphics - */ - rotations?: boolean; -} - export interface ScanOptions { path: string; formats?: BarcodeFormat[]; - - /** - * Preprocessing options applied to both platforms - * Set to false to disable all preprocessing - */ - preprocessing?: PreprocessingOptions | boolean; - - /** - * Platform-specific preprocessing overrides - * These override the general preprocessing settings for specific platforms - */ - platformOverrides?: { - android?: PreprocessingOptions | boolean; - ios?: PreprocessingOptions | boolean; - }; -} - -// Helper function to resolve preprocessing options -function resolvePreprocessingOptions( - preprocessing?: PreprocessingOptions | boolean, - platformOverrides?: { - android?: PreprocessingOptions | boolean; - ios?: PreprocessingOptions | boolean; - }, -): PreprocessingOptions { - const isIOS = Platform.OS === 'ios'; - const platformOverride = isIOS - ? platformOverrides?.ios - : platformOverrides?.android; - - // If platform override is explicitly false, disable all preprocessing - if (platformOverride === false) { - return { - enhanceContrast: false, - grayscale: false, - rotations: false, - }; - } - - // If platform override is an object, use it - if (typeof platformOverride === 'object' && platformOverride !== null) { - return { - enhanceContrast: platformOverride.enhanceContrast ?? false, - grayscale: platformOverride.grayscale ?? false, - rotations: platformOverride.rotations ?? false, - }; - } - - // If general preprocessing is false, disable all - if (preprocessing === false) { - return { - enhanceContrast: false, - grayscale: false, - rotations: false, - }; - } - - // If general preprocessing is true, enable all - if (preprocessing === true) { - return { - enhanceContrast: true, - grayscale: true, - rotations: true, - }; - } - - // Use general preprocessing options if provided - if (typeof preprocessing === 'object' && preprocessing !== null) { - return { - enhanceContrast: preprocessing.enhanceContrast ?? false, - grayscale: preprocessing.grayscale ?? false, - rotations: preprocessing.rotations ?? false, - }; - } - - // Default: no preprocessing - return { - enhanceContrast: false, - grayscale: false, - rotations: false, - }; } const ImageCodeScannerModule = { scan: (options: ScanOptions): Promise => { - const { - path, - formats = [BarcodeFormat.QR_CODE], - preprocessing, - platformOverrides, - } = options; + const { path, formats = [BarcodeFormat.QR_CODE] } = options; - // Resolve preprocessing options based on platform and overrides - const resolvedOptions = resolvePreprocessingOptions( - preprocessing, - platformOverrides, - ); - - // Map to native module format + // Note: Preprocessing is always enabled in native implementation + // The native code automatically tries multiple preprocessing techniques const nativeOptions = { - enhanceContrast: resolvedOptions.enhanceContrast ?? false, - convertToGrayscale: resolvedOptions.grayscale ?? false, - tryRotations: resolvedOptions.rotations ?? false, + enhanceContrast: true, + convertToGrayscale: true, + tryRotations: true, }; return ImageCodeScanner.scanFromPath( path, - formats.map(f => f.toString()), - nativeOptions, + formats.map((f) => f.toString()), + nativeOptions ); }, BarcodeFormat,