fischerX/services/api/src/modules/file/image-processor.service.ts

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();
}
}