分类 前端 下的文章

用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以上设备实测,当用户点击这个文件选择器的时候,手机会自动调出图片库,并带有拍照选择。

PictureSelect

读文件生成预览

用户选择了图片之后,需要读取文件内容,读出的内容可供生成预览图,也可以供后面压缩使用。使用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 查看上传范例程序,或者拍摄下面的二维码:

3385794708.png

因为Android版的微信使用的是QQ浏览器的X5内核,而X5又是Webkit一个早期的分支版本(从UA上能看到是AppleWebKit/533),所以它并没有提供Blob构造器,也就无法使用new Blob()这样的语句,不过它包括了WebKitBlobBuilder,DEMO中实现了一个BlobConstructor来兼容微信,另外Webkit 534版以下的Chrome分支都存在FormData上传文件会变成0字节的问题,Andy EStackoverflow上提供了一个解决方案,我把它移植到DEMO里了。

DEMO程序的服务器端是PHP实现的,用于演示只有一行:

<?php var_dump($_FILES);?>

此DEMO没有实现图片等比缩放的逻辑。

瀑布式照片墙排版算法研究

产品中有一个个人照片墙的页面,设计想出了瀑布式的版式,大家都认为视觉效果不错,下面的原型图大致说明了这种版式的效果。

3074727575.png

照片是用户自己上传的,所以照片的尺寸是不确定的。当然我们也可以完全不在意图片的尺寸,可以用css把它们排版成版式要求的样子。不过由于图片的宽高比并不确定,如何安排每一张图的位置就成为需要解决的关键问题。

按序号奇偶性分发图片

最简单也最直观的方法是直接把图片编号,奇数号放入左栏,偶数号放入右栏。如果用户每张图片的宽高比都差不多,可以用这种方法取得比较好的效果。但是这种方法存在两栏高度差异巨大的风险,如果恰好用户上传照片是横、竖、横、竖……这种顺序,那就会出现左矮右高的悲剧。

3784789452.png

向较矮栏分发图片

针对上面方法出现的问题,一个简单的优化是直接在每次置入图片前计算左右两栏各自的高度,然后将图片置入较矮的一栏。因为只有图片加载出来才能知道它的高度,所以不能像前面的方法那样一股脑儿全塞进左右栏中,需要等图片载成功后一张一张放入。

正因为要等图片加载成功才放入,所以这种算法是不稳定的,可能每次刷新时图片的顺序都不一样。另外这个算法提供的只是一个小改进,它并不能得出让左右栏高度最接近的方案,在出现特长图的时候问题更为明显。

3589395582.png

动态规划解

有没有可能找到一个最优的方案,保证所有图片置入后,左右栏高度最为接近呢?图片的宽度是固定的,变化的只会是高度,问题可以转化为一个抽象的数学问题:将一个由n个整数元素构成的集合N,划分成两个子集AB,确保N = A + B,求能使SUM(A)最为接近SUM(B)的划分方法。

把问题抽象化之后就比较好解了。根据前述条件可知SUM(B) = SUM(N) - SUM(A),要求SUM(A)最为接近SUM(B),不妨假设SUM(A) ≤ SUM(N)/2 ≤ SUM(B),那么问题就变成了从集合N中选中若干元素构成集合A,使SUM(A)最为接近SUM(N)/2。这下问题变得很眼熟了吧,俨然就是背包问题啊。

背包问题及其各种变种的详细解法请参考Tianyi Cui写的《背包问题九讲》,这里我只对这个问题求解。不妨先考虑集合N中的第一个元素N[1],需要决定把它放入或者不放入集合A中,从而使得SUM(A)最接近SUM(N)/2。如果不把N[1]放入集合,那么需要知道对于第二到第n个元素,能构成的最接近SUM(N)/2的和是多少;而如果把N[1]放入集合中,则需要知道对于第二到第n个元素,能构成的最接近SUM(N)/2 - N[1]的和是多少。通过比较就可以确定是否应该放入N[1]。同理,对于第二个元素N[2],决策前需要比较的是第三到第n个元素的放置加和情况。直到第n个元素,放入或者不放一目了然。当记录下这些放置组合之后,可以找出最优决策链。

总结

奇偶分发法最为简单,也最容易遇到麻烦,只适合所有图片尺寸相同(比如Instagram)的情况下使用。

向最矮栏分发法虽然无法取得最优解,但也能得到不错的结果。因为无需提前知晓全部图片的尺寸,比较适合用在分批无限载入的流式布局中。这种方法的最大问题在于图片载入顺序对排版结果影响巨大,如果先成功载入短图(短图一般比较小,很容易先完成载入),较均匀地分发后方才载入长图,可能出现严重的分配不均情况,可以由服务器端下发图片尺寸信息来帮助缓解这一情况,从长到短分发能取得更好的效果。

动态规划的方法可以求得最优解,但算法的复杂度相对较高,而且需要知道所有图片的高度之后才能开始运行,不适合用在图片可以加载更多的流式场景。当然,如果服务器端能提供图片尺寸信息,就能绕过上述缺点。

前面提供了三种实现思路。最后可以看一下例子程序

一些常用的html头标签

有一些常用的html头标签,不容易记忆,所以干脆记在这里。

viewport 控制手机浏览器虚拟屏幕大小

<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">

viewport是用来描述手机浏览器渲染区域的大小的,如果不写上面这句声明,手机浏览器很可能会把页面放在桌面大小的宽度上渲染,然后缩小显示在手机屏幕上。加上上面这句viewport的声明后,手机浏览器就会按设备的宽度来渲染页面。viewport的功能比较复杂,涉及缩放,CSS象素大小等一堆定义,想详细了解可以Google搜索viewport

refresh 延时跳转页面

<meta http-equiv="refresh" content="5; url=http://www.example.com/">

这个标签现在用的少了,论坛系统里比较常见,浏览器会在5秒后跳转到 http://www.example.com/ 上。

shortcut icon 网站图标

<link rel="shortcut icon" href="http://www.example.com/favicon.ico" type="image/x-icon">

上述声明定义了网站的图标,浏览器会把这个图标显示在网站的标签页或者历史记录中。如果没有写上面的声明,浏览器会自动试图请求网站域名根目录下的favicon.ico文件作为图标。所以这个标签仅在favicon不在站点域名根目录下时使用。默认图标应该使用微软的ico格式,不过现代浏览器对png等格式也支持,可以在type属性中定义。

apple-touch-icon 网站的大图标

<link rel="apple-touch-icon" href="http://www.example.com/apple-touch-icon.png">

如果用iOS上Safari的把网站添加桌面的功能,这个图标会显示在桌面上;另外Opera浏览器显示SpeedDial图标时也使用这个。如果没有上面的声明,默认会使用网站域名根目录下的apple-touch-icon.png文件,为了更好的兼容性,应该使用png格式。

x-ua-compatible 请求与网站兼容的渲染引擎

<meta http-equiv="x-ua-compatible" content="ie=5; ie=8">

有了这句声明,如果可以的话,对于IE8以上的浏览器,会用ie8的引擎渲染,对于IE6、7等,会用IE5的引擎渲染。

RSS 聚合源

<link rel="alternate" type="application/atom+xml" title="ATOM 1.0" href="http://www.example.com/atom.xml">
<link rel="alternate" type="application/rss+xml" title="RSS 2.0" href="http://www.example.com/rss.xml">

上面的两句声明告诉浏览器RSS聚合源的URL是什么,一个使用RSS格式标准,另一个使用ATOM格式,供支付的浏览器选择。