import { existsSync, mkdirSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import sharp from 'sharp' import { buildTimePhotoLoader } from './photo-loader.js' import { renderSVGText, wrapSVGText } from './svg-text-renderer.js' // 获取最新的照片 async function getLatestPhotos(count = 4) { const photos = buildTimePhotoLoader.getPhotos() // 按拍摄时间排序,获取最新的照片 const sortedPhotos = photos.sort((a, b) => { if (!a?.exif?.DateTimeOriginal || !b?.exif?.DateTimeOriginal) { return 0 } const aDate = (a.exif.DateTimeOriginal as unknown as string) || a.lastModified const bDate = (b.exif.DateTimeOriginal as unknown as string) || b.lastModified return bDate.localeCompare(aDate) }) return sortedPhotos.slice(0, count) } // 下载并处理照片缩略图 async function downloadAndProcessThumbnail(thumbnailUrl: string, size = 150) { try { // 如果是本地路径,直接读取 if (thumbnailUrl.startsWith('/')) { const localPath = join(process.cwd(), 'public', thumbnailUrl) if (existsSync(localPath)) { return await sharp(localPath) .resize(size, size, { fit: 'cover' }) .png() .toBuffer() } } // 如果是 URL,需要下载(这里先返回 null,后面可以添加网络下载功能) console.warn(`Cannot download thumbnail from URL: ${thumbnailUrl}`) return null } catch (error) { console.warn(`Failed to process thumbnail: ${thumbnailUrl}`, error) return null } } // 创建带特效的照片(旋转、阴影、边框) async function createPhotoWithEffects( imageBuffer: Buffer, size: number, rotation: number, ) { try { // 计算旋转后需要的画布大小 const diagonal = Math.ceil(size * Math.sqrt(2)) const canvasSize = diagonal + 40 // 额外空间用于阴影 // 创建阴影效果的 SVG const shadowSvg = ` ` // 创建阴影层 const shadowBuffer = await sharp(Buffer.from(shadowSvg)).png().toBuffer() // 处理原图片:添加浅灰色边框并旋转(适配黑色主题) const photoWithBorder = await sharp(imageBuffer) .extend({ top: 6, bottom: 6, left: 6, right: 6, background: { r: 240, g: 240, b: 240, alpha: 1 }, }) .png() .toBuffer() // 创建最终画布 const canvas = sharp({ create: { width: canvasSize, height: canvasSize, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 }, }, }) // 计算照片在画布中的位置 const photoX = (canvasSize - size - 12) / 2 const photoY = (canvasSize - size - 12) / 2 // 合成阴影和照片 const result = await canvas .composite([ { input: shadowBuffer, top: 0, left: 0 }, { input: photoWithBorder, top: Math.round(photoY), left: Math.round(photoX), }, ]) .png() .toBuffer() // 旋转整个图像 return await sharp(result) .rotate(rotation, { background: { r: 0, g: 0, b: 0, alpha: 0 } }) .png() .toBuffer() } catch (error) { console.warn('Failed to create photo with effects:', error) // 如果特效失败,返回简单的边框版本(适配黑色主题) return await sharp(imageBuffer) .extend({ top: 4, bottom: 4, left: 4, right: 4, background: { r: 240, g: 240, b: 240, alpha: 1 }, }) .png() .toBuffer() } } interface OGImageOptions { title: string description: string width?: number height?: number outputPath: string includePhotos?: boolean photoCount?: number } export async function generateOGImage(options: OGImageOptions) { const { title, description, width = 1200, height = 630, outputPath, includePhotos = true, photoCount = 4, } = options // 确保输出目录存在 const outputDir = join(process.cwd(), 'public') if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true }) } try { let finalImage: sharp.Sharp if (includePhotos) { // 获取最新照片 const latestPhotos = await getLatestPhotos(photoCount) console.info(`📸 Found ${latestPhotos.length} latest photos`) // 创建基础画布 - 黑色主题 const canvas = sharp({ create: { width, height, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 1 }, }, }) // 创建现代黑色主题渐变背景 const gradientSvg = ` ` const gradientBuffer = await sharp(Buffer.from(gradientSvg)) .png() .toBuffer() // 创建文字层 - 使用 SVG 路径绘制 Helvetica 风格字体 const wrappedTitle = wrapSVGText(title, width - 120, { fontSize: 48, fontWeight: 'bold', }) const wrappedDescription = wrapSVGText(description, width - 120, { fontSize: 24, }) const footerText = `Latest Photos • Generated on ${new Date().toLocaleDateString()}` const titleSVG = renderSVGText(wrappedTitle, 60, 72, { fontSize: 48, fontWeight: 'bold', color: 'white', letterSpacing: 2, }) const descriptionSVG = renderSVGText(wrappedDescription, 60, 146, { fontSize: 24, color: 'rgba(255,255,255,0.9)', letterSpacing: 1, }) const footerSVG = renderSVGText(footerText, 60, 556, { fontSize: 18, color: 'rgba(255,255,255,0.7)', letterSpacing: 0.5, }) const textSvg = ` ${titleSVG} ${descriptionSVG} ${footerSVG} ` const textBuffer = await sharp(Buffer.from(textSvg)).png().toBuffer() // 准备合成图层 const composite: sharp.OverlayOptions[] = [ { input: gradientBuffer, top: 0, left: 0 }, { input: textBuffer, top: 0, left: 0 }, ] // 处理照片缩略图 - 创建倾斜叠加效果 const photoSize = 160 const baseX = 580 const baseY = 200 // 往下移动 50px const rotations = [-12, 5, -8, 10] // 每张照片的旋转角度 const offsets = [ { x: 0, y: 20 }, { x: 90, y: 60 }, { x: 180, y: -10 }, { x: 270, y: 70 }, ] const length = Math.min(latestPhotos.length, photoCount) for (let i = length - 1; i >= 0; i--) { const photo = latestPhotos[i] const thumbnailBuffer = await downloadAndProcessThumbnail( photo.thumbnailUrl, photoSize, ) if (thumbnailBuffer) { const rotation = rotations[i] || 0 const offset = offsets[i] || { x: i * 60, y: 0 } const x = baseX + offset.x const y = baseY + offset.y // 创建带阴影和边框的照片 const photoWithEffects = await createPhotoWithEffects( thumbnailBuffer, photoSize, rotation, ) composite.push({ input: photoWithEffects, top: y, left: x, }) console.info( `📷 Added photo: ${photo.title} at position (${x}, ${y}) with rotation ${rotation}°`, ) } } // 合成最终图像 finalImage = canvas.composite(composite) } else { // 不包含照片的简单版本 - 黑色主题,使用 SVG 路径绘制字体 const simpleWrappedTitle = wrapSVGText(title, width - 120, { fontSize: 72, fontWeight: 'bold', }) const simpleWrappedDescription = wrapSVGText(description, width - 120, { fontSize: 32, }) const simpleFooterText = `Generated on ${new Date().toLocaleDateString()}` const simpleTitleSVG = renderSVGText(simpleWrappedTitle, 60, 152, { fontSize: 72, fontWeight: 'bold', color: 'white', letterSpacing: 3, }) const simpleDescriptionSVG = renderSVGText( simpleWrappedDescription, 60, 256, { fontSize: 32, color: 'rgba(255,255,255,0.9)', letterSpacing: 1.5, }, ) const simpleFooterSVG = renderSVGText(simpleFooterText, 60, 526, { fontSize: 24, color: 'rgba(255,255,255,0.7)', letterSpacing: 1, }) const svgContent = ` ${simpleTitleSVG} ${simpleDescriptionSVG} ${simpleFooterSVG} ` finalImage = sharp(Buffer.from(svgContent)) } // 生成最终图片 const buffer = await finalImage.png().toBuffer() // 写入文件 const fullOutputPath = join(outputDir, outputPath) writeFileSync(fullOutputPath, buffer) console.info(`✅ OG image generated: ${fullOutputPath}`) return fullOutputPath } catch (error) { console.error('❌ Error generating OG image:', error) throw error } }