图片压缩上传

参考1-HTML5实现图片压缩上传功能
参考2-移动前端—图片压缩上传实践
参考3-移动端H5图片压缩上传

大体步骤

  1. 用户使用input file上传图片的时候,利用 FileReader ,读取 blob对象 ,或者是 file对象 ,将图片转化为 data uri (base64格式)的形式。
  2. 使用 canvas ,在页面上新建一个画布,利用 canvas 提供的API,将图片画入这个画布当中。
  3. 利用 canvas.toDataURL() ,进行图片的压缩,得到图片的 data uri 的值,用来上传。
  4. 获取到压缩后的base64格式图片数据,转成二进制塞入formdata,再通过XmlHttpRequest提交formdata。

页面结构

<input type="file" id="choose" accept="image/*" multiple>
<ul class="img-list"></ul>
<a id="upload">上传图片</a>
<span class="tips">只允许上传jpg、png及gif</span>

预览上传的图片

  1. 先是获取图片数据,也就是监听input file的change事件,然后获取到上传的文件对象files,将类数组的files转成数组,然后进行forEach遍历。
  2. 接着判断文件类型,如果不是图片则不作处理。
  3. 如果是图片就实例化一个filereader,利用FileReader读取file对象,将图片格式转化为data URI的形式。
  4. 在进行图片压缩前,还是对图片大小做了判断的,如果图片大小(数据长度)大于200KB时,是直接进行图片上传并预览,不进行图片的压缩;如果图片的大小是大于200KB,则是先进行图片的compress方法压缩、预览再上传。
// 获取选择文件表单
var filechooser = document.getElementById("choose");
// 点击上传
$("#upload").on("click", function() {
          filechooser.click();
    })
    .on("touchstart", function() {
          $(this).addClass("touch")
    })
    .on("touchend", function() {
          $(this).removeClass("touch")
    });
filechooser.onchange = function() {
    if (!this.files.length) return;
    var files = Array.prototype.slice.call(this.files);
    if (files.length > 9) {
        alert("最多同时只可上传9张图片");
        return;
    }
    files.forEach(function(file, i) {
        if (!/\/(?:jpeg|png|gif)/i.test(file.type)) return;
        var reader = new FileReader();
        var li = document.createElement("li");
              //   获取图片大小
        var size = file.size / 1024 > 1024 ? (~~(10 * file.size / 1024 / 1024)) / 10 + "MB" : ~~(file.size / 1024) + "KB";
        li.innerHTML = '<div class="img"></div><div class="size">' + size + '</div>';
        $(".img-list").append($(li));
        reader.onload = function() {
            var result = this.result; 
            var img = new Image();
            img.src = result;
            // 如果图片大小小于200kb,则直接上传
            if (result.length <= 200 * 1024) {
                img = null;
                  $(li).children('div.img').append('![]('+result+')'); // 预览
                upload(result, file.type, $(li));  // 直接上传
                return;
            }
            // 图片加载完毕之后进行压缩,然后上传
            if (img.complete) {
                callback();
            } else {
                img.onload = callback;
            }
            function callback() {
                var data = compress(img); // 使用canvas对大图片进行压缩
                  $(li).children('div.img').append('![]('+data+')'); //预览
                upload(data, file.type, $(li));  // 上传
                img = null;
            }
        };
        reader.readAsDataURL(file);
    })
};

图片压缩使用canvas

上面做完图片数据的获取后,就可以做compress压缩图片的方法了。而 压缩图片也并不是直接把图片绘制到canvas再调用一下 toDataURL就行的。

在IOS中,canvas绘制图片是有两个限制的:

  1. 首先是图片的大小,如果图片的大小超过两百万像素,图片也是无法绘制到canvas上的,调用drawImage的时候不会报错,但是你用toDataURL获取图片数据的时候获取到的是空的图片数据。
  2. 再者就是canvas的大小有限制,如果canvas的大小大于大概五百万像素(即宽高乘积)的时候,不仅图片画不出来,其他什么东西也都是画不出来的。

