feat: Initialize k-convert application with core structure, UI components, and conversion logic.

This commit is contained in:
2026-01-30 14:42:32 +01:00
commit 95ee627f1d
86 changed files with 9506 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
// =============================================================================
// 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
const ALL_IMAGE_OUTPUTS: ImageFormat[] = ['jpeg', 'png', 'webp', 'gif', 'bmp', 'tiff', 'ico'];
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();