问题
上传图片,在传统的 HTML 前端处理时,这是一个经常遇到的问题。在手机端应用普及的今天,随手可以拍照,上传图片的应用场景更成了家常便饭。
但是现在手机随便拿一个都起码 500万像素,动辄几M甚至十几M数十M的照片,要上传怎么办?无论是流量还是网速,都很成问题。
旧的处理办法,要上传一个图片,我们先得放一个 <input type="file" />
的字段,然后通过 POST 方式,将选择的文件二进制传到服务器端,然后服务器端在来做所有的逻辑操作。
包括显示、限制大小、裁剪、存储等等操作,必须先传输到后台!这对于脆弱的移动端流量,乃至即使是普遍的 50KB/s 的 ADSL 上传带宽,都是然并卵。
拍得好图有什么用,上传不了。
解决方案
还好,在 HTML5 的支持下,我们可以从客户端获取到选择的上传文件的二进制 BLOB 内容,从而可以在客户端对其做任意的操作!
可以参照本博此前的文章,可以实现在上传图片之前在客户端显示其内容:https://www.huangwenchao.com.cn/2015/03/html5-image-preview.html
其实对于压缩和上传而言,只是多了一步操作,各步骤逻辑如下:
- 选择图片,触发上传控件的 change 事件;
- 通过事件的
event.target.files
对象,获取到选择的文件; - 通过
file.size
预判文件大小是否超界; - 通过
window.URL
或者window.webkitURL
的createObjectURL
创建图片资源对象; <!-- 以上是此前图片展示里面说明的部分内容,下面是压缩 -->
- 创建 canvas,将图片绘制到 canvas 中;
- 对 canvas 进行裁切缩放,以将图片的像素尺寸压缩到合理的范围(动辄超500万像素是没必要的);
- 对 canvas 上面的图片执行压缩,通过 png 或者 jpeg 压缩方法导出文件的二进制内容;
- 将压缩之后的图片二进制内容放回表单再销毁过程中使用的资源(img/canvas/window.URL等等)。
代码解析
首先我们需要有一个这样的 markup:
<input type="file" id="upload_image" />
<input type="hidden" id="compressed" name="image" />
仅此而已,然后我们要在选择文件之后触发一系列的动作:
var $upload = $('#upload_image').change(function(event) {
var files = event.target.files, file;
if(files && files.length > 0 && images_num < MAX_IMAGES_NUM) {
file = files[0];
var URL = window.URL || window.webkitURL;
if(!URL) {
alert('你的浏览器不支持客户端图片处理!');
return false;
}
var imgURL = URL.createObjectURL(file);
$('#compressed').val(getCompressedImageBase64(imgURL));
}
});
上面是前面步骤的 1-4 步,通过这个方法,我们已经获得了 imgURL
这个可以通过客户端加载图片的链接(注意这个 URL 的形式大概是 blob://...
这个样子,完全是没有触及到任何的网络传输功能的!)。
好了,我们现在来实现 getCompressedImageBase64(imgURL)
这个函数,这个函数接受一个合法的 image 资源 url,然后将其压缩,并且返回压缩之后的图片的 base64 编码内容。
var getCompressedImageBase64 = function(imgURL) {
var $img = $('<img>');
$img.one('load', function() {
var c = document.createElement('canvas');
var ctx = c.getContext('2d');
var w0 = $img.width();
var h0 = $img.height();
// 假设要将宽度压缩到 1024,那么目标宽度就是 w=1024,目标高度就是 h=h0/w0*1024
c.width = 1024;
c.height = h0/w0*1024;
// 填上白色背景,否则对于透明图会有 bug
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, c.width, c.height);
// 将图片渲染上去
ctx.drawImage($img[0], 0, 0, c.width, c.height);
// 通过 HTML5 编码压缩并且获取 base64,这里使用 jpeg 压缩,可能不太理想,但起码能够缩小体积,png 也可以,但文件体积无法缩小
var img_data = c.toDataURL('image/jpeg', 0.5);
// 释放资源
URL.revokeObjectURL(imgURL);
return img_data;
});
$img.attr('src', imgURL);
};
注意 c.toDataURL
这个方法是较新版浏览器才支持的编码方法,有些浏览器还是不支持的(大名鼎鼎的微信浏览器目前实测无法压缩 jpeg,只能不压缩导出 png)。
为了解决上述问题,我特意在 StackOverflow 上面找到了问题 http://stackoverflow.com/a/19286473/2544762 并且得到一个很好的答案。
我们可以从 ctx 里面直接通过 getImageData
方法获得图形矩阵的二进制数据,然后通过这个第三方 js 类库来将其压缩编码成 jpeg 的 base64 编码,由于这个是纯粹手动实现,因此兼容性极好:
var img_data = ctx.getImageData(0, 0, c.width, c.height);
var encoder = new JPEGEncoder();
img_data = encoder.encode(img_data);
这个 JPEGEncoder 的类库这里也直接提供一下:
https://gist.github.com/fish-ball/e30ca9d654cdcbebf86e
<script src="https://gist.github.com/fish-ball/e30ca9d654cdcbebf86e.js"></script>
纯粹借花敬佛,非本人发明,仅为便于查看使用,具体来源请参见上面 Stackoverflow 里面的问题。
至此问题基本已经解决,后面在后台还需要对接收到的 base64 格式文件设法解开一下即可。
后记
其实做到这里,已经很接近可以把一个 file 控件做成是自动压缩的了,假如我们可以将图片压缩之后,再写回 files 对象里面,又或者我们可以模拟到 multipart/form
的 HTTP body 提交模式,按照同样的规则设置 post 表单,可能从接口上可以做到透明,这样子就可以把这部分工作封装成一个控件了,这个暂且留个尾巴,日后机缘到了再行补充。
【转载请附】愿以此功德,回向 >>
原文链接:http://www.huangwenchao.com.cn/2015/08/html5-client-image-compress.html【HTML5 客户端图片压缩】
楼主你好,我有个疑问要请教下。 var w0 = img.width(); var h0 = img.height();
img对象是从哪来的?
好像是笔误,改成
$img
应该没问题主要这篇玩意改过几次,开始写的是
后来全部改成 jquery 了