2025-11-17 10:44:49 +01:00
|
|
|
import { loadImageMagick } from '@/lib/wasm/wasmLoader';
|
|
|
|
|
import type { ConversionOptions, ProgressCallback, ConversionResult } from '@/types/conversion';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Convert image using ImageMagick
|
|
|
|
|
*/
|
|
|
|
|
export async function convertWithImageMagick(
|
|
|
|
|
file: File,
|
|
|
|
|
outputFormat: string,
|
|
|
|
|
options: ConversionOptions = {},
|
|
|
|
|
onProgress?: ProgressCallback
|
|
|
|
|
): Promise<ConversionResult> {
|
|
|
|
|
const startTime = Date.now();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Load ImageMagick instance
|
2025-11-17 11:56:54 +01:00
|
|
|
await loadImageMagick();
|
2025-11-17 10:44:49 +01:00
|
|
|
|
|
|
|
|
// Report initial progress
|
|
|
|
|
if (onProgress) onProgress(10);
|
|
|
|
|
|
|
|
|
|
// Read input file as ArrayBuffer
|
|
|
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
|
|
|
const inputData = new Uint8Array(arrayBuffer);
|
|
|
|
|
|
|
|
|
|
if (onProgress) onProgress(30);
|
|
|
|
|
|
2025-11-17 12:09:49 +01:00
|
|
|
// Import ImageMagick functions (already initialized by loadImageMagick)
|
|
|
|
|
const { ImageMagick } = await import('@imagemagick/magick-wasm');
|
2025-11-17 10:44:49 +01:00
|
|
|
|
2025-11-17 11:56:54 +01:00
|
|
|
if (onProgress) onProgress(40);
|
2025-11-17 10:44:49 +01:00
|
|
|
|
2025-11-17 11:56:54 +01:00
|
|
|
// Get output format enum
|
|
|
|
|
const outputFormatEnum = await getMagickFormatEnum(outputFormat);
|
2025-11-17 10:44:49 +01:00
|
|
|
|
2025-11-17 11:56:54 +01:00
|
|
|
if (onProgress) onProgress(50);
|
2025-11-17 10:44:49 +01:00
|
|
|
|
2025-11-17 11:56:54 +01:00
|
|
|
// Convert image using ImageMagick
|
2025-11-17 12:16:37 +01:00
|
|
|
let result: Uint8Array | undefined;
|
2025-11-17 11:56:54 +01:00
|
|
|
|
|
|
|
|
await ImageMagick.read(inputData, (image) => {
|
|
|
|
|
// Apply quality setting if specified
|
|
|
|
|
if (options.imageQuality !== undefined) {
|
|
|
|
|
image.quality = options.imageQuality;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply resize if specified
|
|
|
|
|
if (options.imageWidth || options.imageHeight) {
|
|
|
|
|
const width = options.imageWidth || 0;
|
|
|
|
|
const height = options.imageHeight || 0;
|
|
|
|
|
|
|
|
|
|
if (width > 0 && height > 0) {
|
|
|
|
|
// Both dimensions specified
|
|
|
|
|
image.resize(width, height);
|
|
|
|
|
} else if (width > 0) {
|
|
|
|
|
// Only width specified, maintain aspect ratio
|
|
|
|
|
const aspectRatio = image.height / image.width;
|
|
|
|
|
image.resize(width, Math.round(width * aspectRatio));
|
|
|
|
|
} else if (height > 0) {
|
|
|
|
|
// Only height specified, maintain aspect ratio
|
|
|
|
|
const aspectRatio = image.width / image.height;
|
|
|
|
|
image.resize(Math.round(height * aspectRatio), height);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (onProgress) onProgress(70);
|
|
|
|
|
|
|
|
|
|
// Write to output format
|
|
|
|
|
image.write(outputFormatEnum, (data) => {
|
|
|
|
|
result = data;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (onProgress) onProgress(90);
|
|
|
|
|
});
|
2025-11-17 10:44:49 +01:00
|
|
|
|
2025-11-17 12:16:37 +01:00
|
|
|
// Verify we have a result
|
|
|
|
|
if (!result || result.length === 0) {
|
|
|
|
|
throw new Error('ImageMagick conversion produced empty result');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('[ImageMagick] Conversion complete:', {
|
|
|
|
|
inputSize: inputData.length,
|
|
|
|
|
outputSize: result.length,
|
|
|
|
|
format: outputFormat,
|
|
|
|
|
quality: options.imageQuality,
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-17 10:44:49 +01:00
|
|
|
// Create blob from result
|
2025-11-17 12:16:37 +01:00
|
|
|
const mimeType = getMimeType(outputFormat);
|
|
|
|
|
const blob = new Blob([result as BlobPart], { type: mimeType });
|
|
|
|
|
|
|
|
|
|
console.log('[ImageMagick] Created blob:', {
|
|
|
|
|
size: blob.size,
|
|
|
|
|
type: blob.type,
|
|
|
|
|
});
|
2025-11-17 10:44:49 +01:00
|
|
|
|
|
|
|
|
if (onProgress) onProgress(100);
|
|
|
|
|
|
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
blob,
|
|
|
|
|
duration,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[ImageMagick] Conversion error:', error);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: error instanceof Error ? error.message : 'Unknown conversion error',
|
|
|
|
|
duration: Date.now() - startTime,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get ImageMagick format enum
|
|
|
|
|
*/
|
2025-11-17 11:56:54 +01:00
|
|
|
async function getMagickFormatEnum(format: string): Promise<any> {
|
|
|
|
|
const { MagickFormat } = await import('@imagemagick/magick-wasm');
|
|
|
|
|
|
|
|
|
|
const formatMap: Record<string, any> = {
|
|
|
|
|
png: MagickFormat.Png,
|
|
|
|
|
jpg: MagickFormat.Jpg,
|
|
|
|
|
jpeg: MagickFormat.Jpg,
|
|
|
|
|
webp: MagickFormat.WebP,
|
|
|
|
|
gif: MagickFormat.Gif,
|
|
|
|
|
bmp: MagickFormat.Bmp,
|
|
|
|
|
tiff: MagickFormat.Tiff,
|
|
|
|
|
svg: MagickFormat.Svg,
|
2025-11-17 10:44:49 +01:00
|
|
|
};
|
|
|
|
|
|
2025-11-17 11:56:54 +01:00
|
|
|
return formatMap[format.toLowerCase()] || MagickFormat.Png;
|
2025-11-17 10:44:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get MIME type for output format
|
|
|
|
|
*/
|
|
|
|
|
function getMimeType(format: string): string {
|
|
|
|
|
const mimeTypes: Record<string, string> = {
|
|
|
|
|
png: 'image/png',
|
|
|
|
|
jpg: 'image/jpeg',
|
|
|
|
|
jpeg: 'image/jpeg',
|
|
|
|
|
webp: 'image/webp',
|
|
|
|
|
gif: 'image/gif',
|
|
|
|
|
bmp: 'image/bmp',
|
|
|
|
|
tiff: 'image/tiff',
|
|
|
|
|
svg: 'image/svg+xml',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return mimeTypes[format.toLowerCase()] || 'application/octet-stream';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Resize image
|
|
|
|
|
*/
|
|
|
|
|
export async function resizeImage(
|
|
|
|
|
file: File,
|
|
|
|
|
width: number,
|
|
|
|
|
height: number,
|
|
|
|
|
outputFormat?: string,
|
|
|
|
|
onProgress?: ProgressCallback
|
|
|
|
|
): Promise<ConversionResult> {
|
|
|
|
|
const format = outputFormat || file.name.split('.').pop() || 'png';
|
|
|
|
|
|
|
|
|
|
return convertWithImageMagick(
|
|
|
|
|
file,
|
|
|
|
|
format,
|
|
|
|
|
{
|
|
|
|
|
imageWidth: width,
|
|
|
|
|
imageHeight: height,
|
|
|
|
|
},
|
|
|
|
|
onProgress
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Convert image to WebP
|
|
|
|
|
*/
|
|
|
|
|
export async function convertToWebP(
|
|
|
|
|
file: File,
|
|
|
|
|
quality: number = 85,
|
|
|
|
|
onProgress?: ProgressCallback
|
|
|
|
|
): Promise<ConversionResult> {
|
|
|
|
|
return convertWithImageMagick(
|
|
|
|
|
file,
|
|
|
|
|
'webp',
|
|
|
|
|
{
|
|
|
|
|
imageQuality: quality,
|
|
|
|
|
},
|
|
|
|
|
onProgress
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Batch convert images
|
|
|
|
|
*/
|
|
|
|
|
export async function batchConvertImages(
|
|
|
|
|
files: File[],
|
|
|
|
|
outputFormat: string,
|
|
|
|
|
options: ConversionOptions = {},
|
|
|
|
|
onProgress?: (fileIndex: number, progress: number) => void
|
|
|
|
|
): Promise<ConversionResult[]> {
|
|
|
|
|
const results: ConversionResult[] = [];
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
|
|
|
const file = files[i];
|
|
|
|
|
|
|
|
|
|
const result = await convertWithImageMagick(
|
|
|
|
|
file,
|
|
|
|
|
outputFormat,
|
|
|
|
|
options,
|
|
|
|
|
(progress) => {
|
|
|
|
|
if (onProgress) {
|
|
|
|
|
onProgress(i, progress);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
results.push(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return results;
|
|
|
|
|
}
|