UNPKG

@julesl23/s5js

Version:

Enhanced TypeScript SDK for S5 decentralized storage with path-based API, media processing, and directory utilities

549 lines 20.5 kB
/** * WebAssembly module loader for image metadata extraction */ export class WASMLoader { static instance; static module; static exports; static memoryView; static useAdvanced = false; /** * Load and instantiate the WASM module */ static async initialize(onProgress) { if (this.instance) return; try { const imports = { env: { // Add any required imports here abort: () => { throw new Error('WASM abort called'); } } }; // Report initial progress onProgress?.(0); // Try streaming compilation first (faster) if (typeof WebAssembly.instantiateStreaming === 'function' && typeof fetch !== 'undefined') { try { const wasmUrl = await this.getWASMUrl(); onProgress?.(10); // Fetching const response = await fetch(wasmUrl); if (response.ok) { onProgress?.(50); // Compiling const result = await WebAssembly.instantiateStreaming(response, imports); this.module = result.module; this.instance = result.instance; this.exports = this.instance.exports; this.updateMemoryView(); onProgress?.(100); // Complete return; } } catch (streamError) { // Expected in Node.js environment - silently fall back if (typeof process === 'undefined' || !process.versions?.node) { console.warn('Streaming compilation failed, falling back to ArrayBuffer:', streamError); } } } // Fallback to ArrayBuffer compilation onProgress?.(20); // Loading buffer const wasmBuffer = await this.loadWASMBuffer(); onProgress?.(60); // Compiling // Use compileStreaming if available and we have a Response if (typeof Response !== 'undefined' && typeof WebAssembly.compileStreaming === 'function') { try { const response = new Response(wasmBuffer, { headers: { 'Content-Type': 'application/wasm' } }); this.module = await WebAssembly.compileStreaming(response); } catch { // Fallback to regular compile this.module = await WebAssembly.compile(wasmBuffer); } } else { this.module = await WebAssembly.compile(wasmBuffer); } onProgress?.(90); // Instantiating // Instantiate with imports this.instance = await WebAssembly.instantiate(this.module, imports); this.exports = this.instance.exports; this.updateMemoryView(); onProgress?.(100); // Complete } catch (error) { // Only log in debug mode - fallback mechanism will handle this gracefully if (process.env.DEBUG) { console.error('WASM initialization failed:', error); } throw new Error(`WASM initialization failed: ${error}`); } } /** * Get WASM URL for streaming compilation */ static async getWASMUrl() { const wasmFile = this.useAdvanced ? 'image-advanced.wasm' : 'image-metadata.wasm'; // In browser environment if (typeof window !== 'undefined' && window.location) { return new URL(`/src/media/wasm/${wasmFile}`, window.location.href).href; } // In Node.js environment if (typeof process !== 'undefined' && process.versions?.node) { const { fileURLToPath } = await import('url'); const { dirname, join } = await import('path'); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const wasmPath = join(__dirname, wasmFile); return `file://${wasmPath}`; } // Fallback return `/src/media/wasm/${wasmFile}`; } /** * Load WASM buffer - tries multiple methods */ static async loadWASMBuffer() { const wasmFile = this.useAdvanced ? 'image-advanced.wasm' : 'image-metadata.wasm'; // Try to load advanced WASM first if available if (!this.useAdvanced) { // Check if advanced WASM exists if (typeof process !== 'undefined' && process.versions?.node) { try { const { readFileSync } = await import('fs'); const { fileURLToPath } = await import('url'); const { dirname, join } = await import('path'); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const advancedPath = join(__dirname, 'image-advanced.wasm'); const buffer = readFileSync(advancedPath); this.useAdvanced = true; return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); } catch { // Advanced not available, fall back to basic } } } // In Node.js environment if (typeof process !== 'undefined' && process.versions?.node) { try { const { readFileSync } = await import('fs'); const { fileURLToPath } = await import('url'); const { dirname, join } = await import('path'); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const wasmPath = join(__dirname, wasmFile); const buffer = readFileSync(wasmPath); return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); } catch (error) { // Expected in Node.js when WASM file not in dist - fallback to base64 if (process.env.DEBUG) { console.warn('WASM file not found, using fallback:', error); } } } // In browser environment or as fallback - use fetch if (typeof fetch !== 'undefined') { try { const response = await fetch(`/src/media/wasm/${wasmFile}`); if (response.ok) { return await response.arrayBuffer(); } } catch (error) { // Expected when not running with HTTP server - fallback to base64 if (process.env.DEBUG) { console.warn('WASM fetch failed, using fallback:', error); } } } // Final fallback: embedded base64 (we'll generate this) return this.loadEmbeddedWASM(); } /** * Load embedded WASM from base64 */ static async loadEmbeddedWASM() { // This will be populated with the base64 content during build const base64 = await this.getBase64WASM(); const binaryString = atob(base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; } /** * Get base64 encoded WASM */ static async getBase64WASM() { // Try to load from file first (Node.js) if (typeof process !== 'undefined' && process.versions?.node) { try { const { readFileSync } = await import('fs'); const { fileURLToPath } = await import('url'); const { dirname, join } = await import('path'); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const base64Path = join(__dirname, 'image-metadata.wasm.base64'); return readFileSync(base64Path, 'utf8'); } catch (error) { // Fall through to embedded } } // Embedded base64 - this is a minimal fallback // In production, this would be replaced during build return 'AGFzbQEAAAABGAVgAX8Bf2ACf38Bf2ACf38CfwBgAABgA39/fwADCQgAAQECAgMEBAQFAwEAEAZPCn8AQQELfwBBAAt/AEEAC38AQYAICwF/AEGACAsBeAZtZW1vcnkCAIABAGV4cG9ydHMJbWFsbG9jAAEGZnJlZQACDmRldGVjdF9mb3JtYXQAAxdleHRyYWN0X3BuZ19kaW1lbnNpb25zAAQYZXh0cmFjdF9qcGVnX2RpbWVuc2lvbnMABRBleHRyYWN0X21ldGFkYXRhAAYHQ29uc3RhbnRzFEhFQVBfUFRSX0lOSVRJQUxJWkUDBwqYBAgUACABQQRJBEBBAA8LCzoAIAIgATYCBCACQQE2AgAgAkEANgIIIAJBADYCDAs='; } /** * Update memory view after potential growth */ static updateMemoryView() { if (this.exports?.memory) { this.memoryView = new Uint8Array(this.exports.memory.buffer); } } /** * Copy data to WASM memory with optimization for large images */ static copyToWASM(data) { if (!this.exports || !this.memoryView) { throw new Error('WASM not initialized'); } // For very large images, consider sampling instead of processing full image const MAX_IMAGE_SIZE = 50 * 1024 * 1024; // 50MB limit let processData = data; if (data.length > MAX_IMAGE_SIZE) { console.warn(`Image too large (${data.length} bytes), will process only metadata`); // For metadata extraction, we only need the header processData = data.slice(0, 65536); // First 64KB should contain all metadata } // Check if memory needs to grow const requiredSize = processData.length + 4096; // Add buffer for alignment const currentSize = this.memoryView.length; if (requiredSize > currentSize) { // Grow memory (in pages of 64KB) const pagesNeeded = Math.ceil((requiredSize - currentSize) / 65536); try { this.exports.memory.grow(pagesNeeded); this.updateMemoryView(); } catch (error) { throw new Error(`Failed to allocate memory: ${error}. Required: ${requiredSize} bytes`); } } // Allocate memory in WASM const ptr = this.exports.malloc(processData.length); if (ptr === 0) { throw new Error('Failed to allocate memory in WASM'); } // Copy data try { this.memoryView.set(processData, ptr); } catch (error) { this.exports.free(ptr); throw new Error(`Failed to copy data to WASM memory: ${error}`); } return ptr; } /** * Read data from WASM memory */ static readFromWASM(ptr, length) { if (!this.memoryView) { throw new Error('WASM not initialized'); } return new Uint8Array(this.memoryView.slice(ptr, ptr + length)); } /** * Read 32-bit integer from WASM memory */ static readInt32(ptr) { if (!this.memoryView) { throw new Error('WASM not initialized'); } const view = new DataView(this.memoryView.buffer, ptr, 4); return view.getInt32(0, true); // little-endian } /** * Extract metadata using WASM */ static extractMetadata(imageData) { // Validate input before processing if (!imageData || imageData.length === 0) { return null; // Empty data } if (imageData.length < 8) { return null; // Too small to be any valid image } if (!this.exports) { throw new Error('WASM not initialized'); } const dataPtr = this.copyToWASM(imageData); try { // Call WASM function const resultPtr = this.exports.extract_metadata(dataPtr, imageData.length); if (resultPtr === 0) { return null; } // Read result from memory const format = this.readInt32(resultPtr); const width = this.readInt32(resultPtr + 4); const height = this.readInt32(resultPtr + 8); const size = this.readInt32(resultPtr + 12); // Map format number to string const formatMap = { 1: 'jpeg', 2: 'png', 3: 'gif', 4: 'bmp', 5: 'webp', 0: 'unknown' }; return { format: formatMap[format] || 'unknown', width, height, size }; } finally { // Free allocated memory this.exports.free(dataPtr); } } /** * Detect image format using WASM */ static detectFormat(imageData) { if (!this.exports) { throw new Error('WASM not initialized'); } const dataPtr = this.copyToWASM(imageData); try { const format = this.exports.detect_format(dataPtr, imageData.length); const formatMap = { 1: 'jpeg', 2: 'png', 3: 'gif', 4: 'bmp', 5: 'webp', 0: 'unknown' }; return formatMap[format] || 'unknown'; } finally { this.exports.free(dataPtr); } } /** * Get dimensions for specific format */ static getDimensions(imageData, format) { if (!this.exports) { throw new Error('WASM not initialized'); } const dataPtr = this.copyToWASM(imageData); try { let width = 0; let height = 0; if (format === 'png') { [width, height] = this.exports.extract_png_dimensions(dataPtr, imageData.length); } else if (format === 'jpeg') { [width, height] = this.exports.extract_jpeg_dimensions(dataPtr, imageData.length); } if (width === 0 && height === 0) { return null; } return { width, height }; } finally { this.exports.free(dataPtr); } } /** * Clean up WASM resources */ static cleanup() { this.instance = undefined; this.module = undefined; this.exports = undefined; this.memoryView = undefined; } /** * Check if WASM is initialized */ static isInitialized() { return !!this.instance && !!this.exports; } /** * Check if advanced functions are available */ static hasAdvancedFunctions() { return !!this.exports?.detect_png_bit_depth; } /** * Get bit depth for PNG images */ static getPNGBitDepth(imageData) { if (!this.exports || !this.exports.detect_png_bit_depth) { return null; } const dataPtr = this.copyToWASM(imageData); try { const bitDepth = this.exports.detect_png_bit_depth(dataPtr, imageData.length); return bitDepth > 0 ? bitDepth : null; } finally { this.exports.free(dataPtr); } } /** * Check if image has alpha channel */ static hasAlpha(imageData) { if (!this.exports || !this.exports.has_alpha_channel) { return false; } const dataPtr = this.copyToWASM(imageData); try { return this.exports.has_alpha_channel(dataPtr, imageData.length) === 1; } finally { this.exports.free(dataPtr); } } /** * Estimate JPEG quality */ static estimateJPEGQuality(imageData) { if (!this.exports || !this.exports.estimate_jpeg_quality) { return null; } const dataPtr = this.copyToWASM(imageData); try { const quality = this.exports.estimate_jpeg_quality(dataPtr, imageData.length); return quality > 0 ? quality : null; } finally { this.exports.free(dataPtr); } } /** * Check if image is progressive */ static isProgressive(imageData, format) { if (!this.exports || !this.exports.is_progressive) { return false; } const formatMap = { 'jpeg': 1, 'png': 2 }; const formatNum = formatMap[format] || 0; if (formatNum === 0) return false; const dataPtr = this.copyToWASM(imageData); try { return this.exports.is_progressive(dataPtr, imageData.length, formatNum) === 1; } finally { this.exports.free(dataPtr); } } /** * Calculate histogram statistics */ static calculateHistogram(imageData) { if (!this.exports || !this.exports.calculate_histogram_stats) { return null; } const dataPtr = this.copyToWASM(imageData); const resultPtr = this.exports.malloc(12); // 3 x i32 try { this.exports.calculate_histogram_stats(dataPtr, imageData.length, resultPtr); const avgLuminance = this.readInt32(resultPtr); const overexposed = this.readInt32(resultPtr + 4); const underexposed = this.readInt32(resultPtr + 8); return { avgLuminance, overexposed, underexposed }; } finally { this.exports.free(dataPtr); this.exports.free(resultPtr); } } /** * Find EXIF data offset */ static findEXIFOffset(imageData) { if (!this.exports || !this.exports.find_exif_offset) { return null; } const dataPtr = this.copyToWASM(imageData); try { const offset = this.exports.find_exif_offset(dataPtr, imageData.length); return offset > 0 ? offset : null; } finally { this.exports.free(dataPtr); } } /** * Perform complete image analysis */ static analyzeImage(imageData) { if (!this.exports || !this.exports.analyze_image) { // Fall back to basic metadata extraction return this.extractMetadata(imageData); } const dataPtr = this.copyToWASM(imageData); const resultPtr = this.exports.malloc(64); // Enough for all fields try { this.exports.analyze_image(dataPtr, imageData.length, resultPtr); const format = this.readInt32(resultPtr); const width = this.readInt32(resultPtr + 4); const height = this.readInt32(resultPtr + 8); const size = this.readInt32(resultPtr + 12); const bitDepth = this.readInt32(resultPtr + 16); const hasAlpha = this.readInt32(resultPtr + 20) === 1; const quality = this.readInt32(resultPtr + 24); const isProgressive = this.readInt32(resultPtr + 28) === 1; const avgLuminance = this.readInt32(resultPtr + 32); const overexposed = this.readInt32(resultPtr + 36); const underexposed = this.readInt32(resultPtr + 40); const exifOffset = this.readInt32(resultPtr + 44); const formatMap = { 1: 'jpeg', 2: 'png', 3: 'gif', 4: 'bmp', 5: 'webp', 0: 'unknown' }; return { format: formatMap[format] || 'unknown', width, height, size, bitDepth: bitDepth > 0 ? bitDepth : undefined, hasAlpha, quality: quality > 0 ? quality : undefined, isProgressive, histogram: avgLuminance > 0 ? { avgLuminance, overexposed, underexposed } : undefined, exifOffset: exifOffset > 0 ? exifOffset : undefined }; } finally { this.exports.free(dataPtr); this.exports.free(resultPtr); } } } //# sourceMappingURL=loader.js.map