/** * 图片处理工具模块 * 使用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 = ` ${textElements} `; 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 = ` "${escapeXml(text)}" `; 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 };