同步工作区剩余变更,主要包括: - 营销页面组件与布局持续优化(about/news/services/solutions/team 等) - 详情页四层叙事组件、布局组件、UI 组件调整 - CMS 数据模型、API 路由、权限、工作流、站内通知、媒体管理扩展 - 新增/补充单元测试与 E2E 测试(cms-workflow.spec.ts 等) - ESLint 9 迁移、jest/tsconfig 配置更新、依赖调整 - 新增 ADR、CMS 评估文档、Release Review / Acceptance 报告 - 移除水墨装饰组件与大体积未使用字体文件
118 lines
2.9 KiB
TypeScript
118 lines
2.9 KiB
TypeScript
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<GeneratedDerivatives> {
|
|
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;
|
|
}
|