sharp 是 Node.js 平臺上至關熱門的一個圖像處理庫,其其實是基於 C 語言編寫 的 libvips 庫封裝而來,所以高性能也成了 sharp 的一大賣點。sharp 能夠方便地實現常見的圖片編輯操做,如裁剪、格式轉換、旋轉變換、濾鏡添加等。固然,網絡上相關的文章比較多,sharp 的官方文檔也比較詳細,因此這不是本文的重點。這裏主要是想記錄一下我在使用 sharp 過程當中遇到的一些稍複雜的圖片處理需求的解決方案,但願分享出來可以對你們有所幫助。前端
sharp 總體採用流式處理模式,其在讀入圖像數據後通過一系列的處理加工而後輸出結果。咱們看一個簡單的示例就能理解:git
const sharp = require('sharp'); sharp('input.jpg') .rotate() .resize(200) .toBuffer() .then( data => ... ) .catch( err => ... ); 複製代碼
sharp 幾乎全部的函數接口都掛載在 Sharp
實例上,所以圖像處理的第一步操做必定是讀入圖片數據(sharp
函數接受圖片本地路徑或者圖片 Buffer 數據做爲參數)並將其轉換爲 Sharp
實例,而後纔是如流水線通常的加工。所以,這裏應該提供一個預處理函數,將服務端接收到的圖片轉換爲 Sharp
實例:github
/** * * @param { String | Buffer } inputImg 圖片本地路徑或圖片 Buffer 數據 * @return { Sharp } */ async convert2Sharp(inputImg) { return sharp(inputImg) } 複製代碼
而後就能夠進行具體的圖像處理。canvas
添加水印功能應該算是比較常見的圖片處理需求了。sharp 在圖像合成方面只提供了一個函數:overlayWith
,其接受一個圖片參數(一樣是圖片本地路徑字符串或者圖片 Buffer 數據)以及一個可選的 options
配置對象(可配置水印圖片的位置等信息)而後將該圖片覆蓋到原圖上。邏輯上也比較簡單,咱們的代碼以下所示:後端
/** * 添加水印 * @param { Sharp } img 原圖 * @param { String } watermarkRaw 水印圖片 * @param { top } 水印距圖片上邊緣距離 * @param { left } 水印距圖片左邊緣距離 */ async watermark(img, { watermarkRaw, top, left }) { const watermarkImg = await watermarkRaw.toBuffer() return img .overlayWith(watermarkImg, { top, left }) } 複製代碼
這裏簡單起見只支持配置水印圖片的位置,sharp 還支持更復雜的配置參數好比是否重複粘貼多個水印圖片、是否只在 α 信道粘貼水印圖片等,具體可參見 overlayWith
的文檔。markdown
這裏還須要順帶提一下前端的實現。固然,若是服務端是按照固定規則給圖片添加水印(好比新浪微博裏圖片水印放置在固定的位置),前端就沒必要作什麼了。可是某些場景下(好比在線圖片編輯類工具中)用戶添加水印的時候會指望可以在前端得到所見即所得的體驗。這個時候若是用戶添加完水印而且選好位置後,必須將數據發送至服務端處理再獲得處理結果,勢必會影響整個服務的流暢性。幸運的是強大的 HTML5 讓前端的功能愈來愈豐富,藉助 canvas
咱們就能在前端實現添加水印的功能。具體的實現細節並不難,主要就是藉助了 canvas
提供的 drawImage
方法,看一下示例:網絡
var canvas = document.getElementById("canvas"); var ctx = canvas.getContext('2d'); // img: 底圖 // watermarkImg: 水印圖片 // x, y 是畫布上放置 img 的座標 ctx.drawImage(img, x, y); ctx.drawImage(watermarkImg, x, y); 複製代碼
實際上,整個添加水印的功能(選擇原圖、選擇水印圖片、設置水印圖片位置、得到添加水印後的圖片)是能夠徹底由前端完成的。固然,爲了追求服務端功能的完整性,仍是建議使用前端展現+後端處理的模式。async
粘貼文字的需求實際上與添加水印比較相似。惟一不一樣的是添加的水印圖片換成了文字,以及咱們可能須要對文字的大小、字體等作一些調整。思路也比較容易想到,把文字轉換成圖片形式便可。這裏咱們用到了 text-to-svg
庫,做用是將文字轉換成 svg。利用 svg 的特色咱們能夠很方便地設置文字的字體大小、顏色等。而後調用 Buffer.from
將 svg 轉換爲 sharp 可使用的 buffer 數據。最後就是和上面的水印添加同樣的步驟了。ide
const Text2SVG = require('text-to-svg') /** * 粘貼文字 * @param { Sharp } img * @param { String } text 待粘貼文字 * @param { Number } fontSize 文字大小 * @param { String } color 文字顏色 * @param { Number } left 文字距圖片左邊緣距離 * @param { Number } top 文字距圖片上邊緣距離 */ async pasteText(img, { text, fontSize, color, left, top, }) { const text2SVG = Text2SVG.loadSync() const attributes = { fill: color } const options = { fontSize, anchor: 'top', attributes, } const svg = Buffer.from(text2SVG.getSVG(text, options)) return img .overlayWith(svg, { left, top }) } 複製代碼
拼接圖片的操做相對來講最爲複雜。這裏咱們提供了兩個配置項:拼接模式(水平/垂直)以及背景顏色。拼接模式比較好理解,無非是水平或是垂直排列圖片。背景顏色則用於填充留白處。拼接圖片時,圖片以根據軸線居中排列。以水平排列圖片爲例,示意圖以下:svg
這裏也沒有 sharp 提供的現成函數,一切仍是用惟一的 overlayWith
解決。overlayWith
的用法是將一張圖粘貼至另外一張圖上,這與咱們拼接圖片的需求略有差別。咱們須要轉換一下思惟:能夠預先建立一張底圖,背景顏色能夠根據配置值肯定,而後將全部待拼接圖片粘貼至其上,便可知足要求。
首先咱們須要讀取全部待拼接圖片的長與寬。假設拼接模式爲水平拼接,那麼最終生成的圖片的寬度爲全部圖片寬度之和,高度則取全部圖片中的最大高度(垂直拼接的話則反過來):
let totalWidth = 0 let totalHeight = 0 let maxWidth = 0 let maxHeight = 0 const imgMetadataList = [] // 獲取全部圖片的寬和高,計算和及最大值 for (let i = 0, j = imgList.length; i < j; i += i) { const { width, height } = await imgList[i].metadata() imgMetadataList.push({ width, height }) totalHeight += height totalWidth += width maxHeight = Math.max(maxHeight, height) maxWidth = Math.max(maxWidth, width) } 複製代碼
而後咱們用獲得的寬度和高度數據新建一個背景顏色爲傳入配置(或默認白色)的 base 圖片:
const baseOpt = { width: mode === 'horizontal' ? totalWidth : maxWidth, height: mode === 'vertical' ? totalHeight : maxHeight, channels: 4, background: background || { r: 255, g: 255, b: 255, alpha: 1, }, } const base = sharp({ create: baseOpt, }).jpeg().toBuffer() 複製代碼
而後在 base 圖片的基礎上重複調用 overlayWith
函數,將待拼接圖片逐個粘貼至 base 圖片上。這裏須要注意的是圖片的擺放位置,前面也提到過,咱們會將圖片根據主軸線進行居中對齊,因此每次擺放圖片時都須要進行 top 和 left 的計算(一個是居中的計算,一個是隨着圖片擺放順序進行偏移的計算),固然,弄明白了原理以後就是小學數學題,沒有太多可講的。另外一個須要注意的則是 overlayWith
每次只能完成兩張圖片之間的合成,所以咱們用到了 reduce
方法,持續地將圖片粘貼至底圖上,並將結果做爲下一次的輸入。
imgMetadataList.unshift({ width: 0, height: 0 }) let imgIndex = 0 const result = await imgList.reduce(async (input, overlay) => { const offsetOpt = {} if (mode === 'horizontal') { offsetOpt.left = imgMetadataList[imgIndex++].width offsetOpt.top = (maxHeight - imgMetadataList[imgIndex].height) / 2 } else { offsetOpt.top = imgMetadataList[imgIndex++].height offsetOpt.left = (maxWidth - imgMetadataList[imgIndex].width) / 2 } overlay = await overlay.toBuffer() return input.then(data => sharp(data).overlayWith(overlay, offsetOpt).jpeg().toBuffer()) }, base) return result 複製代碼
如下是拼接圖片函數的完整實現:
/** * 拼接圖片 * @param { Array<Sharp> } imgList * @param { String } mode 拼接模式:horizontal(水平)/vertical(垂直) * @param { Object } background 背景顏色 格式爲 {r: 0-255, g: 0-255, b: 0-255, alpha: 0-1} 默認 {r: 255, g: 255, b: 255, alpha: 1} */ async joinImage(imgList, { mode, background }) { let totalWidth = 0 let totalHeight = 0 let maxWidth = 0 let maxHeight = 0 const imgMetadataList = [] // 獲取全部圖片的寬和高,計算和及最大值 for (let i = 0, j = imgList.length; i < j; i += i) { const { width, height } = await imgList[i].metadata() imgMetadataList.push({ width, height }) totalHeight += height totalWidth += width maxHeight = Math.max(maxHeight, height) maxWidth = Math.max(maxWidth, width) } const baseOpt = { width: mode === 'horizontal' ? totalWidth : maxWidth, height: mode === 'vertical' ? totalHeight : maxHeight, channels: 4, background: background || { r: 255, g: 255, b: 255, alpha: 1, }, } const base = sharp({ create: baseOpt, }).jpeg().toBuffer() // 獲取圖片的原始尺寸用於偏移 imgMetadataList.unshift({ width: 0, height: 0 }) let imgIndex = 0 const result = await imgList.reduce(async (input, overlay) => { const offsetOpt = {} if (mode === 'horizontal') { offsetOpt.left = imgMetadataList[imgIndex++].width offsetOpt.top = (maxHeight - imgMetadataList[imgIndex].height) / 2 } else { offsetOpt.top = imgMetadataList[imgIndex++].height offsetOpt.left = (maxWidth - imgMetadataList[imgIndex].width) / 2 } overlay = await overlay.toBuffer() return input.then(data => sharp(data).overlayWith(overlay, offsetOpt).jpeg().toBuffer()) }, base) return result }, 複製代碼
以上就是我的在使用 sharp 過程當中總結的一些實用操做。實際上 sharp 還有不少高級的功能我並無用到,正應了「二八定律」:80% 的需求經常是經過 20% 的功能完成的。sharp 更多的用法之後若是還有機會折騰,會繼續跟你們分享~
本文首發於個人博客(點此查看),歡迎關注。