HTML5 file API加canvas实现图片前端JS压缩并上传

这篇文章发布于 2017年07月30日,星期日,23:47,归类于 Canvas相关, JS实例。 阅读 142922 次, 今日 7 次 63 条评论

 

一、图片上传前端压缩的现实意义

对于大尺寸图片的上传,在前端进行压缩除了省流量外,最大的意义是极大的提高了用户体验。

这种体验包括两方面:

  1. 由于上传图片尺寸比较小,因此上传速度会比较快,交互会更加流畅,同时大大降低了网络异常导致上传失败风险。
  2. 最最重要的体验改进点:省略了图片的再加工成本。很多网站的图片上传功能都会对图片的大小进行限制,尤其是头像上传,限制5M或者2M以内是非常常见的。然后现在的数码设备拍摄功能都非常出众,一张原始图片超过2M几乎是标配,此时如果用户想把手机或相机中的某个得意图片上传作为自己的头像,就会遇到因为图片大小限制而不能上传的窘境,不得不对图片进行再处理,而这种体验其实非常不好的。如果可以在前端进行压缩,则理论上对图片尺寸的限制是没有必要的。

二、图片前端JS压缩并上传功能体验

特意制作了一个图片前端压缩并上传的完整demo,您可以狠狠的点击这里:使用canvas在前端压缩图片并上传demo

进入demo会看到一个相貌平平的文件输入框:

相貌平平

啊,不对,应该是这张图:

相貌平平文件选择框

点击文件选择框,我们不妨选一张尺寸比较大的图片,例如下面这种2M多的钓鱼收获照:

上传演示使用的图片

于是图片歘歘歘地传上去了:
上传相关信息截图

此时我们点击最终上传完毕的图片地址,会发现原来2M多3000多像素宽的图片被限制为400像素宽了:
图片缩小后在浏览器中的预览效果图

保存到本地会发现图片尺寸已经变成只有70K了:
保存到本地显示的图片尺寸

以上就是图片前端压缩并上传demo的完整演示。

三、HTML5 file API加canvas实现图片前端JS压缩

要想使用JS实现图片的压缩效果,原理其实很简单,核心API就是使用canvasdrawImage()方法。

canvasdrawImage()方法API如下:

context.drawImage(img, dx, dy);
context.drawImage(img, dx, dy, dWidth, dHeight);
context.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

后面最复杂的语法虽然看上去有9大参数,但不用慌,实际上可以看出就3个参数:

img
就是图片对象,可以是页面上获取的DOM对象,也可以是虚拟DOM中的图片对象。
dx, dy, dWidth, dHeight
表示在canvas画布上规划处一片区域用来放置图片,dx, dy为canvas元素的左上角坐标,dWidth, dHeight指canvas元素上用在显示图片的区域大小。如果没有指定sx,sy,sWidth,sHeight这4个参数,则图片会被拉伸或缩放在这片区域内。
sx,sy,swidth,sheight
这4个坐标是针对图片元素的,表示图片在canvas画布上显示的大小和位置。sx,sy表示图片上sx,sy这个坐标作为左上角,然后往右下角的swidth,sheight尺寸范围图片作为最终在canvas上显示的图片内容。

drawImage()方法有一个非常怪异的地方,大家一定要注意,那就是5参数和9参数里面参数位置是不一样的,这个和一般的API有所不同。一般API可选参数是放在后面。但是,这里的drawImage()9个参数时候,可选参数sx,sy,swidth,sheight是在前面的。如果不注意这一点,有些表现会让你无法理解。

下图为MDN上原理示意:
Canvas drawimage()原理示意

对于本文的图片压缩,需要用的是是5个参数语法。举个例子,一张图片(假设图片对象是img)的原始尺寸是4000*3000,现在需要把尺寸限制为400*300大小,很简单,原理如下代码示意:

var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
canvas.width = 400;
canvas.height = 300;
// 核心JS就这个
context.drawImage(img,0,0,400,300);

把一张大的图片,直接画在一张小小的画布上。此时大图片就天然变成了小图片,压缩就这么实现了,是不是简单的有点超乎想象。

