102 lines
2.8 KiB
TypeScript
102 lines
2.8 KiB
TypeScript
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<Buffer> {
|
|
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<ActivityPosterUploadPrepared> {
|
|
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,
|
|
};
|
|
}
|