/**
* 图片处理工具模块
* 使用Sharp库实现:抠图、合成、叠加文字/标注
*/
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
/**
* 调整图片大小,保持比例
*/
async function resizeImage(inputPath, width, height, fit = 'contain') {
const buffer = await sharp(inputPath)
.resize(width, height, { fit, background: { r: 255, g: 255, b: 255, alpha: 0 } })
.toBuffer();
return buffer;
}
/**
* 将图片转换为PNG(保持透明度)
*/
async function toPng(inputPath) {
return await sharp(inputPath).png().toBuffer();
}
/**
* 创建纯色背景
*/
async function createSolidBackground(width, height, color = '#FFFFFF') {
// 解析颜色
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
return await sharp({
create: {
width,
height,
channels: 3,
background: { r, g, b }
}
}).jpeg().toBuffer();
}
/**
* 创建渐变背景
*/
async function createGradientBackground(width, height, color1 = '#F5EDE4', color2 = '#FFFFFF') {
// 创建简单的渐变效果(从上到下)
const svg = `
`;
return await sharp(Buffer.from(svg)).jpeg().toBuffer();
}
/**
* 合成多个图层
* @param {Buffer} baseImage - 底图
* @param {Array} layers - 图层数组 [{buffer, left, top, width?, height?}]
*/
async function compositeImages(baseImage, layers) {
let composite = sharp(baseImage);
const compositeInputs = [];
for (const layer of layers) {
let input = layer.buffer;
// 如果需要调整大小
if (layer.width || layer.height) {
input = await sharp(layer.buffer)
.resize(layer.width, layer.height, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
.toBuffer();
}
compositeInputs.push({
input,
left: layer.left || 0,
top: layer.top || 0
});
}
return await composite.composite(compositeInputs).toBuffer();
}
/**
* 在图片上叠加文字
*/
async function addTextOverlay(baseImage, texts) {
// texts: [{text, x, y, fontSize, color, fontWeight, align}]
const metadata = await sharp(baseImage).metadata();
const width = metadata.width;
const height = metadata.height;
// 创建SVG文字层
const textElements = texts.map(t => {
const fontSize = t.fontSize || 40;
const color = t.color || '#333333';
const fontWeight = t.fontWeight || 'bold';
const textAnchor = t.align === 'center' ? 'middle' : (t.align === 'right' ? 'end' : 'start');
return `${escapeXml(t.text)}`;
}).join('\n');
const svg = `
`;
return await sharp(baseImage)
.composite([{ input: Buffer.from(svg), top: 0, left: 0 }])
.toBuffer();
}
/**
* 创建圆形裁剪的图片(用于细节放大镜效果)
*/
async function createCircularCrop(inputBuffer, diameter) {
const circle = Buffer.from(`
`);
const resized = await sharp(inputBuffer)
.resize(diameter, diameter, { fit: 'cover' })
.toBuffer();
return await sharp(resized)
.composite([{ input: circle, blend: 'dest-in' }])
.png()
.toBuffer();
}
/**
* 添加圆形边框
*/
async function addCircleBorder(inputBuffer, diameter, borderWidth = 3, borderColor = '#2D4A3E') {
const r = parseInt(borderColor.slice(1, 3), 16);
const g = parseInt(borderColor.slice(3, 5), 16);
const b = parseInt(borderColor.slice(5, 7), 16);
const borderSvg = Buffer.from(`
`);
return await sharp(inputBuffer)
.composite([{ input: borderSvg, top: 0, left: 0 }])
.toBuffer();
}
/**
* 创建带箭头的标注线
*/
async function createArrowLine(width, height, fromX, fromY, toX, toY, color = '#2D4A3E') {
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
// 计算箭头角度
const angle = Math.atan2(toY - fromY, toX - fromX);
const arrowLength = 15;
const arrowAngle = Math.PI / 6;
const arrow1X = toX - arrowLength * Math.cos(angle - arrowAngle);
const arrow1Y = toY - arrowLength * Math.sin(angle - arrowAngle);
const arrow2X = toX - arrowLength * Math.cos(angle + arrowAngle);
const arrow2Y = toY - arrowLength * Math.sin(angle + arrowAngle);
const svg = `
`;
return Buffer.from(svg);
}
/**
* 创建标题横幅
*/
async function createTitleBanner(width, height, text, bgColor = '#2D4A3E', textColor = '#FFFFFF') {
const fontSize = Math.floor(height * 0.5);
const svg = `
`;
return await sharp(Buffer.from(svg)).png().toBuffer();
}
/**
* 将图片转为灰度(用于竞品对比)
*/
async function toGrayscale(inputBuffer) {
return await sharp(inputBuffer).grayscale().toBuffer();
}
/**
* 调整图片亮度/对比度
*/
async function adjustBrightness(inputBuffer, brightness = 1.0) {
return await sharp(inputBuffer)
.modulate({ brightness })
.toBuffer();
}
/**
* 添加阴影效果
*/
async function addShadow(inputBuffer, offsetX = 5, offsetY = 5, blur = 10, opacity = 0.3) {
const metadata = await sharp(inputBuffer).metadata();
const width = metadata.width + offsetX + blur * 2;
const height = metadata.height + offsetY + blur * 2;
// 创建阴影层
const shadowBuffer = await sharp(inputBuffer)
.greyscale()
.modulate({ brightness: 0 })
.blur(blur)
.toBuffer();
// 创建透明背景
const background = await sharp({
create: {
width,
height,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 0 }
}
}).png().toBuffer();
// 合成
return await sharp(background)
.composite([
{ input: shadowBuffer, left: offsetX + blur, top: offsetY + blur, blend: 'over' },
{ input: inputBuffer, left: blur, top: blur }
])
.toBuffer();
}
/**
* 裁剪图片的特定区域
*/
async function cropRegion(inputBuffer, left, top, width, height) {
return await sharp(inputBuffer)
.extract({ left, top, width, height })
.toBuffer();
}
/**
* 辅助函数:转义XML特殊字符
*/
function escapeXml(text) {
return text
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
/**
* 保存图片到文件
*/
async function saveImage(buffer, outputPath, format = 'jpeg', quality = 90) {
let pipeline = sharp(buffer);
if (format === 'jpeg' || format === 'jpg') {
pipeline = pipeline.jpeg({ quality });
} else if (format === 'png') {
pipeline = pipeline.png();
}
await pipeline.toFile(outputPath);
return outputPath;
}
/**
* 读取图片为Buffer
*/
async function readImage(imagePath) {
return fs.readFileSync(imagePath);
}
/**
* 获取图片尺寸
*/
async function getImageSize(inputPath) {
const metadata = await sharp(inputPath).metadata();
return { width: metadata.width, height: metadata.height };
}
module.exports = {
resizeImage,
toPng,
createSolidBackground,
createGradientBackground,
compositeImages,
addTextOverlay,
createCircularCrop,
addCircleBorder,
createArrowLine,
createTitleBanner,
toGrayscale,
adjustBrightness,
addShadow,
cropRegion,
saveImage,
readImage,
getImageSize
};