117 lines
3.7 KiB
TypeScript
117 lines
3.7 KiB
TypeScript
// =============================================================================
|
|
// 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<ImageConverterWorker>;
|
|
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<ImageConverterWorker>(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<Blob> {
|
|
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();
|