应对第一种限制,处理办法就是瓦片绘制了。瓦片绘制,也就是将图片分割成多块绘制到canvas上,我代码里的做法是把图片分割成100万像素一块的大小,再绘制到canvas上。

而应对第二种限制,我的处理办法是对图片的宽高进行适当压缩,我代码里为了保险起见,设的上限是四百万像素,如果图片大于四百万像素就压缩到小于四百万像素。四百万像素的图片应该够了,算起来宽高都有2000X2000了。

如此一来就解决了IOS上的两种限制了。

除了上面所述的限制,还有两个坑。

  1. canvas的toDataURL是只能压缩jpg的,当用户上传的图片是png的话,就需要转成 jpg,也就是统一用canvas.toDataURL('image/jpeg', 0.1) , 类型统一设成jpeg,而压缩比就自己控制了。
  2. 如果是png转jpg,绘制到canvas上的时候,canvas存在透明区域的话,当转成jpg的时候透明区域会变成黑色,因为 canvas的透明像素默认为rgba(0,0,0,0),所以转成jpg就变成rgba(0,0,0,1)了,也就是透明背景会变成了黑色。解决办法就 是绘制之前在canvas上铺一层白色的底色。

在利用canvas进行绘图的过程中,IOS图片上传过程中,存在着这样的问题:

  1. 当你竖着拿手机的时候,拍完照,上传图片时,会出现照片自动旋转的情况,而横着拍照并上传图片时不会出现这个问题。这个时候如果想纠正图片自动旋转的情况,将图片转化为二进制的数据 (使用了binaryajax.js) ,方便获取图片的 exif信息 ,通过获取 exif的信息 来确定图片旋转的角度 (使用了exif.js) ,然后再进行图片相应的旋转处理。
  2. 在 IOS 中,当图片的大小大于 2MB时,会出现图片压扁的情况,这个时候需要重置图片的比例。
  3. 利用FileReader,读取图片的过程需要花费一定时间,将图片数据注入到canvas画布中需要一定时间,图片压缩的过程中,图片越大,CPU计算消耗的时间也越长,可能会出现顿卡的情况。总之,就是这个过程当中需要花费一定时间。
  4. IOS8.1的版本中有个 FileReader 的bug: FileReader 读取的图片转化为Base64时,字符串为空,遇到这个情况的话- - 还是老老实实把图片不做压缩处理扔给服务端吧。
//    使用canvas对大图片进行压缩
function compress(img) {
    //    用于压缩图片的canvas
    var canvas = document.createElement("canvas");
    var ctx = canvas.getContext('2d');
    //    瓦片canvas
    var tCanvas = document.createElement("canvas");
    var tctx = tCanvas.getContext("2d");

    var initSize = img.src.length;
    var width = img.width;
    var height = img.height;
    //如果图片大于四百万像素,计算压缩比并将大小压至400万以下
    var ratio;
    if ((ratio = width * height / 4000000) > 1) {
        ratio = Math.sqrt(ratio);  // 平方根
        width /= ratio;
        height /= ratio;
    } else {
        ratio = 1;
    }
    canvas.width = width;
    canvas.height = height;
    //   铺底色(防止png透明背景转成JPG变黑色)
    ctx.fillStyle = "#fff";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    //  如果图片像素大于100万则使用瓦片绘制
    var count;
    if ((count = width * height / 1000000) > 1) {
        count = ~~(Math.sqrt(count) + 1); // 计算要分成多少块瓦片  
           //   计算每块瓦片的宽和高
        var nw = ~~(width / count);    // ~~取整
        var nh = ~~(height / count);
        tCanvas.width = nw;
        tCanvas.height = nh;
        for (var i = 0; i < count; i++) {
            for (var j = 0; j < count; j++) {
           //  drawImage(image, sourceX, sourceY, sourceWidth, sourceHeight,destX, destY, destWidth, destHeight)
                tctx.drawImage(img, i * nw * ratio, j * nh * ratio, nw * ratio, nh * ratio, 0, 0, nw, nh);  
                ctx.drawImage(tCanvas, i * nw, j * nh, nw, nh);
            }
        }
    } else {
        ctx.drawImage(img, 0, 0, width, height);
    }
    //进行最小压缩(将原来图片的质量压缩到原先的0.1倍)
    var ndata = canvas.toDataURL('image/jpeg', 0.1);
    console.log('压缩前:' + initSize);
    console.log('压缩后:' + ndata.length);
    console.log('压缩率:' + ~~(100 * (initSize - ndata.length) / initSize) + "%");
    tCanvas.width = tCanvas.height = canvas.width = canvas.height = 0;
    console.log(ndata);
    return ndata;
}
drawImage()

