背景&概覽
目前常見的圖床服務都會有圖片動態裁切的功能,主要的應用場景用以為各種終端和業務形態輸出合適尺寸的圖片。
一張動輒以 MB 為計量單位的原始大圖,通常不會只設置一下顯示尺寸就直接輸出到終端中,因為體積太大加載體驗會很差,除了影響加載速度還會增加終端設備的內存占用。所以要想在各種終端下都能保證圖片質量的同時又確保輸出合適的尺寸,那么此時就需要根據圖片 URL 來對原始圖片進行裁切,然后動態生成并輸出一張新的圖片。
URL 的設計
圖片 URL 需要包含圖片 id、尺寸、質量等信息。有兩種類型的圖片 URL,分別是原圖 URL 和帶動態裁切信息的 URL。
// 原圖 URLhttp://example.com/$imgId// 帶裁切信息的圖片 URLhttp://example.com/$cropType/$width_$height_$quality/$imgId
來分析一下上面 URL 中的變量:
那么一張圖片 id 為 4b2d4edcc1f82452 的原圖 URL 應該是:
http://example.com/4b2d4edcc1f82452.jpg
如果想要一張該圖 800×600 的版本,裁切的 URL 大致是下面這樣的:
http://example.com/es/800_600_/4b2d4edcc1f82452.jpg
裁切算法
該來說說以上 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 就是對圖片進行裁切。當然除了中心裁切,還能延伸出頂部裁切,底部裁切等,相對來說使用場景要少很多。
結語
在服務的實際應用中,還會做一些優化,比如對服務的接口做一些安全限制,確保該接口不會被刷,裁切本身是比較消耗資源的操作。由于裁切操作比較耗資源,那么相同的尺寸應該保證只有一次裁切操作,這樣只有第一次請求裁切圖片才會真正有裁切操作,后續的訪問就直接讀取原來就裁切好的實體文件即可。
以上所述是小編給大家介紹的使用 Node.js 實現圖片的動態裁切及算法實例代碼詳解,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對VeVb武林網網站的支持!
新聞熱點
疑難解答