Node圖像處理——Jimp配合node-qrcode生成圖片上傳總結

Node圖像處理——Jimp配合node-qrcode生成圖片上傳總結

上週產品那邊來了一個需求,須要基於原圖針對不一樣用戶生成不一樣二維碼以及文案,並生成新圖片,讓用戶可以保存。接到這個需求時,內心不只沒有拒絕的意思,反而有點小興奮 ~ 由於又能探索一下新東西。html

大體效果以下,原圖:前端

效果圖:node

試水canvas

剛開始打算在前端用canvas生成圖片。咱們都知道canvas有合成圖片的功能,核心是drawImagetoDataURL這兩個方法。git

大體思路是:github

  1. 使用drawImage將生成的二維碼合併到原圖的指定位置
  2. 使用fillText方法生成文案
  3. toDataURL將圖片轉成base64
  4. 使用 atob 以及 Uint8Array 將其轉爲Buffer進行上傳。

不過最終該方案沒有走通,由於不一樣手機尺寸比例不統一,生成的二維碼的位置沒法準確地定位到指定位置,所以採用了另外一種方案:node層生成圖片。canvas

node搞起

在node層就無需考慮適配的問題了,由於只有一個基準,也就是原圖。生成二維碼及文案的尺寸、位置均可以直接寫死。通過調研,node圖像處理庫最出名的有兩個,分別是:JimpSharp,最終選用Jimp,由於Sharp沒安裝上?。二維碼庫卻是不少,最終決定選用 node-qrcode後端

開搞!api

主要步驟就兩步,以下:promise

  1. 生成圖片
  2. 讀取圖片並上傳

下面分解這兩步講解瀏覽器

生成圖片

生成圖片是最麻煩的。步驟比較多:

  1. 使用qrcode生成二維碼 Buffer
  2. 包裝二維碼Buffer爲Jimp對象
  3. 生成文案
  4. 合成圖片並保存

大部分都是調用Jimp及qrcode的api,還有一些node的原生api,如使用Buffer.from將base64轉爲Buffer。感興趣的能夠去參閱它們的文檔:

因爲生成圖片步驟較多,每一步都依賴上一步的結果,而且都是異步的,若是使用回調的話就完全陷入回調地獄了?,所以主要想說的是代碼組織方式。不怕你們笑話,個人初版代碼是這樣的?:

// 生成二維碼Buffer
  const codeBuffer = yield new Promise((resolve, reject) => {
    Qrcode.toDataURL(url, {}, (err, url) => {
      // 注意:這裏必須把「data:image/png;base64,」這一段去掉才能轉成正確的buffer
      const res = Buffer.from(url.replace(/.+,/, ''), 'base64')
      err ? reject(err) : resolve(res)
    })
  }).catch(() => {})
  // 生成文字
  const textJimp = yield new Promise((resolve, reject) => {
    new Jimp(textBgWidth, config.textBgHeight, +`0xFF${config.textBgColor}`, (err, image) => {
      Jimp.loadFont(config.fontPath).then((font) => {
        resolve(image.print(font, config.textPadding, 10, textContent, 10))
      })
    })
  })
  // 將二維碼Buffer包裝成Jimp對象
  const codeJimp = yield new Promise((resolve, reject) => {
    Jimp.read(codeBuffer).then((res) => {
      if (res) {
        resolve(res.resize(config.codeWidth, config.codeWidth))
      } else {
        reject('包裝buffer失敗')
      }
    })
  }).catch(() => {})

  yield new Promise((resolve, reject) => {
    Jimp.read(config.originImgPath).then(img => {
      img.composite(codeJimp, config.codeLeft, config.codeTop)
         .composite(textJimp, config.textLeft, config.textTop)
         // 因爲fs.createReadStream不能接受Buffer做爲參數,只能將生成的圖片臨時保存到本地
         .write(config.tempFilePath, () => {
           // resolve()
           reject('保存圖片失敗!')
         })
    })
  }).catch((err) => {
    console.log('保存圖片出錯:', err)
  })

由於咱們使用的node先後端分離框架 grace 的版本是支持 generator 語法的,因此想到了使用 yield 來將異步操做同步展現,但仍是看起來太繁瑣了?,必須重構!

promise 登場!

使用 promise 的鏈式調用語法,結構就會清晰不少,改寫後代碼是這樣的:

// 組合多個異步I/O
  const imgResult = yield generateCode(href)
    // 生成二維碼Buffer
    .then((res) => {
      codeBuffer = res;
      // 包裝二維碼Buffer爲Jimp對象
      return wrapCodeBuffer(codeBuffer, imgConfig);
    })
    .then((res) => {
      codeJimp = res;
      // 生成文字
      return generateText(textBgWidth, textContent, imgConfig);
    })
    .then((res) => {
      textJimp = res;
      // 組合並生成圖片
      return compositeImg(imgConfig, textJimp, codeJimp);
    })
    // 成功
    .then(() => true)
    // 中途出錯
    .catch((err) => {
      return false;
    });

