Files
amz-pic-flow/lib/image-processor.js

339 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 图片处理工具模块
* 使用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
};