图片上传

文件上传有2种方式:

  1. 将图片转化为 base64,直接传给后台。
  2. 先将base64数据转成字符串,再实例化一个ArrayBuffer,然后将字符串 以8位整型的格式传入ArrayBuffer,再通过BlobBuilder或者Blob对象,将8位整型的ArrayBuffer转成二进制对象 blob,然后把blob对象append到formdata里,再通过ajax发送给后台即可。
//    图片上传,将base64的图片转成二进制对象,塞进formdata上传
function upload(basestr, $li) {
    // 将以base64的图片url数据转换为Blob
    var blob = convertBase64UrlToBlob(basestr);
    var pecent = 0, loop = null;
    var xhr = new XMLHttpRequest();
    var formdata = getFormData();
    formdata.append('imagefile', blob);
    xhr.open('post', '/cupload');
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4 && xhr.status == 200) {
            var jsonData = JSON.parse(xhr.responseText);
            var imagedata = jsonData[0] || {};
            var text = imagedata.path ? '上传成功' : '上传失败';
            console.log(text + ':' + imagedata.path);
            clearInterval(loop);
            //当收到该消息时上传完毕
            $li.find(".progress span").animate({'width': "100%"}, pecent < 95 ? 200 : 0, function() {
                $(this).html(text);
            });
            if (!imagedata.path) return;
            $(".pic-list").append('<a href="' + imagedata.path + '">' + imagedata.name + '(' + imagedata.size + ')![](' + imagedata.path + ')</a>');
        }
    };
    //数据发送进度,前50%展示该进度
    xhr.upload.addEventListener('progress', function(e) {
        if (loop) return;
        pecent = ~~(100 * e.loaded / e.total) / 2;
        $li.find(".progress span").css('width', pecent + "%");
        if (pecent == 50) {
            mockProgress();
        }
    }, false);
    //数据后50%用模拟进度
    function mockProgress() {
      if (loop) return;
      loop = setInterval(function() {
        pecent++;
        $li.find(".progress span").css('width', pecent + "%");
        if (pecent == 99) {
          clearInterval(loop);
        }
      }, 100)
    }
    xhr.send(formdata);
}

另外一种方式并不是直接在选择图片这里上传,而是页面还有很多不止图片的数据需要一次性提交的。

//  第一种
var formdata = getFormData(form);  // 获取form表单中所有数据,相当于序列化表单数据
formdata.append('imagefile', blob);  // 追加
$.ajax({
    url: 'http://example.com/api',
    method: 'post',
    processData: false
    contentType: false,
    data: formdata
});

//  第二种 ,form表单直接提交,没有success回调的
var imgSrc = $("img").attr("src");  // 获取到预览img的src属性
// base64格式转换成blob添加到表单中提交给后台
var imageFile = '<input name="image" value="'+convertBase64UrlToBlob(imgSrc)+'"';  
$(form).submit();

将以base64的图片url数据转换为Blob

