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>
250 lines
6.4 KiB
TypeScript
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;
|
|
}
|