import sharp from 'sharp'; import type { GeneratedDerivatives, ImageDerivative } from './types'; export const THUMBNAIL_SIZE = 320; const IMAGE_MIME_PREFIX = 'image/'; /** * 判断 MIME 类型是否为图片 */ export function isImage(mimeType: string): boolean { return mimeType.startsWith(IMAGE_MIME_PREFIX); } /** * 获取图片宽高元数据 * 非图片或解析失败时返回 null */ export async function getImageMetadata( buffer: Buffer ): Promise<{ width: number; height: number } | null> { try { const metadata = await sharp(buffer).metadata(); if (!metadata.width || !metadata.height) return null; return { width: metadata.width, height: metadata.height }; } catch { return null; } } function buildDerivativeFileName(originalName: string, suffix: string, ext: string): string { const baseName = originalName.replace(/\.[^.]+$/, ''); return `${baseName}-${suffix}.${ext}`; } function buildDerivative( originalName: string, suffix: string, ext: string, width: number, height: number, buffer: Buffer ): ImageDerivative { const fileName = buildDerivativeFileName(originalName, suffix, ext); return { path: `public/uploads/${fileName}`, url: `/uploads/${fileName}`, width, height, buffer, }; } /** * 为图片生成缩略图、WebP、AVIF 派生格式 * 非图片返回空对象 * * 实现说明: * - 缩略图限制长边为 THUMBNAIL_SIZE,保持比例 * - WebP/AVIF 保持原图尺寸,用于现代浏览器优化 */ export async function generateDerivatives( buffer: Buffer, mimeType: string ): Promise { if (!isImage(mimeType)) { return {}; } const metadata = await getImageMetadata(buffer); if (!metadata) { return {}; } const originalName = `image-${Date.now()}`; const derivatives: GeneratedDerivatives = {}; // 缩略图 const thumbnailBuffer = await sharp(buffer) .resize(THUMBNAIL_SIZE, THUMBNAIL_SIZE, { fit: 'inside', withoutEnlargement: true }) .toBuffer(); const thumbnailMeta = await sharp(thumbnailBuffer).metadata(); derivatives.thumbnail = buildDerivative( originalName, 'thumbnail', 'webp', thumbnailMeta.width || THUMBNAIL_SIZE, thumbnailMeta.height || THUMBNAIL_SIZE, thumbnailBuffer ); // WebP const webpBuffer = await sharp(buffer).webp({ quality: 85 }).toBuffer(); const webpMeta = await sharp(webpBuffer).metadata(); derivatives.webp = buildDerivative( originalName, 'webp', 'webp', webpMeta.width || metadata.width, webpMeta.height || metadata.height, webpBuffer ); // AVIF const avifBuffer = await sharp(buffer).avif({ quality: 80 }).toBuffer(); const avifMeta = await sharp(avifBuffer).metadata(); derivatives.avif = buildDerivative( originalName, 'avif', 'avif', avifMeta.width || metadata.width, avifMeta.height || metadata.height, avifBuffer ); return derivatives; }