当然,若要落地于实际开发,我们还需要做些其他的工作,就是要解决图片来源和图片去向的问题。

1. 如何把系统中图片呈现在浏览器中?

HTML5 file API可以让图片在上传之前直接在浏览器中显示,通常使用FileReader方法,代码示意如下:

var reader = new FileReader(), img = new Image();
// 读文件成功的回调
reader.onload = function(e) {
  // e.target.result就是图片的base64地址信息
  img.src = e.target.result;
};
eleFile.addEventListener('change', function (event) {
    reader.readAsDataURL(event.target.files[0]);
});

于是,包含图片信息的context.drawImage()方法中的img图片就有了。

2. 如果把canvas画布转换成img图像
canvas天然提供了2个转图片的方法,一个是:

canvas.toDataURL()方法
语法如下:

canvas.toDataURL(mimeType, qualityArgument)

可以把图片转换成base64格式信息,纯字符的图片表示法。

其中:
mimeType表示canvas导出来的base64图片的类型,默认是png格式,也即是默认值是'image/png',我们也可以指定为jpg格式'image/jpeg'或者webp等格式。file对象中的file.type就是文件的mimeType类型,在转换时候正好可以直接拿来用(如果有file对象)。
qualityArgument表示导出的图片质量,只要导出为jpgwebp格式的时候此参数才有效果,默认值是0.92,是一个比较合理的图片质量输出参数,通常情况下,我们无需再设定。

canvas.toBlob()方法
语法如下:

canvas.toBlob(callback, mimeType, qualityArgument)

可以把canvas转换成Blob文件,通常用在文件上传中,因为是二进制的,对后端更加友好。

toDataURL()方法相比,toBlob()方法是异步的,因此多了个callback参数,这个callback回调方法默认的第一个参数就是转换好的blob文件信息,本文demo的文件上传就是将canvas图片转换成二进制的blob文件,然后再ajax上传的,代码如下:

// canvas转为blob并上传
canvas.toBlob(function (blob) {
  // 图片ajax上传
  var xhr = new XMLHttpRequest();
  // 开始上传
  xhr.open("POST", 'upload.php', true);
  xhr.send(blob);    
});

于是,经过“图片→canvas压缩→图片”三步曲,我们完成了图片前端压缩并上传的功能。

更加完整的核心代码请参见demo页面的左侧,如果对其他交互代码也敢兴趣,请参考页面源代码。

四、结束语

就在几个月前刚写过一篇文章“使用canvas在前端实现图片水印合成”,实际上所使用的技术和套路和本文是如出一辙的,也是“图片→canvas水印→图片”三步曲,区别在于水印合成是连续执行两次context.drawImage()方法,一次是原图一次水印图片,以及最后转换成图片的时候什么是toDataURL()方法,其他代码逻辑和原理都是一样的。

由此及彼,利用同样的原理和代码逻辑,我们还可以实现其它很多以前前端不太好实现的功能,比方说图片的真剪裁效果,所谓“真剪裁”指不是使用个overflow:hidden或者clip这些CSS属性的“伪剪裁”,而是真正意义上就这么大区域图片信息。甚至配合一些前端算法,我们可以直接在前端进行人脸识别,图片自动美化等一系列功能再上传等等。

原理都是一样的,都是利用canvas作为中间媒介进行处理。

好,以上就是本文的全部内容,感谢阅读,欢迎纠错,欢迎交流!

(本篇完)

分享到:


