import { BadRequestException } from '@nestjs/common'; import { activityPosterResizeTarget, activityPosterTemplateTooLarge, } from '@dukang/shared-types'; import sharp from 'sharp'; const JPEG_QUALITY = 85; export type ActivityPosterUploadPrepared = { buffer: Buffer; mimeType: string; width: number; height: number; originalWidth: number; originalHeight: number; compressed: boolean; }; function resolveOutputMime(hasAlpha: boolean, inputMime?: string): string { if (hasAlpha) return 'image/png'; if (inputMime === 'image/webp') return 'image/jpeg'; if (inputMime?.startsWith('image/')) return inputMime; return 'image/jpeg'; } async function encodeImage( pipeline: sharp.Sharp, mimeType: string, ): Promise { if (mimeType === 'image/png') { return pipeline.png().toBuffer(); } if (mimeType === 'image/jpeg' || mimeType === 'image/jpg') { return pipeline.jpeg({ quality: JPEG_QUALITY }).toBuffer(); } if (mimeType === 'image/webp') { return pipeline.webp({ quality: JPEG_QUALITY }).toBuffer(); } return pipeline.jpeg({ quality: JPEG_QUALITY }).toBuffer(); } export async function prepareActivityPosterUpload(input: { buffer: Buffer; mimeType?: string; }): Promise { if (!input.buffer?.length) { throw new BadRequestException('活动图文件为空'); } if (input.mimeType && !input.mimeType.startsWith('image/')) { throw new BadRequestException('活动图仅支持图片文件'); } const meta = await sharp(input.buffer, { failOn: 'none' }).metadata(); if (!meta.width || !meta.height) { throw new BadRequestException('活动图无法读取尺寸,请换一张图片'); } const originalWidth = meta.width; const originalHeight = meta.height; const resizeTarget = activityPosterResizeTarget(originalWidth, originalHeight); if (!resizeTarget) { return { buffer: input.buffer, mimeType: input.mimeType || 'image/jpeg', width: originalWidth, height: originalHeight, originalWidth, originalHeight, compressed: false, }; } const hasAlpha = meta.hasAlpha === true; const outputMime = resolveOutputMime(hasAlpha, input.mimeType); const buffer = await encodeImage( sharp(input.buffer, { failOn: 'none' }).resize(resizeTarget.width, resizeTarget.height, { fit: 'inside', withoutEnlargement: true, }), outputMime, ); const metaOut = await sharp(buffer).metadata(); const width = metaOut.width ?? resizeTarget.width; const height = metaOut.height ?? resizeTarget.height; if (activityPosterTemplateTooLarge(width, height)) { throw new BadRequestException('活动图压缩后仍超出尺寸限制,请换一张较小的图片'); } return { buffer, mimeType: outputMime, width, height, originalWidth, originalHeight, compressed: true, }; }