/** * HEIC Image Converter - Refactored with improved error handling and ES6 modules */ // Error handling utility class HeicConversionError extends Error { constructor(message, context, originalError = null) { super(`HEIC Conversion Error [${context}]: ${message}`); this.name = "HeicConversionError"; this.context = context; this.originalError = originalError; } } // Logger utility for structured error reporting const logger = { error: (message, context, error = null) => { const errorInfo = { timestamp: new Date().toISOString(), context, message, originalError: error?.message || error, }; console.error(JSON.stringify(errorInfo)); }, }; // Async utility for generator functions const asyncGenerator = (context, _args, generator) => new Promise((resolve, reject) => { const step = (result) => { try { const next = generator.next(result); if (next.done) { resolve(next.value); } else { Promise.resolve(next.value).then(step, handleError); } } catch (error) { handleError(error); } }; const handleError = (error) => { logger.error("Async generator failed", context, error); reject(new HeicConversionError(error.message, context, error)); }; try { step(generator.next()); } catch (error) { handleError(error); } }); // HEIC format detection export const isHeic = (file) => { return asyncGenerator("formatDetection", null, function* () { try { const buffer = (yield file.arrayBuffer()).slice(8, 12); const decoder = new TextDecoder("utf-8"); const fileType = decoder.decode(buffer).replace("\0", " ").trim(); const supportedTypes = ["mif1", "msf1", "heic", "heix", "hevc", "hevx"]; if (!supportedTypes.includes(fileType)) { logger.error(`Unsupported file type: ${fileType}`, "formatDetection"); return false; } return true; } catch (error) { throw new HeicConversionError( "Failed to detect HEIC format", "formatDetection", error, ); } }); }; // Worker management let heicWorker = null; const getWorker = () => { if (!heicWorker) { try { // The large worker code blob would be loaded from external file in production const workerCode = `// Worker code here - would be loaded from separate file`; const blob = new Blob([workerCode], { type: "application/javascript" }); heicWorker = new Worker(URL.createObjectURL(blob)); heicWorker.onerror = (error) => { logger.error("Worker initialization failed", "workerSetup", error); throw new HeicConversionError( "Worker failed to initialize", "workerSetup", error, ); }; } catch (error) { logger.error("Failed to create worker", "workerSetup", error); throw new HeicConversionError( "Worker creation failed", "workerSetup", error, ); } } return heicWorker; }; // Convert HEIC to image data using worker const convertToImageData = (buffer) => { return new Promise((resolve, reject) => { try { const worker = getWorker(); const messageId = (Math.random() * new Date().getTime()).toString(); const message = { id: messageId, buffer }; const handleMessage = (event) => { if (event.data.id === messageId) { worker.removeEventListener("message", handleMessage); worker.removeEventListener("error", handleError); if (event.data.error) { reject( new HeicConversionError(event.data.error, "workerConversion"), ); } else { resolve(event.data.imageData); } } }; const handleError = (error) => { worker.removeEventListener("message", handleMessage); worker.removeEventListener("error", handleError); reject( new HeicConversionError( "Worker communication failed", "workerConversion", error, ), ); }; worker.addEventListener("message", handleMessage); worker.postMessage(message); worker.addEventListener("error", handleError); } catch (error) { reject( new HeicConversionError( "Failed to send message to worker", "workerConversion", error, ), ); } }); }; // Convert image data to canvas const imageDataToCanvas = (buffer) => { return asyncGenerator("canvasConversion", null, function* () { try { const imageData = yield convertToImageData(buffer); const canvas = document.createElement("canvas"); canvas.width = imageData.width; canvas.height = imageData.height; const context = canvas.getContext("2d"); if (!context) { throw new HeicConversionError( "Failed to get canvas context", "canvasConversion", ); } context.putImageData(imageData, 0, 0); return canvas; } catch (error) { throw new HeicConversionError( "Failed to convert image data to canvas", "canvasConversion", error, ); } }); }; // Clean up canvas resources const cleanupCanvas = (canvas) => { try { canvas.width = 1; canvas.height = 1; const context = canvas.getContext("2d"); if (context) { context.clearRect(0, 0, 1, 1); } } catch (error) { logger.error("Canvas cleanup failed", "resourceCleanup", error); } }; // Main HEIC conversion function export const heicTo = (options) => { return asyncGenerator( "mainConversion", [options], function* ( { blob, type = "image/jpeg", quality = 0.8, options: convertOptions = {}, }, ) { let canvas = null; try { // Validate inputs if (!blob) { throw new HeicConversionError("Blob is required", "inputValidation"); } if (typeof quality !== "number" || quality < 0 || quality > 1) { throw new HeicConversionError( "Quality must be a number between 0 and 1", "inputValidation", ); } const buffer = yield blob.arrayBuffer(); if (type === "bitmap") { const imageData = yield convertToImageData(buffer); return createImageBitmap(imageData, convertOptions); } else { canvas = yield imageDataToCanvas(buffer); return yield new Promise((resolve, reject) => { try { canvas.toBlob( (result) => { if (result != null) { resolve(result); } else { reject( new HeicConversionError( "Canvas to blob conversion failed", "blobConversion", ), ); } }, type, quality, ); } catch (error) { reject( new HeicConversionError( "Canvas to blob conversion error", "blobConversion", error, ), ); } }); } } catch (error) { // Re-throw HeicConversionError as-is, wrap others if (error instanceof HeicConversionError) { throw error; } throw new HeicConversionError(error.message, "mainConversion", error); } finally { // Always clean up canvas resources if (canvas) { cleanupCanvas(canvas); } } }, ); }; // Export error class for external error handling export { HeicConversionError };