從web圖片裁剪出發:了解H5中的Blob


  剛開始做前端的時候,有個功能卡住我了,就是裁剪並上傳頭像。當時兩個方案擺在我面前,一個是flash,我不會。另一個是通過iframe上傳圖片,然后再上傳坐標由后端裁剪,而我最終的選擇是后者。有人會疑惑,為什么不用H5的Canvas和FormData,第一要考慮ie8的兼容性,第二那時候眼界沒到,這種新東西光是聽聽都怕。

  后來隨着Mobile項目越做越多,類似的功能開發得也越來越多,Canvas+FormData成為了標配方案。但做的多了卻一直沒有靜下心來研究,瀏覽器怎么使用H5的方式裁剪並把文件發送出去,回過頭看都是知其然不知其所以然。這篇隨筆先做個初步的拆解,就是當通過input選擇一張圖片后,這張圖片在瀏覽器里是怎樣的一個存在。

  文件操作一直是早期瀏覽器的痛點,全封閉式,不給JS操作的空間,而隨着H5一系列新接口的推出,這個壁壘被打破。對,是一系列接口,以下會涉及到如下概念:Blob、File、FileReader、ArrayBuffer、ArrayBufferView、DataURL等,其他如FormData、XMLHttpRequest、Canvas等暫不深入。

  我們先創建一個簡單的頁面,只有一個input[type=file]。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <input type="file">
</body>
</html>

  然后我們在JS中獲取這個元素

var input = document.querySelector('input[type=file]');

  可以看到這個元素有個屬性files,它的類型是FileList。這個類不做過多介紹,就是一個類數組,由瀏覽器通過用戶行為往里面添加或刪除元素,JS只有訪問其元素的接口,無法對其進行操作。而files的元素就是File類型,File是blob的子類,比blob主要多出一個name的屬性。

  現在我們選取一個文件,這里問題來了,這個元素是文件在瀏覽器的完整備份,還是一個指向文件系統的引用?答案是后者,我們選定文件,然后修改文件名,再上傳文件,瀏覽器報錯了。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <form name='test'>
    <input type="file">
    <input type="submit" value="提交">
  </form>
  <script>
    var input = document.querySelector('input[type=file]'),
        form = document.test;
    form.addEventListener('submit', function(e) {
      e.preventDefault();
      var file = input.files[0],
            fd = new FormData(),
            xhr = new XMLHttpRequest();
      fd.append('file', file);
      xhr.open('post', '/upload');
      xhr.send(fd);
    });
  </script>
</body>
</html>

  使用chrome打開chrome://blob-internals/,可以看到一條這樣的記錄

  可見這僅僅是一條引用。第二個問題來了,如果我們要對圖片進行處理,那么只拿到引用是不行的,肯定要在瀏覽器有一份數據的備份,那么怎么獲取這個備份呢?答案就是FileReader,FileReader的對象主要有readAsArrayBuffer、readAsBinaryString、readAsDataURL、readAsText等方法,它們的入參都是Blob對象或是File對象,結果對應最終獲取的數據類型。這幾個方法是異步的,讀取過程中會拋出對應的事件,其中讀取完畢的事件為load,所以數據的處理要放在onload下。我先給一個簡單的example:

input.addEventListener('change', function() {
  var file = this.files[0],
      fr = new FileReader(),
      blob;
  fr.onload = function() {
    blob = new Blob([this.result]);
  };
  fr.readAsArrayBuffer(file)
});

  當用戶選取圖片時,調用FileReader的readAsArrayBuffer把圖片數據讀出來,然后生成新的blob對象保存在瀏覽器中。查看chrome://blob-internals/,可以注意到這一項:

  對應的就是剛才的blob,可以對比length和圖片本身的大小。上面那個demo很突兀,完全沒有解釋什么是ArrayBuffer,為什么創建blob要傳入一個ArrayBuffer。那么第三個問題來了,什么是ArrayBuffer、BinaryString、DataURL、Text,它們有什么聯系和不同,Blob類到底是個什么東西?首先,圖片是個二進制文件,它的內容也是由0和1組成的。用戶肯定是看不懂0和1的組合的,能看懂的只有最終展示的圖片,而程序員也看不懂0和1,但程序員能看懂另外幾種0和1變換后的組合。它們就是以上的4種:ArrayBuffer、BinaryString、DataURL和Text。

  其中ArrayBuffer是最接近二進制數據的表現的,可以理解為它就是二進制數據的存儲器,這也是為什么二進制文件的Blob需要傳入ArrayBuffer。正因為它的內部是二進制數據,所以我們是不可以直接操作的。這時候就需要一個代理者幫助我們讀或寫,這個代理者就是ArrayBufferView。

  ArrayBufferView不是一個類,而是一個類的集合,包括:Int8Array、Uint8Array、Uint8ClampedArray、Int16Array、Uint16Array、Int32Array、Uint32Array、Float32Array、Float64Array和DataView,分別表示以8位、16位、32位、64位數字為元素對ArrayBuffer內的二進制數據進行展現,它們都有統一的屬性buffer指向對應的ArrayBuffer。栗子暫時不舉,之后會用到。

  ArrayBuffer簡單介紹了,那什么是BinaryString呢?是二進制數據直接以byte的形式展現的字符串,比如1100001,用Uint8表示就是97,用BinaryString表示就是'a'。對,前者是charCode,后者是char,所以BinaryString和Uint8Array之間是可以自由轉換的。

  接下來是DataURL了,這是一個經過base64編碼的字符串,它的組成如下:

