手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件

前言

今天咱们要讲解下如何编写一个图片压缩、方向纠正插件,附带着会讲解下如何上传和预览。
html

为何重点放在图片压缩和方向纠正?
前端

相信你们在作项目过程当中,常常会遇到上传图片到后端,可是因为图片过大,须要对图片压缩处理。特别在移动端,手机拍的照片广泛过于大了,咱们有时候只是须要上传一张头像,很小就够用了。还有在部分手机上(已知苹果手机)拍的照片存在方向角度问题,这时就须要咱们来纠正图片角度了。
vue

不少同窗多数时候是在用别人写好的图片压缩上传插件。针对咱们的需求,这些插件有时候不能达到咱们最理想的效果,本身写呢,又不会写,非常头疼。今天就深刻剖析讲解下,教会你们编写本身的图片压缩、方向纠正插件,以及预览和上传压缩后图片数据。react

文中用到的一些H5的api和EXIF.js等知识点若是不懂的话,请先阅读文末尾的结语中的知识点资料。git


实现原理

压缩图片而且上传主要用到filereader、canvas 以及 formdata 这三个h5的api和插件EXIF.js。逻辑并不难。整个过程就是:github

  (1)用户使用input file上传图片的时候,用filereader读取用户上传的图片数据(base64格式)ajax

  (2)把图片数据传入img对象,而后将img绘制到canvas上,用EXIF.js对图片方向进行纠正,再调用canvas.toDataURL对图片进行压缩,获取到压缩后的base64格式图片数据,转成二进制canvas

  (3)获取到压缩后的图片二进制数据,预览。后端

  (4)将压缩后的图片二进制数据塞入formdata,再经过XmlHttpRequest提交formdataapi

  如此四步,就完成了图片的压缩、方向纠正、预览和上传。


插件设计思考

考虑到在实际项目中,可能用不一样的开发框架(vue.js/JQ/react.js/angular.js/anu.js等),图片预览的UI样式也可能不一样,图片数据上传方法可能不一样。由于图片压缩和方向纠正这两块的逻辑多变性比较低,咱们这里把图片压缩和方向纠正抽离出来,封装为一个插件库。


【一】获取图片数据

先是获取图片数据,也就是监听input file的change事件,而后获取到用来压缩上传的文件对象files,将files传到【图片压缩、方向纠正插件】中进行处理。

这时候根据每一个人的需求,也能够预览未压缩的图片。

//监听上传组件input的onchange事件,压缩图片,纠正图片方向,同时获取压缩后的图片
filechooser.onchange = function () {
    var fileList = this.files;
    
    //预览压缩前的图片
    var files = Array.prototype.slice.call(fileList);
    files.forEach(function (file, i) {
        var reader = new FileReader();
        reader.onload = function () {
            var li = document.createElement("li")
            li.style.backgroundImage = 'url('+this.result+')';
            document.querySelector('.img_list').appendChild(li)
        }
        reader.readAsDataURL(file);
    });

    //处理图片列表,getCompressiveFileList接受处理后的图片数据列表
    //下面两行代码为图片压缩、方向纠正插件的用法,具体实现细节请继续往下阅读 ~_~ ↓↓↓
    var process = window.lzImgProcess();
    process(fileList, getCompressiveFileList);
}
复制代码

【二】图片压缩、方向纠正插件实现

上面作完图片数据的获取后,就能够作process压缩图片的方法了。而压缩图片也并非直接把图片绘制到canvas再调用一下toDataURL就行的。

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

  首先是图片的大小,若是图片的大小超过两百万像素,图片也是没法绘制到canvas上的,调用drawImage的时候不会报错,可是你用toDataURL获取图片数据的时候获取到的是空的图片数据。

  再者就是canvas的大小有限制,若是canvas的大小大于大概五百万像素(即宽高乘积)的时候,不只图片画不出来,其余什么东西也都是画不出来的。

  应对上面两种限制,我把图片宽度、高度压缩控制在1000px之内,这样图片最大就不超过两百万像素了。在前端开发中,1000px*1000px基本能够知足绝大部分的需求了。固然了还有更完美的瓦片式绘制的方法,咱们这里就说瓦片式绘制方法了。

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

  除了上面所述的限制,还有两个坑,一个就是canvas的toDataURL是只能压缩jpg的(这句话的详细解释能够看下面的Tip讲解),当用户上传的图片是png的话,就须要转成jpg,也就是统一用canvas.toDataURL('image/jpeg', 0.5) , 类型统一设成jpeg,而压缩比就本身控制了。

  另外一个就是若是是png转jpg,绘制到canvas上的时候,canvas存在透明区域的话,当转成jpg的时候透明区域会变成黑色,由于canvas的透明像素默认为rgba(0,0,0,0),因此转成jpg就变成rgba(0,0,0,1)了,也就是透明背景会变成了黑色。解决办法就是绘制以前在canvas上铺一层白色的底色。

  在压缩图片以前,咱们判断图片角度,若是图片角度不正确,还须要用EXIF.js把图片角度纠正过来。

  压缩完图片,把base64的图片数据转成二进制数据存储到暂存区中,等待被getBlobList获取使用。

