feat: add media converter app and fix compilation errors
This commit is contained in:
248
lib/media/converters/imagemagickService.ts
Normal file
248
lib/media/converters/imagemagickService.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import { loadImageMagick } from '@/lib/media/wasm/wasmLoader';
|
||||
import type { ConversionOptions, ProgressCallback, ConversionResult } from '@/types/media';
|
||||
|
||||
/**
|
||||
* 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 format
|
||||
image.write(outputFormatEnum, (data) => {
|
||||
result = 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;
|
||||
}
|
||||
Reference in New Issue
Block a user