Skip to main content
Glama
converter.ts15.7 kB
import sharp from 'sharp'; import Jimp from 'jimp'; import { promises as fs } from 'fs'; import path from 'path'; export interface ConvertImageOptions { input_path?: string; input_data?: Buffer | string; // 支持Buffer数据或base64字符串 input_filename?: string; // 原始文件名,用于确定格式 output_format: string; quality?: number; width?: number; height?: number; maintain_aspect_ratio?: boolean; output_path?: string; } export interface BatchConvertOptions { input_paths?: string[]; input_files?: Array<{ data: Buffer | string; filename: string; }>; // 支持上传的文件数据 output_format: string; quality?: number; width?: number; height?: number; maintain_aspect_ratio?: boolean; output_directory?: string; } export interface ConvertResult { output_path: string; file_size: number; dimensions: { width: number; height: number; }; format: string; } export interface BatchConvertResult { success: boolean; output_path?: string; error?: string; } export interface ImageInfo { format: string; width: number; height: number; channels: number; size: number; space?: string; } export class ImageConverter { private supportedInputFormats = [ 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'tif', 'webp', 'svg', 'ico', 'psd', 'heic', 'heif', 'avif' ]; private supportedOutputFormats = [ 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp', 'svg', 'ico', 'avif' ]; constructor() {} /** * 转换单个图片 */ async convertImage(options: ConvertImageOptions): Promise<ConvertResult> { const { input_path, input_data, input_filename, output_format, quality, width, height, maintain_aspect_ratio = true, output_path } = options; let inputBuffer: Buffer; let inputFormat: string; // 处理不同的输入方式 if (input_data) { // 处理上传的文件数据 if (typeof input_data === 'string') { // 处理base64数据 const base64Data = input_data.includes(',') ? input_data.split(',')[1] : input_data; inputBuffer = Buffer.from(base64Data, 'base64'); } else { // 处理Buffer数据 inputBuffer = input_data; } // 从文件名获取格式 if (input_filename) { inputFormat = this.getFileFormat(input_filename); } else { // 尝试从Buffer检测格式 inputFormat = await this.detectFormatFromBuffer(inputBuffer); } } else if (input_path) { // 处理文件路径 try { await fs.access(input_path); inputBuffer = await fs.readFile(input_path); inputFormat = this.getFileFormat(input_path); } catch { throw new Error(`输入文件不存在: ${input_path}`); } } else { throw new Error('必须提供input_path或input_data参数'); } // 验证输出格式 const normalizedFormat = output_format.toLowerCase().replace('.', ''); if (!this.supportedOutputFormats.includes(normalizedFormat)) { throw new Error(`不支持的输出格式: ${output_format}`); } // 生成输出路径 const finalOutputPath = output_path || this.generateOutputPath( input_path || input_filename || `uploaded_image.${inputFormat}`, normalizedFormat ); // 确保输出目录存在 const outputDir = path.dirname(finalOutputPath); await fs.mkdir(outputDir, { recursive: true }); // 验证输入格式是否支持 if (!this.supportedInputFormats.includes(inputFormat)) { throw new Error(`不支持的输入格式: ${inputFormat}`); } // 对于特殊格式的处理说明 if (inputFormat === 'heic' || inputFormat === 'heif') { try { // Sharp 0.32+ 支持HEIC,但需要libvips支持 await sharp(inputBuffer).metadata(); } catch { throw new Error(`HEIC/HEIF格式需要系统支持libvips,请先转换为JPG或PNG格式`); } } else if (inputFormat === 'psd') { throw new Error(`PSD格式暂不支持,请使用Photoshop导出为JPG或PNG格式`); } try { let sharpInstance = sharp(inputBuffer); // 调整尺寸 if (width || height) { const resizeOptions: sharp.ResizeOptions = { fit: maintain_aspect_ratio ? 'inside' : 'fill', withoutEnlargement: false }; if (width && height) { sharpInstance = sharpInstance.resize(width, height, resizeOptions); } else if (width) { sharpInstance = sharpInstance.resize(width, undefined, resizeOptions); } else if (height) { sharpInstance = sharpInstance.resize(undefined, height, resizeOptions); } } // 根据格式设置输出选项 switch (normalizedFormat) { case 'jpg': case 'jpeg': sharpInstance = sharpInstance.jpeg({ quality: quality || 90 }); break; case 'png': sharpInstance = sharpInstance.png({ quality: quality || 90 }); break; case 'webp': sharpInstance = sharpInstance.webp({ quality: quality || 90 }); break; case 'gif': // Sharp不直接支持GIF输出,使用Jimp return await this.convertWithJimp(inputBuffer, finalOutputPath, normalizedFormat, { width, height, quality }); case 'bmp': // 使用Jimp处理BMP return await this.convertWithJimp(inputBuffer, finalOutputPath, normalizedFormat, { width, height, quality }); case 'tiff': sharpInstance = sharpInstance.tiff({ quality: quality || 90 }); break; case 'avif': sharpInstance = sharpInstance.avif({ quality: quality || 90 }); break; case 'ico': // ICO格式需要特殊处理 return await this.convertToIco(inputBuffer, finalOutputPath, { width: width || 32, height: height || 32 }); case 'svg': // SVG格式转换 return await this.convertToSvg(inputBuffer, finalOutputPath, { width, height }); default: throw new Error(`暂不支持转换为格式: ${normalizedFormat}`); } // 执行转换 await sharpInstance.toFile(finalOutputPath); // 获取结果信息 const stats = await fs.stat(finalOutputPath); const metadata = await sharp(finalOutputPath).metadata(); return { output_path: finalOutputPath, file_size: stats.size, dimensions: { width: metadata.width || 0, height: metadata.height || 0 }, format: normalizedFormat }; } catch (error) { throw new Error(`图片转换失败: ${error instanceof Error ? error.message : String(error)}`); } } /** * 批量转换图片 */ async batchConvertImages(options: BatchConvertOptions): Promise<BatchConvertResult[]> { const { input_paths, input_files, output_format, quality, width, height, maintain_aspect_ratio, output_directory } = options; const results: BatchConvertResult[] = []; // 处理文件路径方式 if (input_paths && input_paths.length > 0) { for (const inputPath of input_paths) { try { const outputPath = output_directory ? path.join(output_directory, this.generateOutputFilename(inputPath, output_format)) : this.generateOutputPath(inputPath, output_format); await this.convertImage({ input_path: inputPath, output_format, quality, width, height, maintain_aspect_ratio, output_path: outputPath }); results.push({ success: true, output_path: outputPath }); } catch (error) { results.push({ success: false, error: error instanceof Error ? error.message : String(error) }); } } } // 处理上传文件方式 if (input_files && input_files.length > 0) { for (const inputFile of input_files) { try { const outputPath = output_directory ? path.join(output_directory, this.generateOutputFilename(inputFile.filename, output_format)) : this.generateOutputPath(inputFile.filename, output_format); await this.convertImage({ input_data: inputFile.data, input_filename: inputFile.filename, output_format, quality, width, height, maintain_aspect_ratio, output_path: outputPath }); results.push({ success: true, output_path: outputPath }); } catch (error) { results.push({ success: false, error: error instanceof Error ? error.message : String(error) }); } } } return results; } /** * 获取图片信息 */ async getImageInfo(imagePath?: string, imageData?: Buffer | string): Promise<ImageInfo> { try { let buffer: Buffer; let size: number; if (imageData) { if (typeof imageData === 'string') { const base64Data = imageData.includes(',') ? imageData.split(',')[1] : imageData; buffer = Buffer.from(base64Data, 'base64'); } else { buffer = imageData; } size = buffer.length; } else if (imagePath) { await fs.access(imagePath); const stats = await fs.stat(imagePath); buffer = await fs.readFile(imagePath); size = stats.size; } else { throw new Error('必须提供imagePath或imageData参数'); } const metadata = await sharp(buffer).metadata(); return { format: metadata.format || 'unknown', width: metadata.width || 0, height: metadata.height || 0, channels: metadata.channels || 0, size: size, space: metadata.space }; } catch (error) { throw new Error(`无法获取图片信息: ${error instanceof Error ? error.message : String(error)}`); } } /** * 获取支持的格式列表 */ getSupportedFormats() { return { input: this.supportedInputFormats, output: this.supportedOutputFormats }; } /** * 从Buffer检测图片格式 */ private async detectFormatFromBuffer(buffer: Buffer): Promise<string> { try { const metadata = await sharp(buffer).metadata(); return metadata.format || 'unknown'; } catch { // 如果Sharp无法识别,尝试通过文件头判断 const header = buffer.slice(0, 12); // PNG if (header[0] === 0x89 && header[1] === 0x50 && header[2] === 0x4E && header[3] === 0x47) { return 'png'; } // JPEG if (header[0] === 0xFF && header[1] === 0xD8) { return 'jpg'; } // GIF if (header[0] === 0x47 && header[1] === 0x49 && header[2] === 0x46) { return 'gif'; } // WebP if (header[8] === 0x57 && header[9] === 0x45 && header[10] === 0x42 && header[11] === 0x50) { return 'webp'; } return 'unknown'; } } /** * 使用Jimp进行转换(用于Sharp不支持的格式) */ private async convertWithJimp(inputBuffer: Buffer, outputPath: string, format: string, options: any): Promise<ConvertResult> { try { let image = await Jimp.read(inputBuffer); // 调整尺寸 if (options.width || options.height) { if (options.width && options.height) { image = image.resize(options.width, options.height); } else if (options.width) { image = image.resize(options.width, Jimp.AUTO); } else if (options.height) { image = image.resize(Jimp.AUTO, options.height); } } // 设置质量 if (options.quality && (format === 'jpg' || format === 'jpeg')) { image = image.quality(options.quality); } await image.writeAsync(outputPath); const stats = await fs.stat(outputPath); return { output_path: outputPath, file_size: stats.size, dimensions: { width: image.getWidth(), height: image.getHeight() }, format }; } catch (error) { throw new Error(`Jimp转换失败: ${error instanceof Error ? error.message : String(error)}`); } } /** * 转换为SVG格式 */ private async convertToSvg(inputBuffer: Buffer, outputPath: string, options: { width?: number; height?: number }): Promise<ConvertResult> { try { // 获取原始图片信息 const metadata = await sharp(inputBuffer).metadata(); const originalWidth = metadata.width || 800; const originalHeight = metadata.height || 600; // 使用指定尺寸或原始尺寸 const svgWidth = options.width || originalWidth; const svgHeight = options.height || originalHeight; // 将图片转换为base64 const imageBuffer = await sharp(inputBuffer) .resize(svgWidth, svgHeight, { fit: 'fill' }) .png() .toBuffer(); const base64Image = imageBuffer.toString('base64'); // 创建SVG内容 const svgContent = `<?xml version="1.0" encoding="UTF-8"?> <svg width="${svgWidth}" height="${svgHeight}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <image x="0" y="0" width="${svgWidth}" height="${svgHeight}" xlink:href="data:image/png;base64,${base64Image}"/> </svg>`; // 写入SVG文件 await fs.writeFile(outputPath, svgContent, 'utf8'); const stats = await fs.stat(outputPath); return { output_path: outputPath, file_size: stats.size, dimensions: { width: svgWidth, height: svgHeight }, format: 'svg' }; } catch (error) { throw new Error(`SVG转换失败: ${error instanceof Error ? error.message : String(error)}`); } } /** * 转换为ICO格式(简化版本) */ private async convertToIco(inputBuffer: Buffer, outputPath: string, options: { width: number; height: number }): Promise<ConvertResult> { try { // 先转换为PNG,然后重命名为ICO(简化处理) const tempPngPath = outputPath.replace('.ico', '.png'); await sharp(inputBuffer) .resize(options.width, options.height, { fit: 'fill' }) .png() .toFile(tempPngPath); // 将PNG重命名为ICO await fs.rename(tempPngPath, outputPath); const stats = await fs.stat(outputPath); return { output_path: outputPath, file_size: stats.size, dimensions: { width: options.width, height: options.height }, format: 'ico' }; } catch (error) { throw new Error(`ICO转换失败: ${error instanceof Error ? error.message : String(error)}`); } } /** * 获取文件格式 */ private getFileFormat(filePath: string): string { return path.extname(filePath).toLowerCase().replace('.', ''); } /** * 生成输出文件路径 */ private generateOutputPath(inputPath: string, outputFormat: string): string { const dir = path.dirname(inputPath); const name = path.parse(inputPath).name; return path.join(dir, `${name}_converted.${outputFormat}`); } /** * 生成输出文件名 */ private generateOutputFilename(inputPath: string, outputFormat: string): string { const name = path.parse(inputPath).name; return `${name}_converted.${outputFormat}`; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pickstar-2002/image-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server