Node.js 服務端圖片處理利器——sharp 進階操做指南

sharp 是 Node.js 平臺上至關熱門的一個圖像處理庫,其其實是基於 C 語言編寫 的 libvips 庫封裝而來,所以高性能也成了 sharp 的一大賣點。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 更多的用法之後若是還有機會折騰,會繼續跟你們分享~

本文首發於個人博客(點此查看),歡迎關注。

相關文章
相關標籤/搜索