美文网首页
图片压缩上传

图片压缩上传

作者: 不要变成发抖的小喵喵喵喵喵喵 | 来源:发表于2017-07-04 23:41 被阅读0次

    参考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协议规范也没有进行大小限制,起限制作用的是服务器的处理程序的处理能力。

    在线演示

    相关文章

      网友评论

          本文标题:图片压缩上传

          本文链接:https://www.haomeiwen.com/subject/rdfxhxtx.html