微信小程式對圖片進行canvas壓縮的方法(詳解)

2020-11-13 06:00:13

微信小程式其實自帶一個圖片壓縮的API wx.compressImage,但是這玩意目前感受就是個垃圾。IOS大多數情況下據說還可以,安卓有的時候降低品質壓縮後體積反而變大,而且沒辦法控制其壓縮至具體指定的大小,壓縮後多大看天意。所以需要使用畫布去自己實現一個圖片壓縮方法。

簡單來講原理就是:找個不顯示在頁面上的畫布畫上去,再取出,如果體積還是太大,縮小尺寸後再畫,再取,遞迴下去,直到體積滿足要求。(所以限制的越小,圖片越大,壓縮越久,遞迴次數越多)

第一步:新建一個zipPic.js檔案(名字你開心就好),裡面的程式碼如下

//通過canvas將圖片壓縮至指定大小

//判斷圖片大小是否滿足需求,limitSize的單位是kb
function imageSizeIsLessLimitSize(imagePath,limitSize,lessCallback,moreCallback){
  //獲取檔案資訊
  wx.getFileInfo({
    filePath:imagePath,
    success:(res)=>{
      console.log("壓縮前圖片大小",res.size/1024,'kb');
      //如果圖片太大了走moreCallback
      if(res.size>1024*limitSize){
        moreCallback()
      }
      //圖片滿足要求了走lessCallback
      else{
        lessCallback()
      }
    }
  })
}

//將圖片畫在畫布上並獲取畫好之後的圖片的路徑
function getCanvasImage(canvasId,imagePath,imageW,imageH,getImgSuccess){
  //建立畫布內容
  const ctx=wx.createCanvasContext(canvasId);
  //圖片畫上去,imageW和imageH是畫上去的尺寸,影象和畫布間隔都是0
  ctx.drawImage(imagePath,0,0,imageW,imageH);
  //這裡一定要加定時器,給足夠的時間去畫(所以每次遞迴最少要耗時200ms,多次遞迴很耗時!)
  ctx.draw(false,setTimeout(function(){
    wx.canvasToTempFilePath({
      canvasId:canvasId,
      x:0,
      y:0,
      width:imageW,
      height:imageH,
      quality:1, //最高品質,只通過尺寸放縮去壓縮,畫的時候都按最高品質來畫
      success:(res)=>{
        getImgSuccess(res.tempFilePath);
      }
    })
  },200));
}

//主函數,預設限制大小1024kb即1mb,drawWidth是繪畫區域的大小
//初始值傳入為畫布自身的邊長(我們這是一個正方形的畫布)
function getLessLimitSizeImage(canvasId,imagePath,limitSize=1024,drawWidth,callback){
  //判斷圖片尺寸是否滿足要求
  imageSizeIsLessLimitSize(imagePath,limitSize,
    (lessRes)=>{
      //滿足要求走callback,將壓縮後的檔案路徑返回
      callback(imagePath);
    },
    (moreRes)=>{
      //不滿足要求需要壓縮的時候
      wx.getImageInfo({
        src:imagePath,
        success:(imageInfo)=>{
          let maxSide=Math.max(imageInfo.width,imageInfo.height);
          let windowW=drawWidth;
          let scale=1;
          /*
          這裡的目的是當繪畫區域縮小的比圖片自身尺寸還要小的時候
          取圖片長寬的最大值,然後和當前繪畫區域計算出需要放縮的比例
          然後再畫經過放縮後的尺寸,保證畫出的一定是一個完整的圖片。由於每次遞迴繪畫區域都會縮小,
          所以不用擔心scale永遠都是1繪畫尺寸永遠不變的情況,只要不滿足壓縮後體積的要求
          就會縮小繪畫區域,早晚會有繪畫區域小於圖片尺寸的情況發生
          */
          if(maxSide>windowW){
            scale=windowW/maxSide;
          }
          //trunc是去掉小數
          let imageW=Math.trunc(imageInfo.width*scale);
          let imageH=Math.trunc(imageInfo.height*scale);
          console.log('呼叫壓縮',imageW,imageH);
          //圖片在規定繪畫區域上畫並獲取新的圖片的path
          getCanvasImage(canvasId,imagePath,imageW,imageH,
            (pressImgPath)=>{
              /*
              再去檢查是否滿足要求,始終縮小繪畫區域,讓圖片適配繪畫區域
              這裡乘以0.95是必須的,如果不縮小繪畫區域,會出現尺寸比繪畫區域小,
              而體積比要求壓縮體積大的情況出現,就會無窮遞迴下去,因為scale的值永遠是1
              但0.95不是固定的,你可以根據需要自己改,0到1之間,越小則繪畫區域縮小的越快
              但不建議取得太小,繪畫區域縮小的太快,壓出來的將總是很糊的
              */
              getLessLimitSizeImage(canvasId,pressImgPath,limitSize,drawWidth*0.95,callback);
            }
          )
        }
      })
    }
  )
}

