图片处理在Web服务里几乎无处不在:用户头像要统一尺寸,商品图要生成缩略图,内容平台要给原创图片加上水印。Node.js生态中处理图片的库不少,但sharp已经成为事实上的首选。它的底层是libvips,用C语言写成,性能比纯JavaScript方案高出一个数量级,而且安装时通过预编译二进制发布,省去了本地编译的麻烦。

sharp的设计思路是把读取、处理、输出串成一条管道。你只需要调用sharp()传入文件路径或Buffer,然后链式调用缩放、裁剪、合成等方法,最后用toFile()、toBuffer()或toFormat()输出结果。这种API风格很符合Node.js的流式处理习惯,也方便你在处理过程中随时插入条件逻辑。接下来会从安装与基础使用开始,逐步展开缩放、裁剪和水印添加的实践细节。
安装sharp并完成基础图片读取
安装sharp只需要一条命令,但要注意Node.js版本兼容性。sharp的维护者通常会跟进最新的LTS版本,建议使用Node.js 18及以上环境。安装命令如下:
// 使用npm安装
// npm install sharp
const sharp = require('sharp');
async function readImageInfo(inputPath) {
const image = sharp(inputPath);
// 读取图片元数据,不加载全部像素
const metadata = await image.metadata();
console.log('格式:', metadata.format);
console.log('宽度:', metadata.width);
console.log('高度:', metadata.height);
console.log('颜色空间:', metadata.space);
return metadata;
}
// 调用示例
readImageInfo('./input.jpg').catch(console.error);
读取元数据是处理前的必要步骤,因为很多缩放和裁剪决策都依赖原始尺寸。sharp在这里做了优化,metadata()只解析文件头信息,不会把整张图片解码到内存中,所以即使处理超大图片也不会瞬间占用大量内存。得到元数据后,你就可以计算目标宽高比、裁剪坐标等参数。
另一个基础操作是格式转换。sharp支持JPEG、PNG、WebP、AVIF等常见输出格式,转换在写入时通过toFormat()指定,非常直观。例如把PNG转成压缩率更高的WebP:
async function convertToWebP(inputPath, outputPath) {
await sharp(inputPath)
.toFormat('webp', { quality: 80 })
.toFile(outputPath);
console.log('转换完成:', outputPath);
}
convertToWebP('./input.png', './output.webp').catch(console.error);
这段代码演示了sharp管道的基本形态:创建实例、设置输出格式、写入文件。理解这个结构后,缩放和裁剪只是在中间插入更多方法调用,不会改变整体逻辑。
用resize实现等比缩放与填充
缩略图生成是sharp最常用的功能之一。核心方法是resize(width, height, options),其中宽高可以只指定一个,另一个传null或undefined,sharp会按照原始宽高比自动计算。如果想保持比例不变,建议同时设置宽高,并通过fit参数控制缩放策略。
fit参数决定了当目标宽高比与原始宽高比不一致时如何处理。常用的取值有cover、contain、fill和inside。cover会裁掉超出部分,保证完全填满目标尺寸,适合头像和封面图;contain会在目标区域内完整显示图片,周围可能留白;fill直接拉伸图片,不保持比例;inside只缩小不放大,适合生成不影响清晰度的缩略图。下面的代码分别展示了这几种模式:
async function resizeExamples(inputPath) {
const image = sharp(inputPath);
// cover模式:填满200x200,多余部分裁剪
await image
.clone()
.resize(200, 200, { fit: 'cover' })
.toFile('./output-cover.jpg');
// contain模式:完整显示,可能留白
await image
.clone()
.resize(200, 200, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 1 } })
.toFile('./output-contain.jpg');
// inside模式:只缩小,不放大
await image
.clone()
.resize(200, 200, { fit: 'inside' })
.toFile('./output-inside.jpg');
}
resizeExamples('./input.jpg').catch(console.error);
注意上面代码中使用了clone()。sharp实例在调用toFile()等输出方法后并不会重置处理链,为了避免多次复用同一个实例导致状态串扰,通常在需要从同一张原图生成多个结果时使用clone()创建独立副本。另外,background选项可以在contain模式下为留白区域指定颜色,默认是黑色透明。
缩放还可以配合withoutEnlargement属性,它等同于fit: 'inside'的快捷方式,适合防止小图被强行放大。如果需要在缩放时保持最长边不超过某个值,可以使用resize({ width: 800, height: 800, fit: 'inside', withoutEnlargement: true }),这样无论原图是横图还是竖图,最长边都不会超过800像素。
extract精确裁剪与trim自动去边
裁剪分为两种场景:一种是你明确知道要裁切的坐标和尺寸,另一种是根据图像内容自动裁掉空白边缘。前者对应extract()方法,后者对应trim()方法。
extract()接收一个对象,包含left、top、width、height四个属性,单位是像素,坐标原点在左上角。比如要从一张1920x1080的图片中裁出左上角400x300的区域:
async function cropRegion(inputPath, outputPath) {
await sharp(inputPath)
.extract({ left: 0, top: 0, width: 400, height: 300 })
.toFile(outputPath);
console.log('裁剪完成:', outputPath);
}
cropRegion('./input.jpg', './output-crop.jpg').catch(console.error);
实际开发中,裁剪坐标往往需要根据原图尺寸和期望目标宽高比动态计算。例如想从图片中心裁出一个正方形区域,可以先读取元数据,然后以较小的边长为边长计算left和top:
async function cropCenterSquare(inputPath, outputPath) {
const image = sharp(inputPath);
const metadata = await image.metadata();
const side = Math.min(metadata.width, metadata.height);
const left = Math.floor((metadata.width - side) / 2);
const top = Math.floor((metadata.height - side) / 2);
await image
.extract({ left, top, width: side, height: side })
.toFile(outputPath);
}
cropCenterSquare('./input.jpg', './output-square.jpg').catch(console.error);
自动去边使用trim(),它内部会分析图片边缘的颜色,自动找出与背景色相近的连续区域并裁掉。这个功能对扫描件、截图或者带白色边框的图片特别有用。可以设置threshold来控制颜色差异的容忍度,数值越小越严格,只有接近完全相同的颜色才会被裁掉。
async function autoTrim(inputPath, outputPath) {
await sharp(inputPath)
.trim({ threshold: 10 })
.toFile(outputPath);
console.log('自动去边完成');
}
autoTrim('./input-with-border.png', './output-trimmed.png').catch(console.error);
用composite添加文字或图片水印
水印添加在sharp中通过composite()方法实现。你可以把另一张图片作为水印叠加到原图上,也可以先创建SVG文本,再把SVG作为叠加层。SVG方式的优势在于文字可以自由控制字体、颜色、透明度和旋转角度,而且不需要额外准备水印图片文件。
先看一个添加文字水印的完整例子。用SVG生成带透明背景的文字,再用composite()把它放在图片右下角:
async function addTextWatermark(inputPath, outputPath, watermarkText) {
const image = sharp(inputPath);
const metadata = await image.metadata();
const padding = 20;
const textWidth = watermarkText.length * 18;
const textHeight = 36;
// 生成SVG字符串,注意转义特殊字符
const svgText = `
<svg width="${textWidth}" height="${textHeight}">
<rect x="0" y="0" width="100%" height="100%" fill="rgba(0,0,0,0.35)" rx="6" ry="6"/>
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle"
font-family="sans-serif" font-size="18" fill="white">${watermarkText}</text>
</svg>`;
const watermarkBuffer = Buffer.from(svgText);
const left = metadata.width - textWidth - padding;
const top = metadata.height - textHeight - padding;
await image
.composite([{ input: watermarkBuffer, left, top }])
.toFile(outputPath);
console.log('文字水印添加完成');
}
addTextWatermark('./input.jpg', './output-watermark.jpg', 'ipipp.com').catch(console.error);
上面这段代码中,SVG字符串里的<和>都做了HTML转义,这样才能正确放入模板字符串并被Buffer识别。水印位置通过left和top控制,这取决于你想把水印放在哪个角落。如果想添加图片水印,只需要把input替换成水印图片的路径或Buffer,并设置gravity或精确坐标即可。
图片水印的典型做法是在右下角叠加公司Logo。sharp的composite()支持gravity枚举值,比如southeast表示东南角,也就是右下角。使用gravity可以省去手动计算坐标的麻烦:
async function addImageWatermark(inputPath, logoPath, outputPath) {
await sharp(inputPath)
.composite([
{
input: logoPath,
gravity: 'southeast',
blend: 'over',
// 可以给logo添加边距,需要包一层resize或者用sharp直接生成带边距的水印
}
])
.toFile(outputPath);
console.log('图片水印添加完成');
}
addImageWatermark('./input.jpg', './logo.png', './output-logo.jpg').catch(console.error);
这里有一个细节:如果Logo本身已经包含透明区域,blend: 'over'就是默认的叠加模式,不需要额外设置。如果想让水印半透明,可以先用sharp(logoPath).resize(...).composite()对Logo做透明度调整,或者在生成Logo图片时就带上Alpha通道。sharp没有直接设置整体不透明度的参数,但可以通过ensureAlpha()配合tint()等操作间接实现。
把缩放、裁剪和水印串联成完整处理管线
实际业务中,这三个操作往往不是孤立使用的。比如用户上传一张原始大图,你需要先把它等比缩放到最大宽度1200像素,再按4:3比例裁剪出封面区域,最后在右下角加上水印。sharp的链式调用让这个过程非常清晰:
async function processUploadedImage(inputPath, outputDir) {
const image = sharp(inputPath);
const metadata = await image.metadata();
// 第一遍:生成最大宽度1200的预览图
const preview = await image
.clone()
.resize({ width: 1200, height: 1200, fit: 'inside', withoutEnlargement: true })
.toBuffer();
// 第二遍:裁剪4:3封面并加水印
const cover = image
.clone()
.resize(1200, 900, { fit: 'cover', position: 'centre' })
.composite([
{
input: Buffer.from(
'<svg width="180" height="40"><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="sans-serif" font-size="18" fill="white">ipipp.com</text></svg>'
),
gravity: 'southeast'
}
])
.toFile(`${outputDir}/cover.jpg`);
return {
previewBuffer: preview,
coverPath: `${outputDir}/cover.jpg`,
originalWidth: metadata.width,
originalHeight: metadata.height
};
}
processUploadedImage('./upload.jpg', './processed').then(console.log).catch(console.error);
这个示例里,resize(1200, 900, { fit: 'cover' })已经同时完成了缩放和裁剪,因为cover模式会自动计算缩放比例并裁掉多出来的部分。然后再叠加SVG水印,gravity: 'southeast'把水印固定到右下角。如果需要精确控制水印与边缘的距离,可以不使用gravity,改为手动计算left和top。
还要注意错误处理。sharp在遇到损坏的图片时会抛出异常,建议在处理函数外层使用try/catch捕获,避免服务崩溃。结合流式上传场景,你可以用sharp(buffer)直接处理内存中的Buffer数据,然后返回处理后的Buffer给客户端,不需要把中间文件写入磁盘。这样能减少I/O开销,也避免临时文件清理的麻烦。
在性能层面,sharp的底层libvips会尽量利用多核CPU,并且处理过程中不会把整张图片的所有像素同时载入内存。对于批量任务,你可以通过sharp.concurrency()查看或设置并发数,不过默认值通常已经适合大多数服务器。如果同一张原图要生成多个尺寸的缩略图,建议先用toBuffer()拿到一个中间Buffer,再分别用这个Buffer创建sharp实例,避免重复解码原图。
Node.js图片处理sharp库水印添加修改时间:2026-09-18 15:37:57