Changed from image.write((data) => data) callback pattern to image.write(outputFormatEnum) which correctly writes the image data in the specified format. This should now produce valid WebP files with proper RIFF headers (52 19 46 46 magic bytes). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
247 lines
6.3 KiB
TypeScript
247 lines
6.3 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) => {
|
|
// 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 with the specified format
|
|
result = image.write(outputFormatEnum);
|
|
|
|
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;
|
|
}
|