Tip: canvas的toDataURL是只能压缩jpg这句话我可能说的不清楚,我想表达的意思是这个api不管是jpeg仍是png,最后导出的时候,都跟jpeg没啥差异了。由于png图片的透明性质在canvas中是无效的,会被canvas添加默认的黑色背景,我在文中讲解的时候,用白色背景处理了,因此最后导出的图片,不管你设置的是png仍是jpeg都跟jpeg没啥区别了,由于没法保持png的透明度性质了

(function(window) {

    /** * * 做者:混沌传奇 * * 邮箱地址:iot-pro_lizeng@foxmail.com * * 日期:2017-10-26 * * 插件功能:压缩图片&&纠正图片方向&&返回二进制(Blob)图片元数据组成的列表 * */

    window.lzImgProcess = function () {


        var Orientation = '', //图片方向角
            blobList = [], //压缩后的二进制图片数据列表

            canvas = document.createElement("canvas"), //用于压缩图片(纠正图片方向)的canvas
            ctx = canvas.getContext('2d'),

            file_type = 'image/jpeg', //图片类型
            qlty = 0.5, //图片压缩品质,默认是0.5,可选范围是0-1的数字类型的值,可配置
            imgWH = 1000; //压缩后的图片的最大宽度和高度,默认是1000px,可配置


        /** * @actionName process, * 方法功能:压缩图片&&纠正图片方向&&返回二进制(Blob)图片元数据 * * @param fileList,传入函数的文件列表对象,fileList对象是来自用户在一个<input>元素上选择文件后返回的FileList对象 * 注意:图片类型必须是jpeg||png * 好比:<input id="uploadImage" onchange="loadImageFile();" /> * function loadImageFile() { * //获取返回的fileList对象 * var fileList = document.getElementById("uploadImage").files; * } * @param getBlobList [Blob],获取压缩结果的钩子函数,接受一个参数。 * 功能:在图片压缩完毕后,获取压缩后的二进制图片数据对象组成的数组,参数即:压缩后的二进制图片数据(blob)组成的list * * @param quality,传入函数的图片压缩比率(品质),可选范围0-1的数字类型的值,默认是0.5 * * @param WH,传入函数的图片压缩后的最大图片宽度和高度,默认是1000,单位是px,可自由配置。 * 注意:最好不要超过1000,数字过大,容易致使canvas压缩失败。因为没作瓦片处理,因此有这个限制。1000*1000的图片在前端中,基本也够用了。 * */
        function process (fileList, getBlobList, quality, WH) {
            blobList = []; //初始化blobList
            // 判断参数fileList的长度是否大于0
            if (!fileList.length){
                console.log('警告:传进方法process的参数fileList长度必须大于零!!!')
                return;
            }

            //若是quality参数有值,则把quality赋值给qlty(图片压缩的品质)
            if(quality)
                qlty = quality;

            //若是WH参数有值,则把WH赋值给imgWH(压缩后的图片的最大宽度和高度)
            if(WH&&WH<1000&&WH>0){
                imgWH = WH;
            }

            // 把传进来的fileList转为数组类型
            var files = Array.prototype.slice.call(fileList);
            
            files.forEach(function (file, i) {
                if (!/\/(?:jpeg|png)/i.test(file.type)){
                    console.log('警告:图片必须是jpeg||png类型!!!');
                    return;
                }
                // file_type = file.type;
                
                var reader = new FileReader();

                // 获取图片压缩前大小,打印图片压缩前大小
                var size = file.size/1024 > 1024 ? (~~(10*file.size/1024/1024))/10 + "MB" :  ~~(file.size/1024) + "KB";
                // console.log('size:', size)

                reader.onload = function () {
                    var img = new Image();
                    img.src = this.result;

                    // 图片加载完毕以后进行压缩
                    if (img.complete) {
                        callback();
                    } else {
                        img.onload = callback;
                    }

                    function callback() {
                        //获取照片方向角属性,用户旋转控制 
                        EXIF.getData(img, function() {
                            // alert(EXIF.pretty(this));
                            EXIF.getAllTags(this);   
                            // alert(EXIF.getTag(this, 'Orientation'));
                            Orientation = EXIF.getTag(this, 'Orientation'); 
							console.log('Orientation:', Orientation)
                            if(Orientation == ""||Orientation == undefined||Orientation == null){
                                Orientation = 1;
                            }
                        });

                        //获取压缩后的图片二进制数据
                        var data = GetImgCompress(img);

                        //将二进制数据塞入到二进制数据列表中
                        blobList.push(data);

                        //将压缩后的二进制图片数据对象(blob)组成的list经过钩子函数返回出去
                        if(blobList.length===files.length){
                            if(getBlobList)
                                getBlobList(blobList);
                        }
                        
                        img = null;
                    }

                };

                reader.readAsDataURL(file);
            })
        }


        /** * @actionName GetImgCompress, * 功能:判断上传图片的方向,若是不是正确的,进行修正,并对图片进行压缩,压缩完后,返回压缩后的二进制图片数据 * * @param img, 用来压缩的图片对象 * * @returns 返回的压缩后的二进制图片数据 */
        function GetImgCompress(img){
            //若是方向角不为1,都须要进行旋转
            if(Orientation != 1){
                switch(Orientation){
                    case 6://须要顺时针90度旋转
                        rotateImg(img,'right',canvas);
                        break;
                    case 8://须要逆时针90度旋转
                        rotateImg(img,'left',canvas);
                        break;
                    case 3://须要180度旋转
                        rotateImg(img,'right2',canvas);//转两次
                        break;
                }
            }else{
                //不作旋转
                rotateImg(img,'no',canvas);
            }

            var ndata;
            
            ndata = canvas.toDataURL(file_type, qlty);
            
            //打印压缩先后的大小,以及压缩比率
            // var initSize = img.src.length;
            // console.log('压缩前:' + initSize);
            // console.log('压缩后:' + ndata.length, 'base64数据', ndata);
            // console.log('压缩率:' + ~~(100 * (initSize - ndata.length) / initSize) + "%");


            //将压缩后的base64数据转为二进制数据
            ndata = dataURItoBlob(ndata);

            //清除canvas画布的宽高
            canvas.width = canvas.height = 0;
            
            return ndata;
        }



        /** * @actionName rotateImg, * 功能:对图片旋转处理 * * @param img, 用来矫正方向的图片对象 * * @param direction, 旋转方向 * * @param canvas, 用来绘制图片的cavas画布对象 */
        function rotateImg(img, direction,canvas) {    
            
            //最小与最大旋转方向,图片旋转4次后回到原方向 
            var min_step = 0;
            var max_step = 3;
            if (img == null)return;    
            //img的高度和宽度不能在img元素隐藏后获取,不然会出错 
            var height = img.height;    
            var width = img.width;

            if(width>imgWH || height>imgWH){
                var ratio = ~~(height/width*10)/10;
                if(width>height){
                    width = imgWH;
                    height = imgWH*ratio;
                }else{
                    height = imgWH;
                    width = height/ratio;
                }
                img.width = width;
                img.height = height;
            }

            var step = 2;    
            if (step == null) {    
                step = min_step;    
            }
            if (direction == 'no'){
                step = 0;    
            } else if (direction == 'left') {    
                step++;    
                //旋转到原位置,即超过最大值 
                step > max_step && (step = min_step);    
            } else if (direction == 'right') {			
				step--;    
                step < min_step && (step = max_step);    
			} else {    
                //旋转180度
            }

            //旋转角度以弧度值为参数 
            var degree = step * 90 * Math.PI / 180; 

            switch (step) {
                case 0:
                    canvas.width = width;    
                    canvas.height = height;   

                    // 铺底色
                    ctx.fillStyle = "#fff";
                    ctx.fillRect(0, 0, width, height);
                    ctx.drawImage(img, 0, 0, width, height);   
                    break;    
                case 1:
                    canvas.width = height;    
                    canvas.height = width;   

                    // 铺底色
                    ctx.fillStyle = "#fff";
                    ctx.fillRect(0, 0, height, width);
                    ctx.rotate(degree);    
                    ctx.drawImage(img, 0, -height, width, height);    
                    break;
                case 2:
                    canvas.width = width;    
                    canvas.height = height;   

                    // 铺底色
                    ctx.fillStyle = "#fff";
                    ctx.fillRect(0, 0, width, height);
                    ctx.rotate(degree);    
                    ctx.drawImage(img, -width, -height, width, height);    
                    break;    
                case 3:
                    canvas.width = height;    
                    canvas.height = width;   

                    // 铺底色
                    ctx.fillStyle = "#fff";
                    ctx.fillRect(0, 0, height, width);
                    ctx.rotate(degree);    
                    ctx.drawImage(img, -width, 0, width, height);    
                    break;
            }
        }




        /** * dataURL to blob, ref to https://gist.github.com/fupslot/5015897 * @param dataURI,图片的base64格式数据 * @returns {Blob} */
        function dataURItoBlob(dataURI) {
            var byteString = atob(dataURI.split(',')[1]);
            var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
            var ab = new ArrayBuffer(byteString.length);
            var ia = new Uint8Array(ab);
            for (var i = 0; i < byteString.length; i++) {
                ia[i] = byteString.charCodeAt(i);
            }
            return new Blob([ab], {type: mimeString});
        }




        /** * 返回一个process方法 * * process方法:用来压缩图片数据,在压缩图片的同时,默认会调用correctOrientation方法纠正图片方向。 * */
        return process;


    }
})(window)
复制代码