/**
 * 将以base64的图片url数据转换为Blob
 * @param urlData
 * 用url方式表示的base64图片数据
 */
function convertBase64UrlToBlob(urlData){

    var bytes=window.atob(urlData.split(',')[1]);        //去掉url的头,并转换为byte
    var type = urlData.split('/')[1].split(';')[0];
    console.log(type);
    //处理异常,将ascii码小于0的转换为大于0
    var ab = new ArrayBuffer(bytes.length);
    var ia = new Uint8Array(ab);
    for (var i = 0; i < bytes.length; i++) {
        ia[i] = bytes.charCodeAt(i);
    }

    return getBlob( [ab] , type);
}
/**
* 获取blob对象的兼容性写法
* @param buffer
* @param format
* @returns {*}
*/
function getBlob(buffer, format) {
    try {
        return new Blob(buffer, {type: format});
    } catch (e) {
        var bb = new (window.BlobBuilder || window.WebKitBlobBuilder || window.MSBlobBuilder);
        buffer.forEach(function(buf) {
            bb.append(buf);
        });
        return bb.getBlob(format);
    }
}
/**
* 获取formdata
*/
function getFormData() {
    var isNeedShim = ~navigator.userAgent.indexOf('Android')
        && ~navigator.vendor.indexOf('Google')
        && !~navigator.userAgent.indexOf('Chrome')
        && navigator.userAgent.match(/AppleWebKit\/(\d+)/).pop() <= 534;
    return isNeedShim ? new FormDataShim() : new FormData()
}
/**
* formdata 补丁, 给不支持formdata上传blob的android机打补丁
* @constructor
*/
function FormDataShim() {
    console.warn('using formdata shim');
    var o = this,
        parts = [],
        boundary = Array(21).join('-') + (+new Date() * (1e16 * Math.random())).toString(36),
        oldSend = XMLHttpRequest.prototype.send;
    this.append = function(name, value, filename) {
        parts.push('--' + boundary + '\r\nContent-Disposition: form-data; name="' + name + '"');
        if (value instanceof Blob) {
            parts.push('; filename="' + (filename || 'blob') + '"\r\nContent-Type: ' + value.type + '\r\n\r\n');
            parts.push(value);
        }
        else {
            parts.push('\r\n\r\n' + value);
        }
        parts.push('\r\n');
    };
    // Override XHR send()
    XMLHttpRequest.prototype.send = function(val) {
        var fr,
            data,
            oXHR = this;
        if (val === o) {
            // Append the final boundary string
            parts.push('--' + boundary + '--\r\n');
            // Create the blob
            data = getBlob(parts);
            // Set up and read the blob into an array to be sent
            fr = new FileReader();
            fr.onload = function() {
                oldSend.call(oXHR, fr.result);
            };
            fr.onerror = function(err) {
                throw err;
            };
            fr.readAsArrayBuffer(data);
            // Set the multipart content type and boudary
            this.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);
            XMLHttpRequest.prototype.send = oldSend;
        }
        else {
            oldSend.call(this, val);
        }
    };
}

使用 jQuery,上传formdata的小问题 参考- 关于 HTML5 的文件上传处理,兼容,以及 BLOB 对象的使用

如果使用 jQuery,产生含有数据的 FormData 对象之后,我们可以将其传进$.ajaxdata 参数里面。这种情况,只需要将 formdata 对象传入,并且制定 processDatacontentTypefalse,就可以用 multipart/form-data 的方式通过 ajax post 一个请求出去,当然这里面就可以包含一般的二进制对象(File 或者 Blob),但是实际测试中发现腾讯QQ浏览器在将 Blob 传入 Formdata 中的时候就会出问题,肯定是内核对 FormData 的实现上面有 Bug。
于是为了兼容这个问题,封装一个模拟出来的表单,即由 boundary 分割的multipart/form-data 请求体。