瞬間優雅的許多 ~
實現方法也很簡單,就是讓每一個步驟的方法都返回一個 promise 便可,拿該方法爲例:

/**
 * 包裝二維碼Buffer爲Jimp對象
 * @param  {Buffer} codeBuffer  [二維碼Buffer對象]
 * @param  {Object} config      [配置對象]
 * @return {Promise}
 */
function wrapCodeBuffer(codeBuffer, config) {
  return new Promise((resolve, reject) => {
    Jimp.read(codeBuffer).then((res) => {
      if (res) {
        resolve(res.resize(config.codeWidth, config.codeWidth));
      } else {
        reject('包裝二維碼Buffer失敗');
      }
    });
  });
}

上傳圖片

接下來是使用node上傳圖片。因爲使用的後端接口是基於FormData方式的,因此要在node層模擬一個FormData上傳請求。
起初是徹底懵逼的,由於對http協議的這塊標準一直是隻知其一;不知其二。在前端使用FormData上傳圖片時咱們常常能看到請求體是這樣的:

------WebKitFormBoundarywQMoN5B2ZNAD6uqN
Content-Disposition: form-data; name="file"; filename="avatar.jpeg"
Content-Type: image/jpeg

------WebKitFormBoundarywQMoN5B2ZNAD6uqN--

請求頭的Content-Type是這樣的:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundarywQMoN5B2ZNAD6uqN

看起來挺複雜的,尤爲是這個------WebKitFormBoundarywQMoN5B2ZNAD6uqN--究竟是個什麼鬼?。

別急,先從個人這個上傳方法講起:

/**
 * 上傳圖片方法
 * @param  {ClientRequest} request  [由http.request方法返回的對象]
 * @param  {Object}        config   [配置對象]
 * @param  {String}        cookies  [用戶請求時所帶的全部cookie]
 * @return
 */
function uploadImg(request, config, cookies = '') {
  // 模擬form-data請求後端接口上傳圖片
  const boundaryKey = Math.random().toString(16);
  const endData = '\r\n----' + boundaryKey + '--';
  let contentLength = 0,
      content = '';
  content += '\r\n----' + boundaryKey + '\r\n' +
            'Content-Type: application/octet-stream\r\n' +
            'Content-Disposition: form-data; name="file"; ' +
            'filename="bg_invite.png"; \r\n' +
            'Content-Transfer-Encoding: binary\r\n\r\n';
  let contentBinary = Buffer.from(content, 'utf-8');
  // 獲取上傳內容總大小
  contentLength = fs.statSync(config.tempFilePath).size + Buffer.byteLength(contentBinary) + Buffer.byteLength(endData);
  // 設置請求頭
  request.setHeader('Content-Type', 'multipart/form-data; boundary=--' + boundaryKey);
  request.setHeader('Content-Length', contentLength);
  request.setHeader('Cookie', cookies);
  request.write(contentBinary);
  const fileStream = fs.createReadStream(config.tempFilePath, { bufferSize: 4 * 1024 });
  fileStream.on('end', () => {
    // 發送請求
    request.end(endData);
  });
  fileStream.pipe(request);
}

能夠看到,這個方法其實就是構造了請求,拆分下來就以下幾件事:

  • 構造請求頭
  • 計算上傳內容總大小
  • 將文件以流的形式寫入http.ClientRequest對象

先說請求頭,FormData形式的請求Content-Type爲multipart/form-data,而且必定要提供boundary字段。但是爲何呢?

咱們都知道默認提交表單時,Content-Type是application/x-www-form-urlencoded,而且參數都是已相似name=John&age=12這種形式在請求體中傳遞的,參數是以&分割的。這裏的boundary的做用就跟&同樣,是用來分割多個參數的,而且是能夠自定義的,而在瀏覽器中,是瀏覽器爲咱們自動生成的,這就知道了上文中那個boundary是怎麼回事了 ~

再看每一個boundary之間的內容,也就是每一個字段,其中還有Content-type及Content-Disposition字段咱們很陌生。

Content-Type跟http協議的Content-Type是同樣的,只不過在multipart/form-data類型中,咱們能夠手動指定每一個參數的Content-Type。方法中的字段值爲application/octet-stream,就是告訴Server這部份內容是字節流,由於咱們須要以字節流的形式上傳圖片。

而Content-Disposition是每一個參數必須的選項,而且值必須爲form-data。該頭其實還有其餘用途,能夠參閱MDN的官方文檔

接下來是計算Content-Length。這裏主要使用了node的fs模塊,以及Buffer模塊的api,都很好理解,查看文檔便可。

最後是將圖片寫入http.ClientRequest對象中。該對象是由node的http.request方法返回,而且是一個可寫流。引用node官方文檔的話:

ClientRequest 實例是一個可寫流。 若是須要經過 POST 請求上傳一個文件,則寫入到 ClientRequest 對象。

最後再調用http.ClientRequest對象的end方法,便可完成請求對象的寫入,就發出請求啦 ~

至此,一個Node合成圖片並上傳的需求完成!過程當中收穫很是多!

生命不息折騰不止!

相關文章
相關標籤/搜索