Files
convert-ui/lib/converters/imagemagickService.ts
Sebastian Krüger 9eb66b4fa3 fix: add proper ImageMagick WASM initialization with CDN URL
The initializeImageMagick() function requires a URL argument pointing
to the WASM file. Without it, the library won't properly load and
conversions may fail.

Changes:
- Add initializeImageMagick() call with CDN URL
- Use jsDelivr CDN for magick.wasm file
- Matches the installed package version (0.0.30)

This ensures ImageMagick WASM is properly initialized before performing
image conversions, preventing potential runtime errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 12:02:05 +01:00

212 lines
5.1 KiB
TypeScript

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
await loadImageMagick();
// 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);
// Import ImageMagick functions
const { ImageMagick, initializeImageMagick } = await import('@imagemagick/magick-wasm');
// Initialize ImageMagick with WASM URL from CDN
const wasmUrl = 'https://cdn.jsdelivr.net/npm/@imagemagick/magick-wasm@0.0.30/dist/magick.wasm';
await initializeImageMagick(wasmUrl);
if (onProgress) onProgress(40);
// Get output format enum
const outputFormatEnum = await getMagickFormatEnum(outputFormat);
if (onProgress) onProgress(50);
// Convert image using ImageMagick
let result: Uint8Array;
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);
});
// Create blob from result
const blob = new Blob([result! as BlobPart], { type: getMimeType(outputFormat) });
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
*/
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,
};
return formatMap[format.toLowerCase()] || MagickFormat.Png;
}
/**
* 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;
}