import {Image, ImageSourcePropType, Platform} from 'react-native' import RNFS from 'react-native-fs' // URL download cache: Map const urlCache = new Map() // Track temporary files that should be cleaned up const tempFiles = new Set() /** * Download image with caching to avoid re-downloading same URL * @param url Remote image URL * @returns Local file path */ const downloadWithCache = async (url: string): Promise => { // Check cache first const cachedPath = urlCache.get(url) if (cachedPath) { const exists = await RNFS.exists(cachedPath) if (exists) { console.log('[JS] ImageDownloader.downloadWithCache - CACHE HIT:', cachedPath) return cachedPath } // Cache invalid, remove urlCache.delete(url) } // Generate filename with timestamp const filename = `img_${Date.now()}.png` const localPath = `${RNFS.CachesDirectoryPath}/${filename}` console.log('[JS] ImageDownloader.downloadWithCache - DOWNLOADING:', url) await RNFS.downloadFile({ fromUrl: url, toFile: localPath, }).promise // Validate downloaded file const stat = await RNFS.stat(localPath) if (stat.size === 0) { await RNFS.unlink(localPath) throw new Error('Downloaded file is empty') } // Save to cache and track for cleanup urlCache.set(url, localPath) tempFiles.add(localPath) console.log('[JS] ImageDownloader.downloadWithCache - SUCCESS:', localPath) return localPath } /** * Validate and normalize local file path * @param path File path (with or without file:// prefix) * @returns Normalized file:// path */ const validateLocalPath = async (path: string): Promise => { // Normalize path const normalized = path.startsWith('file://') ? path : `file://${path}` const filePath = normalized.replace('file://', '') // Check file exists const exists = await RNFS.exists(filePath) if (!exists) { throw new Error(`File not found: ${filePath}`) } // Validate file size (must be > 0) const stat = await RNFS.stat(filePath) if (stat.size === 0) { throw new Error(`File is empty: ${filePath}`) } console.log('[JS] ImageDownloader.validateLocalPath - SUCCESS:', normalized) return normalized } /** * Resolve bundled asset (from require() or import) * @param imageSource Asset source * @returns Local file path */ const resolveAsset = async (imageSource: ImageSourcePropType): Promise => { const resolved = Image.resolveAssetSource(imageSource) if (!resolved?.uri) { throw new Error('Could not resolve image asset') } console.log('[JS] ImageDownloader.resolveAsset - RESOLVED URI:', resolved.uri) // If already file:// or http://, handle accordingly if (resolved.uri.startsWith('file://')) { return validateLocalPath(resolved.uri) } if (resolved.uri.startsWith('http://') || resolved.uri.startsWith('https://')) { return downloadWithCache(resolved.uri) } // Handle bundled assets const tempPath = `${RNFS.CachesDirectoryPath}/asset_${Date.now()}.png` // Check if already cached const cacheExists = await RNFS.exists(tempPath) if (cacheExists) { console.log('[JS] ImageDownloader.resolveAsset - CACHE HIT:', tempPath) tempFiles.add(tempPath) return `file://${tempPath}` } if (Platform.OS === 'android') { // Android: Copy from assets folder const cleanPath = resolved.uri .replace('asset:/', '') .replace('asset://', '') .replace(/^\/+/, '') // Remove leading slashes console.log('[JS] ImageDownloader.resolveAsset - ANDROID PATH:', cleanPath) try { await RNFS.copyFileAssets(cleanPath, tempPath) tempFiles.add(tempPath) console.log('[JS] ImageDownloader.resolveAsset - SUCCESS:', tempPath) return `file://${tempPath}` } catch (error) { throw new Error(`Failed to copy Android asset: ${cleanPath} - ${error}`) } } else { // iOS: Bundled resources const bundlePath = resolved.uri // Check if accessible const exists = await RNFS.exists(bundlePath) if (exists) { console.log('[JS] ImageDownloader.resolveAsset - IOS BUNDLED:', bundlePath) // Don't track bundled files for cleanup (they're not temp files) return `file://${bundlePath}` } // Try copy to temp try { await RNFS.copyFile(bundlePath, tempPath) tempFiles.add(tempPath) console.log('[JS] ImageDownloader.resolveAsset - SUCCESS:', tempPath) return `file://${tempPath}` } catch (error) { throw new Error(`Failed to access iOS bundled image: ${bundlePath} - ${error}`) } } } /** * Universal image path resolver - Main entry point * * Supports: * - Remote URL: 'https://example.com/logo.png' * - Local file: '/path/to/image.png' or 'file:///path/to/image.png' * - Bundled asset: require('./assets/logo.png') * * @param source Image source (URL string, file path, or require() asset) * @returns Normalized file:// path ready for printing * * @example * // URL * await resolveImagePath('https://example.com/logo.png') * * // Local file * await resolveImagePath('/sdcard/Pictures/logo.png') * await resolveImagePath('file:///sdcard/Pictures/logo.png') * * // Asset * await resolveImagePath(require('./assets/logo.png')) */ export const resolveImagePath = async (source: string | ImageSourcePropType): Promise => { console.log('[JS] ImageDownloader.resolveImagePath - INPUT:', typeof source === 'string' ? source : 'require() asset') // Case 1: String input (URL or file path) if (typeof source === 'string') { // HTTP/HTTPS URL if (source.startsWith('http://') || source.startsWith('https://')) { return downloadWithCache(source) } // Local file path (with or without file://) return validateLocalPath(source) } // Case 2: require() asset (ImageSourcePropType) return resolveAsset(source) } /** * Cleanup specific image file after printing * @param imagePath File path to cleanup (with or without file:// prefix) * @internal Used internally after printing */ export const cleanupImageFile = async (imagePath: string): Promise => { try { // Normalize path const filePath = imagePath.replace('file://', '') // Only cleanup if it's a tracked temp file if (!tempFiles.has(filePath)) { console.log('[JS] ImageDownloader.cleanupImageFile - SKIP: Not a temp file:', filePath) return } // Check if file exists const exists = await RNFS.exists(filePath) if (!exists) { tempFiles.delete(filePath) return } // Delete file await RNFS.unlink(filePath) tempFiles.delete(filePath) // Also remove from URL cache if present for (const [url, path] of urlCache.entries()) { if (path === filePath) { urlCache.delete(url) break } } console.log('[JS] ImageDownloader.cleanupImageFile - SUCCESS: Deleted:', filePath) } catch (error) { console.error('[JS] ImageDownloader.cleanupImageFile - ERROR:', error) } }