data:[mimeType];base64,[base64(binaryString)]

  除了固定的字符串部分,它主要包含兩個重要信息即中括號括起的部分,mimeType和base64編碼后的binaryString,從它里面我們可以這樣取到這兩個信息。

var binaryString = atob(dataUrl.split(',')[1]),
    mimeType = dataUrl.split(',')[0].match(/:(.*?);/)[1];

  最后,Text是什么呢?在ftp上,文本傳輸和二進制傳輸的區別是什么,那Text類型和BinaryString類型的區別就是什么了,也就是Text類型是經過一定轉換的BinaryString,對於圖片來說,這個類型是用不到的。

  好了,現在我們了解了一張圖片在瀏覽器里以數據的形式可以表現為ArrayBuffer、BinaryString、DataURL,那么第四個問題來了,它們各有實際用途呢?我們從應用場景出發,回到文章開頭的問題,圖片的裁剪和上傳。圖片的裁剪我們要倚仗牛逼的canvas,而canvas的context有這么一個方法toDataURL,就是把canvas的內容轉換為圖片數據,而數據的表現形式就是DataURL!圖片的上傳我們用的是FormData,它可以添加Blob類型的對象進去,那Blob類型除了從input[type=file]中直接獲取,還能靠什么生成呢?自然是ArrayBuffer!好了,裁剪圖片的功能要用到DataURL,上傳圖片的功能要用到ArrayBuffer,那怎么從DataURL轉換為ArrayBuffer呢?我們知道DataURL很重要的組成部分就是經過base64編碼的BinaryString,那么很顯然我們可以從DataURL中提取BinaryString,而BinaryString就是ArrayBuffer對應的Uint8Array的字符形式的表現,所以可以由BinaryString生成ArrayBuffer,那么DataURL到ArrayBuffer之間的橋就是BinaryString!

  到現在為止,我們說了很多概念,然而這並沒有什么卵用,驗證概念的方法不是提出新的概念,而是建立一個example。以下的example就是把圖片數據從input中取出,然后以DataURL的格式進行預覽,提交時把預覽生成圖片上傳的整個流程。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <form name='test'>
    <input type="file" name='file'>
    <input type="submit" value="提交">
  </form>
  <img src="" alt="">
  <script>
    var img = document.querySelector('img'),
        preview;
    document.test.file.addEventListener('change', function() {
      var fr = new FileReader();
      fr.onload = function() {
        preview = this.result;
        img.src = preview;
      };
      fr.readAsDataURL(this.files[0]);
    })
    document.test.addEventListener('submit', function(e) {
      e.preventDefault();
      var binaryString = atob(preview.split(',')[1]),
          mimeType = preview.split(',')[0].match(/:(.*?);/)[1],
          length = binaryString.length,
          u8arr = new Uint8Array(length),
          blob,
          fd = new FormData(),
          xhr = new XMLHttpRequest();
      while(length--) {
        u8arr[length] = binaryString.charCodeAt(length);
      }
      blob = new Blob([u8arr.buffer], {type: mimeType});
      fd.append('file', blob);
      xhr.open('post', '/upload');
      xhr.send(fd);
    })
  </script>
</body>
</html>

  現在圖片已經被我們發射出去了,那么圖片在協議包里是以怎樣的數據形式存在的呢?當然是以二進制的形式,我們抓一下包,發現在fiddler里面這個二進制串會轉換為字符串,即上面的binaryString。

  既然通過發送的blob到最后在數據包里都是以binaryString的形式展示,那么是否可以直接使用xhr.send(binaryString)發送圖片呢?貌似是可以的,但我們試一下就會發現問題,服務器獲取到的信息不能生成一張圖片,說明數據被破壞了。那么數據是誰破壞的呢?這個罪魁禍首就是send,當send的參數是字符串的時候,會對字符串進行utf8編碼。我們看下相同的圖片通過blob發送出去和通過binaryString直接發送出去的數據會有什么不同。這里我們用wireshark抓包,因為wireshark會自動對數據塊進行分割,可以比較直觀的看到圖片所對應的數據。PS: 這張圖片一張1px白色的png。

 

   前面是正常的圖片數據,后面是經過了utf8編碼的圖片數據。我們可以看到數據確實被破壞了,當然在知道元數據是binaryString的情況下,這種破壞是可以恢復的,不過不是這里討論的范疇了,感興趣的可以跳轉阮老師的博客《字符編碼筆記:ASCII,Unicode和UTF-8》

  好了,整個圖片在瀏覽器端的拆解到此結束。理解了這些,就走完了寫出牛逼的客戶端圖片裁剪工具的第一步。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM