用Canvas技术压缩要上传的图片
背景
现在摄像头已经是手机的标配了,移动网站也做得越来越像APP。然而拍照上传这件事情的体验似乎仍然不如APP,主要原因是现在手机拍摄的照片太大,上传非常消耗流量也非常耗时。APP都会在上传前缩小要上传的照片尺寸,以期更节省流量和时间。在HTML5时代,利用文件API和Canvas技术,Web上也可以做到图片压缩上传。
过滤文件类型
首先我们希望用户能直接选择手机照片,而不是在各种类型的文件中选择。只需要在input
标签中加入accept
属性就可以实现这一点:
<div id="preview"></div>
<form>
<input type="file" accept="image/*">
<input type="submit">
<form>
在Android4以上,iOS7以上设备实测,当用户点击这个文件选择器的时候,手机会自动调出图片库,并带有拍照选择。
读文件生成预览
用户选择了图片之后,需要读取文件内容,读出的内容可供生成预览图,也可以供后面压缩使用。使用HTML5的FileReader API可以达成这一目的。
var file = document.querySelector("[type=file]");
file.addEventListener("change", function(e) {
for (var i = 0, f; f = e.target.files[i]; i++) {
if (f.type.indexOf("image") !== 0) continue;
var reader = new FileReader();
reader.onload = function(e) {
var img = document.createElement("img");
img.src = e.target.result;
document.getElementById("preview").appendChild(img);
}
reader.readAsDataURL(f);
}
}, false);
如果不需要预览图,可以不把img对象添加到DOM上。
压缩
利用Canvas渲染上下文的drawImage接口,可以把一张图片绘制到Canvas上,在这个过程中可以重新定义图片尺寸,然后再用Canvas的toDataURL接口可以生成出压缩后的图片。
var images = document.querySelectorAll("#preview img");
var dstWidth = 400, dstHeight = 300;
var compressedImages = [];
[].forEach.call(images, function (image) {
var canvas = document.createElement("canvas");
canvas.width = dstWidth;
canvas.height = dstHeight;
canvas.getContent("2d").drawImage(image); // 这里传入img元素对象
var compressed = canvas.toDataURL("image/jpeg", 0.7);
compressedImages.push(compressed);
});
上传
前面一步骤生成的压缩后图片是Data URL
形式的,上传前需要把开头部分的data:image/jpeg;base64,
截掉才是图片的Base64编码形式。
可以直接把Base64的字符串上传到服务器,然后由服务端解码为JPG图片,也可以在前端解码上传。如果要在前端解码并以文件方式上传,先要用atob函数把Base64解开,然后转换为ArrayBuffer,再用它创建一个Blob对象。文件方式上传需要用multipart/form-data
格式,可以利用FormData对象组装生成好的Blob对象来实现。
function b64toBlob(b64Data, contentType, sliceSize) {
contentType = contentType || '';
sliceSize = sliceSize || 512;
var byteCharacters = atob(b64Data);
var byteArrays = [];
for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
var slice = byteCharacters.slice(offset, offset + sliceSize);
var byteNumbers = new Array(slice.length);
for (var i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
var byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
var blob = new Blob(byteArrays, {type: contentType});
return blob;
}
var fileBlob = b64toBlob(compressed.substr(23), "image/jpeg");
var fd = new FormData();
fd.append("file", fileBlob);
var xhr = new XMLHttpRequest();
xhr.open("POST", "upload.php");
xhr.send(fd);
安全
最后,请不要忘记在服务端对用户上传的文件数据进行合法性检查,毕竟黑客也可能用非浏览器上传垃圾文件或者恶意脚本。
DEMO
您可以访问 https://xts.so/demo/compress/index.html 查看上传范例程序,或者拍摄下面的二维码:
因为Android版的微信使用的是QQ浏览器的X5内核,而X5又是Webkit一个早期的分支版本(从UA上能看到是AppleWebKit/533),所以它并没有提供Blob构造器,也就无法使用new Blob()
这样的语句,不过它包括了WebKitBlobBuilder
,DEMO中实现了一个BlobConstructor来兼容微信,另外Webkit 534版以下的Chrome分支都存在FormData上传文件会变成0字节的问题,Andy E在Stackoverflow上提供了一个解决方案,我把它移植到DEMO里了。
DEMO程序的服务器端是PHP实现的,用于演示只有一行:
<?php var_dump($_FILES);?>
此DEMO没有实现图片等比缩放的逻辑。