export default getLessLimitSizeImage

好的接下來是使用的方法:

在你想壓縮圖片的js程式碼所對應的頁面中。先放置一個使用者看不見的畫布。
(就是如果我想在index.js中chooseImage再壓縮,就需要你在index.html中加上下面的html程式碼)

<!--用於圖片壓縮的canvas畫布,不在頁面中展示,且id固定不可變-->
  <canvas
    style="width: {{cw}}px; height: {{cw}}px;position: absolute; z-index: -1; left: -10000rpx;; top: -10000rpx;"
    canvas-id="zipCanvas"
  ></canvas>
  <!--畫布結束-->

其中cw的值我個人建議選擇使用者螢幕的寬度,如下,在page({…})的data中新增

	//畫板邊長預設是螢幕寬度,正方形畫布
    cw:wx.getSystemInfoSync().windowWidth,

個人建議畫布和繪畫區域都是正方形的,畢竟你也不知道要壓縮的圖片是橫向的還是縱向的。

然後,引入,不解釋

import getLessLimitSizeImage from '../../utils/zipPic'

在js程式碼中:

    wx.chooseImage({
      count:1, //只傳一張
      sizeType:'original', //原圖品質好,然後通過canvas壓縮,縮圖壓縮就太糊了
      sourceType: ['album', 'camera'], // 來源是相簿和相機
      success:(res)=>{
        let canvasId='zipCanvas' //注意這裡的id和你在頁面中寫的html程式碼的canvas的id要一致
        let imagePath=res.tempFilePaths[0];//原圖的路徑
        let limitSize=2048;//大小限制2048kb
        let drawWidth=wx.getSystemInfoSync().windowWidth;//初始繪畫區域是畫布自身的寬度也就是螢幕寬度
        wx.showLoading({title:'圖片壓縮中...',mask:true}) //不需要你可以刪掉
        getLessLimitSizeImage(canvasId,imagePath,limitSize,drawWidth,(resPath)=>{
          wx.hideLoading(); //不需要你可以刪掉
          
          //resPath就是壓縮後圖片的路徑,然後想做什麼都隨你
          
        })
      }
    })

補充:

  1. 這裡程式碼的主體不是我做的,網上一搜基本都是這個寫法,這裡是經過專案實踐測試後沒問題了做的講解。
  2. 這裡圖片是隻選了一張去壓縮,如果你需要選多張再挨個壓縮那就去寫個迴圈,找個陣列存壓縮後的結果,網上也有很多內容。
  3. 回撥函數中有lessRes和moreRes,細心的會發現這兩個引數並沒有被用到,他們只是個提醒作用,表明當前是less回撥還是more回撥,如果你不怕弄混刪掉了或者自己另外寫了兩個新方法那都隨你。
  4. 極限情況下比如說將圖片強制壓縮至10kb,這個東西我沒測試過,不知道會不會有問題。
  5. 圖片壓縮體積的減小不是線性的,給人的感覺有點像二次函數(y=x^2左面那一半),越往後壓縮的尺寸變化會越小。當然,這和使用者的解析度,螢幕本身的大小都有關係。
  6. 還是那句話,由於每次遞迴都要給至少200ms的時間去畫,所以遞迴很耗時!!!而不遞迴進行壓縮的話,網路傳輸又會很耗時!!!所以這個地方怎麼取捨,壓縮至多大,繪畫區域縮小的多快,都要靠你自己的經驗去偵錯。
  7. 圖片的壓縮,長寬比理論上來講是不變的,但是因為捨棄了小數,可能會有肉眼難以察覺的誤差,但是問題不大。如果前端想展示一下壓縮後的圖片的話,不要忘記在image中加入mode=「aspectFit」 。