172 lines
4.3 KiB
TypeScript
172 lines
4.3 KiB
TypeScript
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<Buffer> {
|
|
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<sharp.Sharp> {
|
|
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<Buffer> {
|
|
const svg = `
|
|
<svg width="300" height="100">
|
|
<style>
|
|
.watermark {
|
|
fill: rgba(255, 255, 255, 0.8);
|
|
font-size: 24px;
|
|
font-family: Arial, sans-serif;
|
|
font-weight: bold;
|
|
}
|
|
</style>
|
|
<text x="150" y="60" text-anchor="middle" class="watermark">${text}</text>
|
|
</svg>
|
|
`;
|
|
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<sharp.Metadata> {
|
|
return sharp(buffer).metadata();
|
|
}
|
|
|
|
async generateThumbnail(
|
|
buffer: Buffer,
|
|
size = 200,
|
|
): Promise<Buffer> {
|
|
return sharp(buffer)
|
|
.resize(size, size, {
|
|
fit: 'cover',
|
|
withoutEnlargement: true,
|
|
})
|
|
.jpeg({ quality: 80 })
|
|
.toBuffer();
|
|
}
|
|
|
|
async optimizeImage(
|
|
buffer: Buffer,
|
|
maxWidth = 1920,
|
|
quality = 85,
|
|
): Promise<Buffer> {
|
|
return sharp(buffer)
|
|
.resize(maxWidth, null, {
|
|
withoutEnlargement: true,
|
|
})
|
|
.jpeg({ quality })
|
|
.toBuffer();
|
|
}
|
|
}
|