Upload latest code and optimized prompts (v6)

This commit is contained in:
tony
2025-12-14 19:52:54 +08:00
commit f5cb1042ae
42 changed files with 15302 additions and 0 deletions

338
lib/image-processor.js Normal file
View 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
/**
* 保存图片到文件
*/
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
};