Spaces:
Sleeping
Sleeping
| import sharp from 'sharp'; | |
| export interface ImageOptimizationOptions { | |
| quality?: number; | |
| format?: 'webp' | 'jpeg' | 'png'; | |
| width?: number; | |
| height?: number; | |
| cropToCenter?: boolean; | |
| cropHeight?: number; | |
| } | |
| export interface ContentImageOptions { | |
| quality?: number; | |
| targetWidth?: number; | |
| targetHeight?: number; | |
| } | |
| export class ImageOptimizer { | |
| /** | |
| * データURLをBufferに変換 | |
| */ | |
| static dataUrlToBuffer(dataUrl: string): Buffer { | |
| const base64Data = dataUrl.replace(/^data:image\/\w+;base64,/, ''); | |
| return Buffer.from(base64Data, 'base64'); | |
| } | |
| /** | |
| * BufferをデータURLに変換 | |
| */ | |
| static bufferToDataUrl(buffer: Buffer, mimeType: string = 'image/webp'): string { | |
| return `data:${mimeType};base64,${buffer.toString('base64')}`; | |
| } | |
| /** | |
| * 画像をWebP形式に圧縮 | |
| */ | |
| static async compressToWebP(input: Buffer | string, quality: number = 65): Promise<Buffer> { | |
| const buffer = typeof input === 'string' ? this.dataUrlToBuffer(input) : input; | |
| return await sharp(buffer).webp({ quality }).toBuffer(); | |
| } | |
| /** | |
| * FV画像用の最適化(WebP変換のみ) | |
| */ | |
| static async optimizeFvImage(input: Buffer | string, options: ImageOptimizationOptions = {}): Promise<string> { | |
| const { quality = 65, format = 'webp' } = options; | |
| const buffer = typeof input === 'string' ? this.dataUrlToBuffer(input) : input; | |
| let processedBuffer: Buffer; | |
| if (format === 'webp') { | |
| processedBuffer = await sharp(buffer).webp({ quality }).toBuffer(); | |
| } else if (format === 'jpeg') { | |
| processedBuffer = await sharp(buffer).jpeg({ quality }).toBuffer(); | |
| } else { | |
| processedBuffer = await sharp(buffer).png({ quality }).toBuffer(); | |
| } | |
| const mimeType = format === 'webp' ? 'image/webp' : format === 'jpeg' ? 'image/jpeg' : 'image/png'; | |
| return this.bufferToDataUrl(processedBuffer, mimeType); | |
| } | |
| /** | |
| * コンテンツ画像用の最適化(リサイズ + クロップ + WebP変換) | |
| */ | |
| static async optimizeContentImage(input: Buffer | string, options: ContentImageOptions = {}): Promise<string> { | |
| const { | |
| quality = 50, // 圧縮率を50%に変更 | |
| targetWidth = 400, // 幅を400pxに縮小 | |
| targetHeight = 300, // 高さを300pxに縮小 | |
| } = options; | |
| const buffer = typeof input === 'string' ? this.dataUrlToBuffer(input) : input; | |
| // まず400x400にリサイズ | |
| const resizedBuffer = await sharp(buffer) | |
| .resize(400, 400, { | |
| fit: 'cover', | |
| position: 'center', | |
| }) | |
| .toBuffer(); | |
| // 上下50pxをクロップして400x300に | |
| const croppedBuffer = await sharp(resizedBuffer) | |
| .extract({ | |
| left: 0, | |
| top: 50, // クロップ位置も調整 | |
| width: targetWidth, | |
| height: targetHeight, | |
| }) | |
| .webp({ quality }) | |
| .toBuffer(); | |
| return this.bufferToDataUrl(croppedBuffer, 'image/webp'); | |
| } | |
| /** | |
| * 汎用的な画像最適化メソッド | |
| */ | |
| static async optimizeImage(input: Buffer | string, options: ImageOptimizationOptions = {}): Promise<string> { | |
| const { quality = 65, format = 'webp', width, height, cropToCenter = false, cropHeight } = options; | |
| const buffer = typeof input === 'string' ? this.dataUrlToBuffer(input) : input; | |
| let pipeline = sharp(buffer); | |
| // リサイズが指定されている場合 | |
| if (width || height) { | |
| pipeline = pipeline.resize(width, height, { | |
| fit: cropToCenter ? 'cover' : 'inside', | |
| position: 'center', | |
| }); | |
| } | |
| // クロップが指定されている場合 | |
| if (cropToCenter && width && cropHeight) { | |
| const metadata = await sharp(buffer).metadata(); | |
| const currentHeight = metadata.height || 0; | |
| if (currentHeight > cropHeight) { | |
| const topOffset = Math.floor((currentHeight - cropHeight) / 2); | |
| pipeline = pipeline.extract({ | |
| left: 0, | |
| top: topOffset, | |
| width: width, | |
| height: cropHeight, | |
| }); | |
| } | |
| } | |
| // フォーマット変換 | |
| let processedBuffer: Buffer; | |
| if (format === 'webp') { | |
| processedBuffer = await pipeline.webp({ quality }).toBuffer(); | |
| } else if (format === 'jpeg') { | |
| processedBuffer = await pipeline.jpeg({ quality }).toBuffer(); | |
| } else { | |
| processedBuffer = await pipeline.png({ quality }).toBuffer(); | |
| } | |
| const mimeType = format === 'webp' ? 'image/webp' : format === 'jpeg' ? 'image/jpeg' : 'image/png'; | |
| return this.bufferToDataUrl(processedBuffer, mimeType); | |
| } | |
| /** | |
| * 画像のメタデータを取得 | |
| */ | |
| static async getImageMetadata(input: Buffer | string): Promise<sharp.Metadata> { | |
| const buffer = typeof input === 'string' ? this.dataUrlToBuffer(input) : input; | |
| return await sharp(buffer).metadata(); | |
| } | |
| /** | |
| * 画像サイズの計算(圧縮前後の比較用) | |
| */ | |
| static calculateDataUrlSize(dataUrl: string): number { | |
| const base64Data = dataUrl.replace(/^data:image\/\w+;base64,/, ''); | |
| return Buffer.from(base64Data, 'base64').length; | |
| } | |
| /** | |
| * 圧縮率の計算 | |
| */ | |
| static calculateCompressionRatio(originalSize: number, compressedSize: number): number { | |
| return Math.round((1 - compressedSize / originalSize) * 100); | |
| } | |
| } | |