import { Injectable, Logger } from '@nestjs/common'; import sharp from 'sharp'; import { ProcessImageDto } from './dto'; interface WatermarkPosition { left: number; top: number; } @Injectable() export class ImageProcessorService { private readonly logger = new Logger(ImageProcessorService.name); async processImage( inputBuffer: Buffer, options: ProcessImageDto, ): Promise { let pipeline = sharp(inputBuffer); if (options.width || options.height) { pipeline = pipeline.resize(options.width, options.height, { fit: (options.fit as any) || 'inside', withoutEnlargement: true, }); } if (options.format) { pipeline = pipeline.toFormat(options.format, { quality: options.quality || 80, }); } else if (options.quality) { const metadata = await sharp(inputBuffer).metadata(); const format = metadata.format as keyof sharp.FormatEnum; if (format) { pipeline = pipeline.toFormat(format, { quality: options.quality, }); } } if (options.watermarkText || options.watermarkImage) { pipeline = await this.applyWatermark( pipeline, inputBuffer, options as ProcessImageDto & { watermarkText?: string; watermarkImage?: string }, ); } return pipeline.toBuffer(); } private async applyWatermark( pipeline: sharp.Sharp, originalBuffer: Buffer, options: ProcessImageDto & { watermarkText?: string; watermarkImage?: string }, ): Promise { const metadata = await sharp(originalBuffer).metadata(); const width = metadata.width || 0; const height = metadata.height || 0; let watermarkBuffer: Buffer; if (options.watermarkImage) { watermarkBuffer = Buffer.from(options.watermarkImage, 'base64'); } else if (options.watermarkText) { watermarkBuffer = await this.createTextWatermark( options.watermarkText, width, height, ); } else { return pipeline; } const position = this.calculatePosition( width, height, options.watermarkPosition || 'bottom-right', ); return pipeline.composite([ { input: watermarkBuffer, left: position.left, top: position.top, opacity: options.watermarkOpacity || 0.3, }, ]); } private async createTextWatermark( text: string, _width: number, _height: number, ): Promise { const svg = ` ${text} `; return sharp(Buffer.from(svg)).png().toBuffer(); } private calculatePosition( imageWidth: number, imageHeight: number, position: string, watermarkWidth = 300, watermarkHeight = 100, ): WatermarkPosition { const padding = 20; switch (position) { case 'top-left': return { left: padding, top: padding }; case 'top-right': return { left: imageWidth - watermarkWidth - padding, top: padding }; case 'bottom-left': return { left: padding, top: imageHeight - watermarkHeight - padding }; case 'center': return { left: (imageWidth - watermarkWidth) / 2, top: (imageHeight - watermarkHeight) / 2, }; case 'bottom-right': default: return { left: imageWidth - watermarkWidth - padding, top: imageHeight - watermarkHeight - padding, }; } } async getImageMetadata(buffer: Buffer): Promise { return sharp(buffer).metadata(); } async generateThumbnail( buffer: Buffer, size = 200, ): Promise { return sharp(buffer) .resize(size, size, { fit: 'cover', withoutEnlargement: true, }) .jpeg({ quality: 80 }) .toBuffer(); } async optimizeImage( buffer: Buffer, maxWidth = 1920, quality = 85, ): Promise { return sharp(buffer) .resize(maxWidth, null, { withoutEnlargement: true, }) .jpeg({ quality }) .toBuffer(); } }