UNPKG

react-native-pdf-jsi

Version:

🚀 Ultra-fast React Native PDF viewer with JSI (JavaScript Interface) integration for maximum performance. Features lazy loading, smart caching, progressive loading, and zero-bridge overhead operations. Perfect for large PDF files with 30-day persistent c

763 lines (622 loc) • 26.9 kB
/** * Copyright (c) 2025-present, Enhanced PDF JSI JavaScript Bridge * All rights reserved. * * JavaScript interface for high-performance PDF operations via JSI * Provides direct access to native PDF functions without bridge overhead */ import { NativeModules, NativeEventEmitter, Platform } from 'react-native'; const { PDFJSIManager: PDFJSIManagerNative, EnhancedPdfJSIBridge, RNPDFPdfViewManager } = NativeModules; /** * Enhanced PDF JSI Manager * Provides high-performance PDF operations via JSI */ class PDFJSIManager { constructor() { this.isJSIAvailable = false; this.performanceMetrics = new Map(); this.cacheMetrics = new Map(); this.initializationPromise = null; this.initializeJSI(); } /** * Initialize JSI availability check */ async initializeJSI() { if (this.initializationPromise) { return this.initializationPromise; } this.initializationPromise = this.checkJSIAvailability(); return this.initializationPromise; } /** * Check if JSI is available */ async checkJSIAvailability() { try { let isAvailable = false; if (Platform.OS === 'android') { isAvailable = await PDFJSIManagerNative.isJSIAvailable(); } else if (Platform.OS === 'ios') { // For iOS, we use the native module methods directly isAvailable = await RNPDFPdfViewManager.checkJSIAvailability(); } else { console.log('📱 PDFJSI: Platform not supported:', Platform.OS); return false; } this.isJSIAvailable = isAvailable; console.log(`📱 PDFJSI: JSI availability on ${Platform.OS}: ${isAvailable ? 'AVAILABLE' : 'NOT AVAILABLE'}`); return isAvailable; } catch (error) { console.error('📱 PDFJSI: Error checking JSI availability:', error); this.isJSIAvailable = false; return false; } } /** * Render page directly via JSI (high-performance) * @param {string} pdfId - PDF identifier * @param {number} pageNumber - Page number to render * @param {number} scale - Render scale factor * @param {string} base64Data - Base64 encoded PDF data * @returns {Promise<Object>} Render result */ async renderPageDirect(pdfId, pageNumber, scale, base64Data) { if (!this.isJSIAvailable) { throw new Error('JSI not available - falling back to bridge mode'); } const startTime = performance.now(); try { console.log(`📱 PDFJSI: Rendering page ${pageNumber} at scale ${scale} for PDF ${pdfId}`); let result; if (Platform.OS === 'android') { result = await PDFJSIManagerNative.renderPageDirect(pdfId, pageNumber, scale, base64Data); } else if (Platform.OS === 'ios') { result = await RNPDFPdfViewManager.renderPageDirect(pdfId, pageNumber, scale, base64Data); } else { throw new Error(`Platform ${Platform.OS} not supported`); } const endTime = performance.now(); const renderTime = endTime - startTime; // Track performance this.trackPerformance('renderPageDirect', renderTime, { pdfId, pageNumber, scale, success: result.success }); console.log(`📱 PDFJSI: Page rendered in ${renderTime.toFixed(2)}ms`); return result; } catch (error) { const endTime = performance.now(); const renderTime = endTime - startTime; console.error(`📱 PDFJSI: Error rendering page in ${renderTime.toFixed(2)}ms:`, error); this.trackPerformance('renderPageDirect', renderTime, { pdfId, pageNumber, scale, success: false, error: error.message }); throw error; } } /** * Get page metrics via JSI * @param {string} pdfId - PDF identifier * @param {number} pageNumber - Page number * @returns {Promise<Object>} Page metrics */ async getPageMetrics(pdfId, pageNumber) { if (!this.isJSIAvailable) { throw new Error('JSI not available - falling back to bridge mode'); } try { console.log(`📱 PDFJSI: Getting metrics for page ${pageNumber} of PDF ${pdfId}`); let metrics; if (Platform.OS === 'android') { metrics = await PDFJSIManagerNative.getPageMetrics(pdfId, pageNumber); } else if (Platform.OS === 'ios') { metrics = await RNPDFPdfViewManager.getPageMetrics(pdfId, pageNumber); } else { throw new Error(`Platform ${Platform.OS} not supported`); } console.log(`📱 PDFJSI: Page metrics retrieved:`, metrics); return metrics; } catch (error) { console.error(`📱 PDFJSI: Error getting page metrics:`, error); throw error; } } /** * Preload pages directly via JSI * @param {string} pdfId - PDF identifier * @param {number} startPage - Start page number * @param {number} endPage - End page number * @returns {Promise<boolean>} Success status */ async preloadPagesDirect(pdfId, startPage, endPage) { if (!this.isJSIAvailable) { throw new Error('JSI not available - falling back to bridge mode'); } const startTime = performance.now(); try { console.log(`📱 PDFJSI: Preloading pages ${startPage}-${endPage} for PDF ${pdfId}`); let success; if (Platform.OS === 'android') { success = await PDFJSIManagerNative.preloadPagesDirect(pdfId, startPage, endPage); } else if (Platform.OS === 'ios') { success = await RNPDFPdfViewManager.preloadPagesDirect(pdfId, startPage, endPage); } else { throw new Error(`Platform ${Platform.OS} not supported`); } const endTime = performance.now(); const preloadTime = endTime - startTime; console.log(`📱 PDFJSI: Pages preloaded in ${preloadTime.toFixed(2)}ms, Success: ${success}`); this.trackPerformance('preloadPagesDirect', preloadTime, { pdfId, startPage, endPage, success }); return success; } catch (error) { const endTime = performance.now(); const preloadTime = endTime - startTime; console.error(`📱 PDFJSI: Error preloading pages in ${preloadTime.toFixed(2)}ms:`, error); throw error; } } /** * Get cache metrics via JSI * @param {string} pdfId - PDF identifier * @returns {Promise<Object>} Cache metrics */ async getCacheMetrics(pdfId) { if (!this.isJSIAvailable) { throw new Error('JSI not available - falling back to bridge mode'); } try { console.log(`📱 PDFJSI: Getting cache metrics for PDF ${pdfId}`); let metrics; if (Platform.OS === 'android') { metrics = await PDFJSIManagerNative.getCacheMetrics(pdfId); } else if (Platform.OS === 'ios') { metrics = await RNPDFPdfViewManager.getCacheMetrics(); } else { throw new Error(`Platform ${Platform.OS} not supported`); } // Cache the metrics this.cacheMetrics.set(pdfId, metrics); console.log(`📱 PDFJSI: Cache metrics retrieved:`, metrics); return metrics; } catch (error) { console.error(`📱 PDFJSI: Error getting cache metrics:`, error); throw error; } } /** * Clear cache directly via JSI * @param {string} pdfId - PDF identifier * @param {string} cacheType - Cache type to clear ('all', 'base64', 'bytes') * @returns {Promise<boolean>} Success status */ async clearCacheDirect(pdfId, cacheType = 'all') { if (!this.isJSIAvailable) { throw new Error('JSI not available - falling back to bridge mode'); } try { console.log(`📱 PDFJSI: Clearing cache type '${cacheType}' for PDF ${pdfId}`); let success; if (Platform.OS === 'android') { success = await PDFJSIManagerNative.clearCacheDirect(pdfId, cacheType); } else if (Platform.OS === 'ios') { success = await RNPDFPdfViewManager.clearCacheDirect(pdfId, cacheType); } else { throw new Error(`Platform ${Platform.OS} not supported`); } // Clear local cache metrics if (success) { this.cacheMetrics.delete(pdfId); } console.log(`📱 PDFJSI: Cache cleared, Success: ${success}`); return success; } catch (error) { console.error(`📱 PDFJSI: Error clearing cache:`, error); throw error; } } /** * Optimize memory via JSI * @param {string} pdfId - PDF identifier * @returns {Promise<boolean>} Success status */ async optimizeMemory(pdfId) { if (!this.isJSIAvailable) { throw new Error('JSI not available - falling back to bridge mode'); } try { console.log(`📱 PDFJSI: Optimizing memory for PDF ${pdfId}`); let success; if (Platform.OS === 'android') { success = await PDFJSIManagerNative.optimizeMemory(pdfId); } else if (Platform.OS === 'ios') { success = await RNPDFPdfViewManager.optimizeMemory(pdfId); } else { throw new Error(`Platform ${Platform.OS} not supported`); } console.log(`📱 PDFJSI: Memory optimization completed, Success: ${success}`); return success; } catch (error) { console.error(`📱 PDFJSI: Error optimizing memory:`, error); throw error; } } /** * Search text directly via JSI * @param {string} pdfId - PDF identifier * @param {string} searchTerm - Search term * @param {number} startPage - Start page number * @param {number} endPage - End page number * @returns {Promise<Array>} Search results */ async searchTextDirect(pdfId, searchTerm, startPage, endPage) { if (!this.isJSIAvailable) { throw new Error('JSI not available - falling back to bridge mode'); } const startTime = performance.now(); try { console.log(`📱 PDFJSI: Searching for '${searchTerm}' in pages ${startPage}-${endPage}`); let results; if (Platform.OS === 'android') { results = await PDFJSIManagerNative.searchTextDirect(pdfId, searchTerm, startPage, endPage); } else if (Platform.OS === 'ios') { results = await RNPDFPdfViewManager.searchTextDirect(pdfId, searchTerm); } else { throw new Error(`Platform ${Platform.OS} not supported`); } const endTime = performance.now(); const searchTime = endTime - startTime; console.log(`📱 PDFJSI: Search completed in ${searchTime.toFixed(2)}ms, Results: ${results.length}`); this.trackPerformance('searchTextDirect', searchTime, { pdfId, searchTerm, startPage, endPage, resultCount: results.length }); return results; } catch (error) { const endTime = performance.now(); const searchTime = endTime - startTime; console.error(`📱 PDFJSI: Error searching text in ${searchTime.toFixed(2)}ms:`, error); throw error; } } /** * Get performance metrics via JSI * @param {string} pdfId - PDF identifier * @returns {Promise<Object>} Performance metrics */ async getPerformanceMetrics(pdfId) { if (!this.isJSIAvailable) { throw new Error('JSI not available - falling back to bridge mode'); } try { console.log(`📱 PDFJSI: Getting performance metrics for PDF ${pdfId}`); let metrics; if (Platform.OS === 'android') { metrics = await PDFJSIManagerNative.getPerformanceMetrics(pdfId); } else if (Platform.OS === 'ios') { metrics = await RNPDFPdfViewManager.getPerformanceMetricsDirect(pdfId); } else { throw new Error(`Platform ${Platform.OS} not supported`); } console.log(`📱 PDFJSI: Performance metrics retrieved:`, metrics); return metrics; } catch (error) { console.error(`📱 PDFJSI: Error getting performance metrics:`, error); throw error; } } /** * Set render quality via JSI * @param {string} pdfId - PDF identifier * @param {number} quality - Render quality (1-3) * @returns {Promise<boolean>} Success status */ async setRenderQuality(pdfId, quality) { if (!this.isJSIAvailable) { throw new Error('JSI not available - falling back to bridge mode'); } if (quality < 1 || quality > 3) { throw new Error('Render quality must be between 1 and 3'); } try { console.log(`📱 PDFJSI: Setting render quality to ${quality} for PDF ${pdfId}`); let success; if (Platform.OS === 'android') { success = await PDFJSIManagerNative.setRenderQuality(pdfId, quality); } else if (Platform.OS === 'ios') { success = await RNPDFPdfViewManager.setRenderQuality(pdfId, quality); } else { throw new Error(`Platform ${Platform.OS} not supported`); } console.log(`📱 PDFJSI: Render quality set, Success: ${success}`); return success; } catch (error) { console.error(`📱 PDFJSI: Error setting render quality:`, error); throw error; } } /** * Get JSI performance statistics * @returns {Promise<Object>} JSI stats */ async getJSIStats() { try { console.log(`📱 PDFJSI: Getting JSI stats`); let stats; if (Platform.OS === 'android') { stats = await EnhancedPdfJSIBridge.getJSIStats(); } else if (Platform.OS === 'ios') { stats = await RNPDFPdfViewManager.getJSIStats(); } else { throw new Error(`Platform ${Platform.OS} not supported`); } console.log(`📱 PDFJSI: JSI stats retrieved:`, stats); return stats; } catch (error) { console.error(`📱 PDFJSI: Error getting JSI stats:`, error); throw error; } } /** * Track performance metrics * @private */ trackPerformance(operation, duration, metadata = {}) { const key = `${operation}_${Date.now()}`; this.performanceMetrics.set(key, { operation, duration, timestamp: Date.now(), metadata }); // Keep only last 100 performance entries if (this.performanceMetrics.size > 100) { const firstKey = this.performanceMetrics.keys().next().value; this.performanceMetrics.delete(firstKey); } } /** * Get all performance metrics * @returns {Array} Performance metrics array */ getPerformanceHistory() { return Array.from(this.performanceMetrics.values()); } /** * Clear performance history */ clearPerformanceHistory() { this.performanceMetrics.clear(); console.log('📱 PDFJSI: Performance history cleared'); } /** * Lazy load PDF pages for large files * @param {string} pdfId - PDF identifier * @param {number} currentPage - Current page number * @param {number} preloadRadius - Number of pages to preload around current page * @param {number} totalPages - Total number of pages in PDF * @returns {Promise<Object>} Lazy load result */ async lazyLoadPages(pdfId, currentPage, preloadRadius = 3, totalPages = null) { if (!this.isJSIAvailable) { throw new Error('JSI not available - falling back to bridge mode'); } const startTime = performance.now(); try { console.log(`📱 PDFJSI: Lazy loading pages around page ${currentPage} for PDF ${pdfId}`); // Calculate pages to preload const startPage = Math.max(1, currentPage - preloadRadius); const endPage = totalPages ? Math.min(totalPages, currentPage + preloadRadius) : currentPage + preloadRadius; // Preload pages in background const preloadResult = await this.preloadPagesDirect(pdfId, startPage, endPage); const endTime = performance.now(); const lazyLoadTime = endTime - startTime; // Track performance this.trackPerformance('lazyLoadPages', lazyLoadTime, { pdfId, currentPage, startPage, endPage, preloadRadius, success: preloadResult }); console.log(`📱 PDFJSI: Lazy loaded pages ${startPage}-${endPage} in ${lazyLoadTime.toFixed(2)}ms`); return { success: preloadResult, currentPage, preloadedRange: { startPage, endPage }, lazyLoadTime, preloadRadius }; } catch (error) { const endTime = performance.now(); const lazyLoadTime = endTime - startTime; console.error(`📱 PDFJSI: Error lazy loading pages in ${lazyLoadTime.toFixed(2)}ms:`, error); this.trackPerformance('lazyLoadPages', lazyLoadTime, { pdfId, currentPage, preloadRadius, success: false, error: error.message }); throw error; } } /** * Progressive loading for large PDF files * @param {string} pdfId - PDF identifier * @param {number} startPage - Starting page number * @param {number} batchSize - Number of pages to load in each batch * @param {Function} onProgress - Progress callback function * @returns {Promise<Object>} Progressive load result */ async progressiveLoadPages(pdfId, startPage = 1, batchSize = 5, onProgress = null) { if (!this.isJSIAvailable) { throw new Error('JSI not available - falling back to bridge mode'); } const startTime = performance.now(); try { console.log(`📱 PDFJSI: Progressive loading starting from page ${startPage} for PDF ${pdfId}`); let currentPage = startPage; let totalLoaded = 0; const loadResults = []; // Load pages in batches while (true) { const batchStartPage = currentPage; const batchEndPage = currentPage + batchSize - 1; console.log(`📱 PDFJSI: Loading batch ${batchStartPage}-${batchEndPage}`); const batchResult = await this.preloadPagesDirect(pdfId, batchStartPage, batchEndPage); if (!batchResult) { console.log(`📱 PDFJSI: Batch loading failed at page ${currentPage}`); break; } loadResults.push({ startPage: batchStartPage, endPage: batchEndPage, success: batchResult }); totalLoaded += batchSize; currentPage += batchSize; // Call progress callback if provided if (onProgress && typeof onProgress === 'function') { onProgress({ currentPage, totalLoaded, batchStartPage, batchEndPage, success: batchResult }); } // Small delay between batches to prevent blocking await new Promise(resolve => setTimeout(resolve, 100)); } const endTime = performance.now(); const progressiveLoadTime = endTime - startTime; // Track performance this.trackPerformance('progressiveLoadPages', progressiveLoadTime, { pdfId, startPage, batchSize, totalLoaded, batchesLoaded: loadResults.length }); console.log(`📱 PDFJSI: Progressive loading completed: ${totalLoaded} pages in ${progressiveLoadTime.toFixed(2)}ms`); return { success: true, totalLoaded, batchesLoaded: loadResults.length, loadResults, progressiveLoadTime }; } catch (error) { const endTime = performance.now(); const progressiveLoadTime = endTime - startTime; console.error(`📱 PDFJSI: Error in progressive loading in ${progressiveLoadTime.toFixed(2)}ms:`, error); this.trackPerformance('progressiveLoadPages', progressiveLoadTime, { pdfId, startPage, batchSize, success: false, error: error.message }); throw error; } } /** * Smart caching for frequently accessed pages * @param {string} pdfId - PDF identifier * @param {Array<number>} frequentPages - Array of frequently accessed page numbers * @returns {Promise<Object>} Smart cache result */ async smartCacheFrequentPages(pdfId, frequentPages = []) { if (!this.isJSIAvailable) { throw new Error('JSI not available - falling back to bridge mode'); } const startTime = performance.now(); try { console.log(`📱 PDFJSI: Smart caching ${frequentPages.length} frequent pages for PDF ${pdfId}`); const cacheResults = []; // Cache each frequent page for (const pageNumber of frequentPages) { try { const cacheResult = await this.preloadPagesDirect(pdfId, pageNumber, pageNumber); cacheResults.push({ pageNumber, success: cacheResult }); } catch (error) { console.warn(`📱 PDFJSI: Failed to cache page ${pageNumber}:`, error); cacheResults.push({ pageNumber, success: false, error: error.message }); } } const endTime = performance.now(); const smartCacheTime = endTime - startTime; const successfulCaches = cacheResults.filter(result => result.success).length; // Track performance this.trackPerformance('smartCacheFrequentPages', smartCacheTime, { pdfId, totalPages: frequentPages.length, successfulCaches, cacheResults }); console.log(`📱 PDFJSI: Smart caching completed: ${successfulCaches}/${frequentPages.length} pages cached in ${smartCacheTime.toFixed(2)}ms`); return { success: true, totalPages: frequentPages.length, successfulCaches, cacheResults, smartCacheTime }; } catch (error) { const endTime = performance.now(); const smartCacheTime = endTime - startTime; console.error(`📱 PDFJSI: Error in smart caching in ${smartCacheTime.toFixed(2)}ms:`, error); this.trackPerformance('smartCacheFrequentPages', smartCacheTime, { pdfId, totalPages: frequentPages.length, success: false, error: error.message }); throw error; } } } // Create singleton instance const pdfJSIManager = new PDFJSIManager(); export default pdfJSIManager; // Export individual methods for convenience export const { renderPageDirect, getPageMetrics, preloadPagesDirect, getCacheMetrics, clearCacheDirect, optimizeMemory, searchTextDirect, getPerformanceMetrics, setRenderQuality, getJSIStats, getPerformanceHistory, clearPerformanceHistory, lazyLoadPages, progressiveLoadPages, smartCacheFrequentPages } = pdfJSIManager;