请求体的封装
如果使用 multipart/form-data 的 Content-Type 去提交一个请求,实际上发出的 HTTP 请求是这样的:

// 请求头
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryWwE7y8P3JK82rxsk

// 请求体
------WebKitFormBoundaryWwE7y8P3JK82rxsk
Content-Disposition: form-data; name="username"

admin
------WebKitFormBoundaryWwE7y8P3JK82rxsk
Content-Disposition: form-data; name="avatar"; filename="avatar.png"
Content-Type: image/png

[binary stream]
------WebKitFormBoundaryWwE7y8P3JK82rxsk--

关键的格式就是这样,只要满足这个规范,后台就可以从$_POST$_FILE获取提交的字段或者上传的文件。

// 其中,请求头很简单,首先随机一个 boundary 字符串,然后通过 ajax 的 contentType 参数输入即可:
var makeBoundary = function() {
    return '----JQBoundary'+btoa(Math.random().toString()).substr(0,12);
};

var boundary = makeBoundary();

$.ajax({
    contentType: 'multipart/form-data; boundary='+boundary,
    // ...
});

// 不涉及二进制的传递
$.ajax(url, {
    method: 'post',
    processData: false,
    contentType: 'multipart/form-data; boundary='+boundary,
    data: '--' + boundary + '\r\n' +
        'Content-Disposition: form-data; name="username"\r\n\r\n' + 
        '呆滞的慢板\r\n' +
        '--' + boundary + '--\r\n'
});

由于文本类型(而且还是 unicode 文本)类型与直接的二进制流放在一起,产生了编码混乱,ajax 发出之间,由于这是一个字符串,因此 xhr 对象帮我们自动编码这个字符串,结果造成了二进制流的破坏,后台识别不出来了。可以通过unicode和二进制混编构造的字符串,在传递给 ajax 之前,将其一个一个字节编码到 Uint8Array 中,再获取其 buffer,作为 data 传给 ajax。

/**
 * unicode 字符一个一个拆开
 * Encode a given string to utf8 encoded binary string. 
 * @param str:
 * @returns string:
 */
var str2utf8 = window.TextEncoder ? function(str) {
    var encoder = new TextEncoder('utf8');
    var bytes = encoder.encode(str);
    var result = '';
    for(var i = 0; i < bytes.length; ++i) {
        result += String.fromCharCode(bytes[i]);
    }
    return result;
} : function(str) {
    return eval('\''+encodeURI(str).replace(/%/gm, '\\x')+'\'');
};
/**
 * 其编码成 Uint8Array
 */
var str2Uint8Array = function(str) {
    var arr = [], c;
    for(var i = 0; i < str.length; ++i) {
        c = str.charCodeAt(i);
        if(c > 0xff) {
            alert('Char code range out of 8 bit, parse error!');
            return [];
        }
        arr.push(str.charCodeAt(i));
    }
    return new Uint8Array(arr);
};
// 那么最终我们可以这样来发送一个 ajax,就可以完全兼容二进制流和普通字段了:
var strctured_body = '...';  // 这是我们手工混编出来的,带有 unicode 字符的完整 request body
var encoded_body = str2utf8(structured_body);
var byte_array = str2Uint8Array(encoded_body);
$.ajax(url, {
    method: 'post',
    processData: false,
    contentType: 'multipart/form-data; boundary='+boundary,
    data: byte_array.buffer
});

关于直接上传base64格式的限制。

get 和post的长度限制 参考

  1. HTTP协议规范没有对URL长度进行限制。这个限制是特定的浏览器及服务器对它的限制。IE对URL长度的限制是2083字节(2K+35)。对于其他浏览器,如Netscape、FireFox等,理论上没有长度限制,其限制取决于操作系统的支持。
  2. 理论上讲,POST是没有大小限制的。HTTP协议规范也没有进行大小限制,起限制作用的是服务器的处理程序的处理能力。

在线演示

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容