发表评论(目前63 条评论)

  1. abc说道:

    new XMLSerializer().serializeToString
    这一步将svg 转为base64 会使得 dom中的所有样式失效。
    序列化不包括样式。不知道有没有解决的方法。

  2. 上传upload.php 不知道怎么上传呢?大佬可否指点下说道:

    上传upload.php 不知道怎么上传呢?大佬可否指点下

  3. 黑衣人说道:

    上传upload.php 不知道怎么上传呢?大佬可否指点下

    这是我的上传代码

  4. HYLCPY说道:

    大侠,您的完整代码可以发一份给我吗?谢谢!特别是上传部分,做得太好了

  5. 冬未至说道:

    使用Canvas的图片压缩还是太粗糙,我是做人像识别相关开发的,也会用到H5拍照后上传到服务器进行人像对比。所以在压缩图片的同时又同时希望画质不要下降,请问是否有什么比较好的图片压缩的第三方API可以推荐的呢?

  6. cainiao说道:

    请教一个问题,苹果手机竖屏拍的照片和横屏拍的照片,用你的demo测试,得到的结果为:竖屏拍的照片变成了横屏显示。调试了下,图片本来尺寸是2448*3264,代码里面拿到的this.width=3264,this.height=2448,不知道问题出在哪里

    • 阿伟的小蝴蝶说道:

      这是由于压缩时候导致图片旋转信息丢失了,压缩前需要配合使用exif.js拿到旋转值。

  7. 我自向天笑说道:

    请教您一个问题:将苹果手机竖屏拍的照片用canvas绘制出来的时候为何会有一部分图片被剪切掉绘制不出来?我已研究多天,尝试了很多方法,修改参数或者旋转图片均不奏效

    • 张 鑫旭说道:

      安卓机器没有这个问题吗?

      • 我自向天笑说道:

        就测试的小米,华为,vivo这三款安卓机而已,未发现问题,苹果用原生js实现也不存在这个问题。我用的是ionic3框架,在这个框架下就会出问题,经过多次测试,怀疑是naturalWidth和naturalHeight与图片的实际不符,同样的图片,两种写法得到的结果恰恰相反,现在还在想办法处理。

        • noahlam说道:

          苹果照片好像分辨率都是1,而普通的照片是 72,会不会是这一个原因导致 naturalWidth 和 naturalHeight 计算不准确?

    • 同问,你的问题解决了么?说道:

      H5通过canvas绘制图片并上传的坑真的是太多了,
      1:第一个问题跟你遇到的一样
      2:第二个问题,Android没发现,苹果机竖屏拍的照片会旋转90°,有一种处理办法是,通过这个插件Exif.js取到拍摄的角度,然后做对应的角度旋转,但是我亲测发现无论横屏还是竖屏,这个值都是6,所以通过这个插件解决失败

      • fly说道:

        targetHeight = Math.round(maxWidth * (originHeight / originWidth));
        targetWidth = Math.round(maxHeight * (originWidth / originHeight));
        这两个地方写反了吧?

  8. abaddonpoet说道:

    人脸识别已经有js库了,opencv和ccv。我用过了。。感觉超棒。。就是挨千刀的索尼手机照片exif信息总是不随主流。。方向老是反的-,-

  9. 前端Logic说道:

    用toBlob得到流数据后,上传时显示fileName为”blob”。
    前端能自己设置这个上传的文件名吗

  10. 前端中白说道:

    学到了 !!感激不尽

  11. C# 后台怎么接收啊说道:

    C# 后台怎么接收啊

  12. 大神说道:

    大神大神,快收下我的膝盖!!!

  13. BaoMax说道:

    canvas.toBlob的方法会阻塞页面,但是它不是异步的吗,大佬知道因为啥吗

  14. vcxiaohan说道:

    这个应该是缩放尺寸吧,压缩应该是对图片质量的压缩
    canvas.toDataURL(“image/jpeg”, [压缩比率])

  15. 图片压缩问题说道:

    被压缩的图片上传后模糊失真

  16. Valk说道:

    目前正在实现一个功能,利用canvas合成图片并上传到服务器。然后在微信端展示出来的。现在的问题是在canvas上传到服务器这一步出了问题。谷歌浏览器是没问题的,然而搜狗浏览器不支持toBlob(想要各浏览器兼容),因此我使用了toDataURL,得到base64的数据,然后 blob = new Blob([ 图片的base64数据 ],’image/png’) ;然后将这个blob对象传到服务器。一切都正常,然而后台得到的图片展示不出来了。猜测是这样转成blob,数据有丢失?请问大神有没有其他方法可以将canvas画出来的图片传到服务器(不支持toBlob的情况下)?

  17. Faith说道:

    已按您的说的成功写出案例,但是在使用中,发现图片模糊的比较模糊,就算把qualityArgument设为1,得到的图片也很模糊,比如拍一张A4,等压缩好上传之后,字基本上都是看不清楚的。请问,还有没有什么比较好的解决方案。

    • Flcwl说道:

      把drawImage()的 dWidth, dHeight两个参数调大试试,应该可以让像素高点,当然空间也就占的更大了。

  18. IMIN说道:

    鑫大王,我点你那个上传,不小心把我驾驶证上传了。。。你可别拿我驾驶证扣分啊。。QAQ

  19. IE中不兼容
    file.type.indexOf(“image”) 始终为 -1
    canvas.toBlob(function (blob) { … … 在IE中出错

    换了几个JQ(包括最新版的)都不兼容

  20. 白铭说道:

    upload.php 代码 能给一下吗?

  21. 说道:

    支持多个图片上传吗

  22. 压缩后的图片很模糊说道:

    压缩后的图片很模糊,应该是没处理高倍屏

    • Faith说道:

      那你怎么实现压缩之后,图片还是清晰的呀?我现在也是,压缩完之后,图片的内容一点也看不清楚。

  23. winlion说道:

    参考楼主代码封装了一个demo
    http://www.imwinlion.com/archives/1
    前端js 后端go

  24. xiaoxin说道:

    最近使用FileReader出现一个问题,在小米2s手机上拍照上传中,读取成功后获取不到编码result值,弹出是data:,后面什么都没有,用别的手机测试没问题,而且是只有拍照是这样,选择相册是可以获取到该值的,现在也不知道是什么原因造成的,不知道大神遇到过没有

  25. vc2001895说道:

    求处理页的upload.php demo

  26. 请问一下说道:

    为什么canvas建立之后要
    // 清除画布
    context.clearRect(0, 0, targetWidth, targetHeight);
    这里清除了一下。
    但建立之后,应该是个白屏(或透明色)
    即便有颜色,直接覆盖绘制,结果不是也一样的吗?
    求解答。

  27. 小白说道:

    H5在真机上好像不兼容

  28. boyang说道:

    裁剪尺寸的图转blob对象 size 会比 原图转blob对象的 size 大很多是为什么?

  29. Faith说道:

    移动端用canvas的时候,为了保证适配,用的rem为单位布局,可是canvas的width不支持rem单位,所以移动端如何做?

  30. xiaobin说道:

    很受益

  31. yuye说道:

    能 公布后台 upload.php 代码吗,我想看看后台怎么接收的

  32. kml1200说道:

    请问:
    使用FileReader将文件转换为base64

    使用window的URL工具的createObjectURL方法将文件转换成bool对象,
    让图片在上传之前在浏览器中显示,有什么差别呢,哪种方法较好?

  33. 益阳说道:

    把一张大的图片,直接画在一张小小的画布上。用户体验不好呀。

  34. leega0说道:

    在Mac的safari下,TypeError: canvas.toBlob is not a function. (In ‘canvas.toBlob’, ‘canvas.toBlob’ is undefined)
    看来不支持canvas这个方法

  35. JUNE说道:

    你好!请问upload.php应该怎么接收传过来的数据,upload.php文件传我一份呗;谢啦

  36. 摩缺说道:

    我做微信公众h5注册页面需要图片上传,也出现上传图片过大问题,后面采用了canvas压缩的办法,但是出现一个新的问题,压缩前的图片过大,压缩时会出现浏览器崩溃闪退问题,目前测试出较大图片压缩,以及低配的手机(小米4、Nexus 5等)出现此类问题频率较高

  37. wusy说道:

    后台代码 upload.php
    我用java怎么写
    以前都是 后台接受file 然后上传源文件 现在后台怎么写呢?
    以前
    —————————–32525982912960643881027537595
    Content-Disposition: form-data; name=”upload”; filename=”truck.png”
    Content-Type: image/png

    —————————–32525982912960643881027537595–

    现在:没有上面的内容 后台咋接受

  38. andy说道:

    为什么没有demo下载呢?因为里面使用到了upload.php ,那就要配合后端使用。能提供这个php的demo么?

  39. 系心说道:

    “使用canvas在前端实现图片水印合成”那个超链接地址放错了……

  40. wyl说道:

    不用注册也可以填吗

  41. ptrsvltns说道:

    沙发!!!