Files
convert-ui/lib/converters/imagemagickService.ts
Sebastian Krüger b9f9a7bea6 fix: correct ImageMagick write API to produce valid WebP data
The previous implementation was producing invalid image data with
wrong magic bytes (d0 e5 67 00 instead of 52 49 46 46 for WebP).

Root cause: Incorrect usage of ImageMagick write API.

Changes:
- Set image.format BEFORE writing (tells ImageMagick the output format)
- Simplified write() call to: image.write((data) => data)
- This returns the correctly formatted image data

The proper pattern is:
1. Set image.format = outputFormatEnum
2. Apply transformations (quality, resize)
3. Call image.write() which returns the encoded data

This should now produce valid WebP files with correct RIFF header
(52 49 46 46) and allow the preview to display properly.

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

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

250 lines
6.4 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 (already initialized by loadImageMagick)
const { ImageMagick } = await import('@imagemagick/magick-wasm');
if (onProgress) onProgress(40);
// Get output format enum
const outputFormatEnum = await getMagickFormatEnum(outputFormat);
if (onProgress) onProgress(50);
// Convert image using ImageMagick
let result: Uint8Array | undefined;
await ImageMagick.read(inputData, (image) => {
// Set the output format first
image.format = outputFormatEnum;
// 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 the image data
result = image.write((data) => data);
if (onProgress) onProgress(90);
});
// 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,
});
// Verify the data looks like valid image data by checking magic bytes
const first4Bytes = Array.from(result.slice(0, 4)).map(b => b.toString(16).padStart(2, '0')).join(' ');
console.log('[ImageMagick] First 4 bytes:', first4Bytes);
// Create blob from result
const mimeType = getMimeType(outputFormat);
const blob = new Blob([result as BlobPart], { type: mimeType });
console.log('[ImageMagick] Created blob:', {
size: blob.size,
type: blob.type,
});
// Verify blob can be read
try {
const testReader = new FileReader();
const testPromise = new Promise((resolve) => {
testReader.onloadend = () => {
if (testReader.result instanceof ArrayBuffer) {
const testArr = new Uint8Array(testReader.result);
console.log('[ImageMagick] Blob verification - first 4 bytes:',
Array.from(testArr.slice(0, 4)).map(b => b.toString(16).padStart(2, '0')).join(' '));
}
resolve(true);
};
});
testReader.readAsArrayBuffer(blob.slice(0, 4));
await testPromise;
} catch (err) {
console.error('[ImageMagick] Blob verification failed:', err);
}
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;
}