使用 Node.js 实现图片的动态裁切及算法实例代码

目前常见的图床服务都会有图片动态裁切的功能,主要的应用场景用以为各种终端和业务形态输出合适尺寸的图片。

一张动辄以 MB 为计量单位的原始大图,通常不会只设置一下显示尺寸就直接输出到终端中,因为体积太大加载体验会很差,除了影响加载速度还会增加终端设备的内存占用。所以要想在各种终端下都能保证图片质量的同时又确保输出合适的尺寸,那么此时就需要根据图片 URL 来对原始图片进行裁切,然后动态生成并输出一张新的图片。

URL 的设计

图片 URL 需要包含图片 id、尺寸、质量等信息。有两种类型的图片 URL,分别是原图 URL 和带动态裁切信息的 URL。

// 原图 URL $imgId // 带裁切信息的图片 URL $cropType/$width_$height_$quality/$imgId

来分析一下上面 URL 中的变量:

$imgId

$cropType

$width

$height

$quality

那么一张图片 id 为 4b2d4edcc1f82452 的原图 URL 应该是:

如果想要一张该图 800×600 的版本,裁切的 URL 大致是下面这样的:

裁切算法

该来说说以上 URL 背后的算法了。在 Node.js 中可以使用著名的图片裁切库 GM ,该库是基于 imagemagick 和 graphicsmagick 底层库的封装。

最常见的裁切算法是等比例裁切,等比裁切的算法需要至少给出裁切目标图片的宽度和高度的其中一个,如果图片限宽就给出宽度,限高就给出高度,如果两个参数都有,就需要确保裁切的目标宽高相对于原始的宽高是按比例计算的,否则裁切的结果就会出现拉伸。

var gm = require('gm'); // 裁切的最小尺寸 var minSize = 48; var defaultQuality = 90; /** * 等比例缩放 equal scaling * @param { String } 原文件路径 * @param { String } 新文件路径 * @param { String } 缩放规则 * @return { promise } */ var es = function(src, dest, rules) { return new Promise(function(resolve, reject) { // 900_600_90 => 宽度900/高度600/品质90 rules = rules.split('_'); if (rules.length !== 3) { return reject(new Error('Resize rules invalid')); } // 解析裁切的目标宽高 let resizeWidth = parseInt(rules[0]); let resizeHeight = parseInt(rules[1]); let quality = parseInt(rules[2]) || defaultQuality; const readStream = fs.createReadStream(src); const writeStream = fs.createWriteStream(dest); gm(readStream) .size({ bufferStream: true }, function(err, size) { if (err) { return reject(err); } const origWidth = size.width; const origHeight = size.height; let resizeResult; // 缩放的宽度和高度做最大最小值限制 if (resizeWidth) { if (resizeWidth > origWidth * 1.5) { resizeWidth = Math.floor(origWidth * 1.5); } else if (resizeWidth < minSize) { resizeWidth = minSize; } } if (resizeHeight) { if (resizeHeight > origHeight * 1.5) { resizeHeight = Math.floor(origHeight * 1.5); } else if (resizeHeight < minSize) { resizeHeight = minSize; } } resizeResult = this.resize(resizeWidth, resizeHeight); resizeResult .quality(quality) .interlace('line') // 使用逐行扫描方式 .unsharp(2, 0.5, 0.5, 0) .stream() .on('end', resolve) .pipe(writeStream); }); }); };

说说几个重要的 API:

quality 设置图片的质量,GM 图片质量范围是 0-100,默认的质量是 75。
interlace 用于设置图片在显示器上加载时的显示方式,当然显示方式本身还要受图片本身的影响。
unsharp 用来设置图片的锐度,将一张大图缩放成一张小图时,会损失很多像素,需要适当的增加图片锐度来保证图片的质量。关于 unsharp 的使用,详见 Using ImageMagick to make sharp web-sized photographs 。
等比例裁切严格来说实际上还只是对图片进行缩放,并未动用图片裁切的 API。

还有一种比较常见的裁切方式,会先将图片等比例缩放后再从中心裁切,裁切出来的图片是一个正方形,这样能尽可能保证图片的内容。

/* * 等比例缩放后从中心裁切 equal scaling crop center(正方形裁切) * @param { String } 原文件路径 * @param { String } 新文件路径 * @param { String } 缩放规则 * @return { promise } */ var escc = function(src, dest, rules) { return new Promise(function(resolve, reject) { // 600_90 => 宽度600/高度600/品质90 rules = rules.split('_'); if (rules.length !== 2) { return reject(new Error('Resize rules invalid')); } let cropSize = parseInt(rules[0]); let quality = parseInt(rules[1]) || defaultQuality; const readStream = fs.createReadStream(src); const writeStream = fs.createWriteStream(dest); if (!cropSize) { reject(new Error('Crop params invalid')); return; } gm(readStream) .size({ bufferStream: true }, function(err, size) { if (err) { reject(err); return; } const origWidth = size.width; const origHeight = size.height; let cropX = 0; let cropY = 0; let resizeWidth; let resizeHeight; let resizeResult; // 裁切的宽度和高度做最大最小值限制 if (cropSize > origWidth) { cropSize = origWidth; } else if (cropSize > origHeight) { cropSize = origHeight; } else if (cropSize < minSize) { cropSize = minSize; } // 先计算出等比缩放的尺寸,然后再根据此尺寸计算出裁切位置 if (origWidth > origHeight) { resizeWidth = cropSize / origHeight * origWidth; resizeHeight = cropSize; cropX = Math.floor((resizeWidth - cropSize) / 2); cropY = 0; } else { resizeHeight = cropSize / origWidth * origHeight; resizeWidth = cropSize; cropX = 0; cropY = Math.floor((resizeHeight - cropSize) / 2); } resizeResult = this.resize(resizeWidth, resizeHeight); resizeResult .quality(quality) .interlace('line') // 使用逐行扫描方式 .crop(cropSize, cropSize, cropX, cropY) .unsharp(2, 0.5, 0.5, 0) .stream() .on('end', resolve) .pipe(writeStream); }); }); };

上面的 crop 就是对图片进行裁切。当然除了中心裁切,还能延伸出顶部裁切,底部裁切等,相对来说使用场景要少很多。

结语

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/61d37cb37543527dfaf3cdc7521788b6.html