专栏首页Golang语言社区从web图片裁剪出发:了解H5中的Blob

从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中获取这个元素

  1. 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》。

  好了,整个图片在浏览器端的拆解到此结束。理解了这些,就走完了写出牛逼的客户端图片裁剪工具的第一步。

本文分享自微信公众号 - Golang语言社区(Golangweb)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-09-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Golang语言 syscall 例子

    获取Windows的系统默认目录 Windows系统目录函数请参考MSDN:https://msdn.microsoft.com/en-us/library/w...

    李海彬
  • 厚土Go学习笔记 | 14. switch 的条件写的有点灵活,不过风格还是go的一贯风格

    switch是很容易理解的,先来个代码,运行起来,看看你的操作系统是什么吧。 package main import ( "fmt" "run...

    李海彬
  • 厚土Go学习笔记 | 14. switch 的条件写的有点灵活,不过风格还是go的一贯风格

    switch是很容易理解的,先来个代码,运行起来,看看你的操作系统是什么吧。 package main import ( "fmt" "run...

    李海彬
  • Oracle Database 18c 的10大新特性一览

    在 2017 Oracle OpenWorld大会上,关于 Oracle 18c 的一系列新特性已经被披露出来,借助分散在各个会场的公开分享主题和内容、OOW提...

    数据和云
  • Oracle 18c十大新特性

    虽然我们主要用的还是11g,但是Oracle 18c、19c,甚至20c逐渐成为了选择,eygle的博客中对Oracle 18c、19c和20c十大新特性做了介...

    bisal
  • 干货 | 史上最好记的神经网络结构速记表(上)

    本文提供了神经网络结构速查表,盘点了神经网络的大量框架,并绘制了直观示意图进行说明,是人手必备的神经网络学习小抄。 新的神经网络结构不断涌现,我们很难一一掌握。...

    AI科技评论
  • 史上最好记的神经网络结构速记表(上)

    翻译 / 陈俊雅 校对 / 李傲 整理 / 雷锋字幕组 本文提供了神经网络结构速查表,盘点了神经网络的大量框架,并绘制了直观示意图进行说明,是人手必备的神经网络...

    AI研习社
  • 【分布式架构之旅-理论篇】为什么需要高质量API网关接口

    用户2032165
  • 【深度学习】详细的神经网络架构图

    将这些架构绘制成节点图的一个问题:它并没有真正展示这些架构的工作方式。比如说,变自编码器(VAE)可能看起来和自编码器(AE)一样,但其训练过程却相当不同。训练...

    机器人网
  • 【中秋赏阅】美丽的神经网络:13种细胞构筑的深度学习世界

    【新智元导读】人是视觉动物,因此要了解神经网络,没有什么比用图将它们的形象画出来更加简单易懂了。本文囊括 26 种架构,虽然不都是神经网络,但却覆盖了几乎所有常...

    新智元

扫码关注云+社区

领取腾讯云代金券