@julesl23/s5js
Version:
Enhanced TypeScript SDK for S5 decentralized storage with path-based API, media processing, and directory utilities
549 lines • 20.5 kB
JavaScript
/**
* 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