Upload latest code and optimized prompts (v6)
This commit is contained in:
338
lib/image-processor.js
Normal file
338
lib/image-processor.js
Normal file
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* 图片处理工具模块
|
||||
* 使用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 = `
|
||||
<svg width="${width}" height="${height}">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:${color1};stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:${color2};stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#grad)"/>
|
||||
</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 `<text x="${t.x}" y="${t.y}" font-size="${fontSize}" fill="${color}"
|
||||
font-weight="${fontWeight}" font-family="Arial, sans-serif" text-anchor="${textAnchor}">${escapeXml(t.text)}</text>`;
|
||||
}).join('\n');
|
||||
|
||||
const svg = `
|
||||
<svg width="${width}" height="${height}">
|
||||
${textElements}
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return await sharp(baseImage)
|
||||
.composite([{ input: Buffer.from(svg), top: 0, left: 0 }])
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建圆形裁剪的图片(用于细节放大镜效果)
|
||||
*/
|
||||
async function createCircularCrop(inputBuffer, diameter) {
|
||||
const circle = Buffer.from(`
|
||||
<svg width="${diameter}" height="${diameter}">
|
||||
<circle cx="${diameter/2}" cy="${diameter/2}" r="${diameter/2}" fill="white"/>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
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(`
|
||||
<svg width="${diameter}" height="${diameter}">
|
||||
<circle cx="${diameter/2}" cy="${diameter/2}" r="${diameter/2 - borderWidth/2}"
|
||||
fill="none" stroke="rgb(${r},${g},${b})" stroke-width="${borderWidth}"/>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
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 = `
|
||||
<svg width="${width}" height="${height}">
|
||||
<line x1="${fromX}" y1="${fromY}" x2="${toX}" y2="${toY}"
|
||||
stroke="rgb(${r},${g},${b})" stroke-width="2"/>
|
||||
<polygon points="${toX},${toY} ${arrow1X},${arrow1Y} ${arrow2X},${arrow2Y}"
|
||||
fill="rgb(${r},${g},${b})"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return Buffer.from(svg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建标题横幅
|
||||
*/
|
||||
async function createTitleBanner(width, height, text, bgColor = '#2D4A3E', textColor = '#FFFFFF') {
|
||||
const fontSize = Math.floor(height * 0.5);
|
||||
|
||||
const svg = `
|
||||
<svg width="${width}" height="${height}">
|
||||
<rect width="100%" height="100%" fill="${bgColor}" rx="5" ry="5"/>
|
||||
<text x="${width/2}" y="${height/2 + fontSize/3}" font-size="${fontSize}"
|
||||
fill="${textColor}" font-weight="bold" font-family="Arial, sans-serif"
|
||||
text-anchor="middle">"${escapeXml(text)}"</text>
|
||||
</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, '"')
|
||||
.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
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user