UNPKG

react-native-native-doc-scanner

Version:

High-performance cross-platform document scanner with session-based crash recovery, native bridge architecture, and TurboModule compatibility

296 lines (262 loc) 8.94 kB
import { NativeModules, TurboModuleRegistry, Platform } from 'react-native'; import type { ScannerConfig, ScannerResult, ScanSuccessCallback, ScanErrorCallback, ScannerPromiseResult, ScannerCapabilities, NativeDocScannerInterface, CrashRecoveryResult, LastScanResult, } from './types'; import { ScannerError } from './types'; /** * Native Document Scanner Module * Provides cross-platform document scanning with automatic architecture detection */ class NativeDocumentScanner { private nativeModule: NativeDocScannerInterface | null = null; private isInitialized = false; private capabilities: ScannerCapabilities | null = null; constructor() { this.initializeModule(); } /** * Initialize the native module with architecture detection */ private initializeModule(): void { try { // Check if TurboModule Registry is available and functional (New Architecture) // @ts-ignore - global is available in React Native runtime if (global?.__turboModuleProxy != null && TurboModuleRegistry?.getEnforcing) { try { const turboModule = TurboModuleRegistry.getEnforcing('NativeDocScanner'); this.nativeModule = turboModule as NativeDocScannerInterface; console.log('[NativeDocScanner] Using New Architecture (TurboModule)'); this.capabilities = { isTurboModuleEnabled: true, features: this.getFeatures(), platform: Platform.OS as 'ios' | 'android', framework: this.getFramework(), }; } catch (error) { console.log('[NativeDocScanner] TurboModule failed, falling back to Legacy Bridge:', error); this.initializeLegacyBridge(); } } else { // Use Legacy Bridge (Old Architecture) this.initializeLegacyBridge(); } this.isInitialized = true; } catch (error) { console.error('[NativeDocScanner] Failed to initialize native module:', error); this.isInitialized = false; } } /** * Initialize legacy bridge module */ private initializeLegacyBridge(): void { const { NativeDocScanner } = NativeModules; if (!NativeDocScanner) { throw new Error( 'NativeDocScanner native module is not available. Make sure the library is properly linked.' ); } this.nativeModule = NativeDocScanner as NativeDocScannerInterface; console.log('[NativeDocScanner] Using Legacy Bridge Architecture'); this.capabilities = { isTurboModuleEnabled: false, features: this.getFeatures(), platform: Platform.OS as 'ios' | 'android', framework: this.getFramework(), }; } /** * Get platform-specific features */ private getFeatures() { return { galleryImport: Platform.OS === 'android', pdfGeneration: true, multiPageScan: true, imageFiltering: true, }; } /** * Get native framework being used */ private getFramework(): 'VisionKit' | 'MLKit' | 'Unknown' { if (Platform.OS === 'ios') { return 'VisionKit'; } else if (Platform.OS === 'android') { return 'MLKit'; } return 'Unknown'; } /** * Scan documents using callback-based API * @param config - Scanner configuration options * @param onSuccess - Callback for successful scanning * @param onError - Callback for errors */ public scanDocument( config: ScannerConfig, onSuccess: ScanSuccessCallback, onError: ScanErrorCallback, ): void { if (!this.isInitialized || !this.nativeModule) { onError(ScannerError.MODULE_NOT_FOUND); return; } // Validate configuration const validationError = this.validateConfig(config); if (validationError) { onError(validationError); return; } try { this.nativeModule.scanDocument( config, (result: string) => { try { const parsedResult: ScannerResult = JSON.parse(result); onSuccess(parsedResult); } catch (parseError) { console.error('[NativeDocScanner] Failed to parse result:', parseError); onError(ScannerError.UNKNOWN_ERROR); } }, (error: string) => { onError(error || ScannerError.UNKNOWN_ERROR); } ); } catch (error) { console.error('[NativeDocScanner] Native module call failed:', error); onError(ScannerError.UNKNOWN_ERROR); } } /** * Scan documents using Promise-based API * @param config - Scanner configuration options * @returns Promise with scan result */ public scanDocumentAsync(config: ScannerConfig): Promise<ScannerResult> { return new Promise((resolve, reject) => { this.scanDocument( config, (result: ScannerResult) => resolve(result), (error: string) => reject(new Error(error)) ); }); } /** * Get scanner capabilities and information * @returns Scanner capabilities object */ public getCapabilities(): ScannerCapabilities | null { return this.capabilities; } /** * Check if the native module is properly initialized * @returns True if module is ready for use */ public isReady(): boolean { return this.isInitialized && this.nativeModule !== null; } /** * Check for scan results from interrupted sessions (crash recovery) * Only returns results from the current interrupted scan session * @returns Promise resolving to recovery data or null if no pending results */ public async checkForCrashRecovery(): Promise<CrashRecoveryResult | null> { if (!this.isInitialized || !this.nativeModule) { throw new Error('Native module is not initialized'); } if (!this.nativeModule.checkForCrashRecovery) { console.warn('[NativeDocScanner] Crash recovery not supported in this version'); return null; } try { const result = await this.nativeModule.checkForCrashRecovery(); return result; } catch (error) { console.error('[NativeDocScanner] Failed to check for crash recovery:', error); throw error; } } /** * Get the last scan result (legacy method for backward compatibility) * @returns Promise resolving to last scan result or null */ public async getLastScanResult(): Promise<LastScanResult | null> { if (!this.isInitialized || !this.nativeModule) { throw new Error('Native module is not initialized'); } if (!this.nativeModule.getLastScanResult) { console.warn('[NativeDocScanner] getLastScanResult not supported in this version'); return null; } try { const result = await this.nativeModule.getLastScanResult(); return result; } catch (error) { console.error('[NativeDocScanner] Failed to get last scan result:', error); throw error; } } /** * Clear all cached scan data * Useful for testing or manual cleanup * @returns Promise resolving to true when cleared */ public async clearScanCache(): Promise<boolean> { if (!this.isInitialized || !this.nativeModule) { throw new Error('Native module is not initialized'); } if (!this.nativeModule.clearScanCache) { console.warn('[NativeDocScanner] clearScanCache not supported in this version'); return false; } try { const result = await this.nativeModule.clearScanCache(); return result; } catch (error) { console.error('[NativeDocScanner] Failed to clear scan cache:', error); throw error; } } /** * Validate scanner configuration * @param config - Configuration to validate * @returns Error message if invalid, null if valid */ private validateConfig(config: ScannerConfig): string | null { if (!config) { return 'Scanner configuration is required'; } if (typeof config.scannerMode !== 'number' || ![1, 2, 3].includes(config.scannerMode)) { return 'Invalid scanner mode. Must be 1 (FULL), 2 (BASE_WITH_FILTER), or 3 (BASE)'; } if (typeof config.isGalleryImportRequired !== 'boolean') { return 'isGalleryImportRequired must be a boolean'; } if (typeof config.pageLimit !== 'number' || (config.pageLimit !== -1 && (config.pageLimit < 1 || config.pageLimit > 50))) { return 'pageLimit must be a number between 1 and 50, or -1 for unlimited pages'; } if (config.maxSizeLimit !== undefined) { if (typeof config.maxSizeLimit !== 'number' || config.maxSizeLimit < 1) { return 'maxSizeLimit must be a positive number (in bytes)'; } } // Platform-specific validations if (Platform.OS === 'ios' && config.isGalleryImportRequired) { console.warn('[NativeDocScanner] Gallery import is not supported on iOS, ignoring setting'); } return null; } } // Create singleton instance const documentScanner = new NativeDocumentScanner(); export default documentScanner;