// ============================================================================= // HEIC Adapter // Converts HEIC/HEIF images to any image format // Uses heic2any for HEIC→JPEG/PNG, then ImageMagick worker for further conversion // ============================================================================= import * as Comlink from 'comlink'; import heic2any from 'heic2any'; import type { IFileConverter, SupportedFormat, ConversionRequest, ImageFormat, } from '../core/types'; import type { ImageConverterWorker } from '../workers/image.worker'; // Formats heic2any can directly output const HEIC2ANY_OUTPUTS: ImageFormat[] = ['jpeg', 'png']; // All image formats we support as final output (via two-step conversion) const ALL_IMAGE_OUTPUTS: ImageFormat[] = [ 'jpeg', 'png', 'webp', 'gif', 'bmp', 'tiff', 'ico', 'avif', 'jxl', 'svg', 'psd', 'tga' ]; type WorkerType = Comlink.Remote; let workerInstance: WorkerType | null = null; function getWorker(): WorkerType { if (!workerInstance) { const worker = new Worker( new URL('../workers/image.worker.ts', import.meta.url), { type: 'module' } ); workerInstance = Comlink.wrap(worker); } return workerInstance; } export class HeicAdapter implements IFileConverter { readonly name = 'heic-adapter'; supports(inputFormat: SupportedFormat, outputFormat: SupportedFormat): boolean { return ( inputFormat === 'heic' && ALL_IMAGE_OUTPUTS.includes(outputFormat as ImageFormat) ); } async convert( request: ConversionRequest, onProgress?: (progress: number) => void ): Promise { const { file, targetFormat, options } = request; const quality = options?.quality ?? 85; onProgress?.(5); try { // Step 1: Convert HEIC to intermediate format using heic2any (main thread) // Use JPEG as intermediate for efficiency, PNG if target is PNG const intermediateFormat = targetFormat === 'png' ? 'png' : 'jpeg'; const toType = intermediateFormat === 'png' ? 'image/png' : 'image/jpeg'; let intermediateBlob: Blob; try { const result = await heic2any({ blob: file, toType, quality: quality / 100, }); intermediateBlob = Array.isArray(result) ? result[0] : result; } catch (heicError) { // heic2any failed - provide helpful error message const errorMessage = heicError instanceof Error ? heicError.message : String(heicError); if (errorMessage.includes('LIBHEIF') || errorMessage.includes('format not supported')) { throw new Error(`This HEIC file format is not supported. Try using a different photo or convert it on your iPhone first.`); } throw new Error(`Failed to decode HEIC: ${JSON.stringify(errorMessage)}`); } onProgress?.(50); // Step 2: If target is JPEG or PNG, we're done if (HEIC2ANY_OUTPUTS.includes(targetFormat as ImageFormat)) { onProgress?.(100); return intermediateBlob; } // Step 3: Convert intermediate to final format using ImageMagick worker const arrayBuffer = await intermediateBlob.arrayBuffer(); onProgress?.(70); const worker = getWorker(); const result = await worker.convertImage( arrayBuffer, intermediateFormat, targetFormat as ImageFormat, quality ); onProgress?.(100); return result; } catch (error) { // Re-throw with context if (error instanceof Error) { throw error; } throw new Error(`HEIC conversion failed: ${String(error)}`); } } } // Export singleton instance export const heicAdapter = new HeicAdapter();