// ============================================================ // canvas/thumbnail.ts — 画布缩略图生成 // // 职责: // - 生成画布专用缩略图(返回 base64 数据) // - 支持缓存优先策略(.cache 目录) // // 优先级: // 1. .cache/{filename}.webp — 预生成 WEBP 缩略图(最快) // 2. .cache/{filename} — 预生成 PNG 缩略图 // 3. sharp — 实时缩放(可选依赖) // 4. 原始文件字节 — 浏览器端缩放回退 // ============================================================ import fs from 'node:fs'; import path from 'node:path'; import sharp from 'sharp'; import { getMimeType } from './mime'; import type { ThumbnailResult } from './types'; /** * 生成画布缩略图。 * * @param filePath - 源文件路径 * @param size - 缩略图尺寸 * @returns 缩略图数据(base64) */ export async function generateCanvasThumbnail( filePath: string, size: number, ): Promise { const dir = path.dirname(filePath); const baseName = path.basename(filePath); const cacheDir = path.join(dir, '.cache'); // ── 优先级 1:.cache/{filename}.webp ── const cachedWebp = path.join(cacheDir, `${baseName}.webp`); if (fs.existsSync(cachedWebp)) { const buffer = fs.readFileSync(cachedWebp); return { data: buffer.toString('base64'), mimeType: 'image/webp', width: 0, height: 0, }; } // ── 优先级 2:.cache/{filename}(PNG 副本) ── const cachedPng = path.join(cacheDir, baseName); if (fs.existsSync(cachedPng)) { const buffer = fs.readFileSync(cachedPng); return { data: buffer.toString('base64'), mimeType: 'image/png', width: 0, height: 0, }; } if (sharp) { const image = sharp(filePath); const metadata = await image.metadata(); const resized = await image .resize(size, size, { fit: 'inside', withoutEnlargement: true }) .jpeg({ quality: 85 }) .toBuffer(); return { data: resized.toString('base64'), mimeType: 'image/jpeg', width: metadata.width || 0, height: metadata.height || 0, }; } // ── 优先级 4:回退原始文件 ── const buffer = fs.readFileSync(filePath); const mimeType = getMimeType(filePath); return { data: buffer.toString('base64'), mimeType, width: 0, height: 0, }; }