Exif.js 提供了 JavaScript 读取图像的原始数据的功能扩展,例如:拍照方向、相机设备型号、拍摄时间、ISO 感光度、GPS 地理位置等数据。
Exif.js官方github 仓库地址:github.com/exif-js/exi…


【三】获取压缩后的图片二进制数据,预览图片

完成图片压缩后,就能够获取压缩后的图片二进制数据了,把获取到的图片二进制数据存起来;获取到数据后,能够拿来预览。

因为实际项目中,每一个项目的UI样式设计可能不同,开发者能够根据本身的UI样式来预览图片。

//获取压缩后的图片
function getCompressiveFileList(fileList) {
    blobFileList = fileList;
    // console.log('fileBlobList:', fileList);
    fileList.forEach(function (blob) {
        var reader = new FileReader();
        reader.onload = function () {
            var li = document.createElement("LI")
            li.style.backgroundImage = 'url('+this.result+')';
            document.querySelector('.imgCompress_list').appendChild(li)
        }
        reader.readAsDataURL(blob);
    })
}
复制代码

【四】提交图片数据到后台

new一个formdata对象,将上一步获取到的 blobFileList 图片二进制数据 append 到 formdata中,用任意你喜欢的ajax库进行上传。固然也能够用原生ajax上传。

//将压缩后的二进制图片数据流append到formdata对象中上传到后台服务器
//注意:上传的是formdata对象,后台接口接收的时候,也要从formdata对象中读取二进制数据流
function formUpData(blobFiles){
    var formData = new FormData();

    formData.append("files", blobFiles);
    
    var xhr = new XMLHttpRequest();
    
    //连接你本身上传图片接口便可,这里的接口地址,是我写的示例,不可真实使用,讲解意义更大
    xhr.open('post', 'http://xxx/welcome/index/');

    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4 && xhr.status == 200) {
                console.log('上传成功!');
        }
    };

    xhr.send(formData);
}
复制代码

结语

完整代码以及demo:git仓库地址

文中用到了一些H5的知识点,不懂的童鞋能够进入下面的知识点资料地址,详细阅读
HTMLCanvasElement.toDataURL()
CanvasRenderingContext2D.drawImage()
FileReader()
FormData
Blob()
EXIF.js中文博客文章

喜欢这篇文章的话,请关注我,我会持续更新技术文章到个人掘金主页。谢谢支持~
喜欢交流前端技术的朋友能够加个人QQ群:583575104 或 366420656
相关文章
相关标签/搜索