/** * 图片处理工具 * * 提供图片文件读取、格式检测、base64 编码等功能 */ import * as fs from 'fs/promises'; import * as path from 'path'; /** 支持的图片扩展名 */ export const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp']; /** 图片 MIME 类型映射 */ const MIME_TYPES: Record = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', }; /** 图片信息 */ export interface ImageInfo { /** 原始文件路径 */ path: string; /** 文件名 */ filename: string; /** 扩展名 */ extension: string; /** MIME 类型 */ mimeType: string; /** 文件大小(字节) */ size: number; /** base64 编码的数据 */ base64: string; /** 完整的 data URL */ dataUrl: string; } /** 图片加载结果 */ export interface ImageLoadResult { success: boolean; image?: ImageInfo; error?: string; } /** * 判断文件路径是否为图片 */ export function isImagePath(filePath: string): boolean { const ext = path.extname(filePath).toLowerCase(); return IMAGE_EXTENSIONS.includes(ext); } /** * 从输入文本中提取图片引用 * 支持多种格式: * 1. @path/to/image.png(不带空格的路径) * 2. @"path/to/image with spaces.png"(带空格的路径用引号包裹) * 3. @/path/to/image.png(绝对路径,自动匹配到图片扩展名结束) * * @param input 用户输入 * @returns 图片路径列表和去除图片引用后的文本 */ export function extractImageReferences(input: string): { imagePaths: string[]; textContent: string; } { const imagePaths: string[] = []; let textContent = input; // 模式1: 带引号的路径 @"path/to/image.png" 或 @'path/to/image.png' const quotedMatches = [...input.matchAll(/@["']([^"']+\.(?:png|jpg|jpeg|gif|webp))["']/gi)]; for (const match of quotedMatches) { imagePaths.push(match[1]); textContent = textContent.replace(match[0], ' '); } // 模式2: 绝对路径(以 / 或 ~ 开头,匹配到图片扩展名结束) // 支持路径中包含空格 const absoluteMatches = [...textContent.matchAll(/@([/~][^\n]*?\.(?:png|jpg|jpeg|gif|webp))(?=\s|$)/gi)]; for (const match of absoluteMatches) { if (!imagePaths.includes(match[1])) { imagePaths.push(match[1]); textContent = textContent.replace(match[0], ' '); } } // 模式3: 相对路径(不以 / 开头,不包含空格) const relativeMatches = [...textContent.matchAll(/@((?:\.\/|\.\.\/)?[^\s@"'/][^\s@"']*\.(?:png|jpg|jpeg|gif|webp))/gi)]; for (const match of relativeMatches) { if (!imagePaths.includes(match[1])) { imagePaths.push(match[1]); textContent = textContent.replace(match[0], ' '); } } // 清理多余空格 textContent = textContent.replace(/\s+/g, ' ').trim(); return { imagePaths, textContent }; } /** * 加载图片文件 * @param filePath 图片路径(相对或绝对) * @param workdir 工作目录(用于解析相对路径) */ export async function loadImage( filePath: string, workdir: string = process.cwd() ): Promise { try { // 解析路径 const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(workdir, filePath); // 检查扩展名 const ext = path.extname(absolutePath).toLowerCase(); if (!IMAGE_EXTENSIONS.includes(ext)) { return { success: false, error: `不支持的图片格式: ${ext}。支持的格式: ${IMAGE_EXTENSIONS.join(', ')}`, }; } // 读取文件 const buffer = await fs.readFile(absolutePath); const stats = await fs.stat(absolutePath); // 转换为 base64 const base64 = buffer.toString('base64'); const mimeType = MIME_TYPES[ext] || 'application/octet-stream'; const dataUrl = `data:${mimeType};base64,${base64}`; return { success: true, image: { path: absolutePath, filename: path.basename(absolutePath), extension: ext, mimeType, size: stats.size, base64, dataUrl, }, }; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { return { success: false, error: `图片文件不存在: ${filePath}`, }; } return { success: false, error: `加载图片失败: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * 批量加载图片 * @param filePaths 图片路径列表 * @param workdir 工作目录 */ export async function loadImages( filePaths: string[], workdir: string = process.cwd() ): Promise<{ images: ImageInfo[]; errors: Array<{ path: string; error: string }>; }> { const images: ImageInfo[] = []; const errors: Array<{ path: string; error: string }> = []; for (const filePath of filePaths) { const result = await loadImage(filePath, workdir); if (result.success && result.image) { images.push(result.image); } else { errors.push({ path: filePath, error: result.error || '未知错误' }); } } return { images, errors }; } /** * 格式化文件大小 */ export function formatFileSize(bytes: number): string { if (bytes < 1024) { return `${bytes}B`; } else if (bytes < 1024 * 1024) { return `${(bytes / 1024).toFixed(1)}KB`; } else { return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; } }