您当前的位置:首页 > 计算机 > 编程开发 > Html+Div+Css(前端)

前端图片压缩、方向纠正、预览、上传插件

时间:12-14来源:作者:点击数:

今天我们要讲解下如何编写一个图片压缩、方向纠正插件,附带着会讲解下如何上传和预览。为什么重点放在图片压缩和方向纠正?

相信大家在做项目过程中,经常会遇到上传图片到后端,但是由于图片过大,需要对图片压缩处理。特别在移动端,手机拍的照片普遍过于大了,我们有时候只是需要上传一张头像,很小就够用了。还有在部分手机上(已知苹果手机)拍的照片存在方向角度问题,这时就需要我们来纠正图片角度了。

很多同学多数时候是在用别人写好的图片压缩上传插件。针对我们的需求,这些插件有时候不能达到我们最理想的效果,自己写呢,又不会写,很是头疼。今天就深入剖析讲解下,教会大家编写自己的图片压缩、方向纠正插件,以及预览和上传压缩后图片数据。

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


实现原理

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

  1. 用户使用 input file 上传图片的时候,用 filereader 读取用户上传的图片数据(base64 格式)
  2. 把图片数据传入 img 对象,然后将 img 绘制到 canvas 上,用 EXIF.js 对图片方向进行纠正,再调用 canvas.toDataURL 对图片进行压缩,获取到压缩后的base64格式图片数据,转成二进制
  3. 获取到压缩后的图片二进制数据,预览。
  4. 将压缩后的图片二进制数据塞入 formdata,再通过 XmlHttpRequest 提交 formdata

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


插件设计思考

考虑到在实际项目中,可能用不同的开发框架(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 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的图片在前端中,基本也够用了。
     *    
     */functionprocess (fileList, getBlobList, quality, WH) {
      blobList = []; //初始化blobList// 判断参数fileList的长度是否大于0if (!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;
          }

          functioncallback() {
            //获取照片方向角属性,用户旋转控制  
            EXIF.getData(img, function() {
              // alert(EXIF.pretty(this));
              EXIF.getAllTags(this);   
              // alert(EXIF.getTag(this, 'Orientation'));
              Orientation = EXIF.getTag(this, '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 返回的压缩后的二进制图片数据
     */functionGetImgCompress(img){
      if (navigator.userAgent.match(/iphone/i)) {
        //console.log('iphone');//如果方向角不为1,都需要进行旋转if(Orientation != "" && Orientation != 1){
          switch(Orientation){
            case6://需要顺时针(向左)90度旋转
              rotateImg(img,'left',canvas);
              break;
            case8://需要逆时针(向右)90度旋转
              rotateImg(img,'right',canvas);  
              break;  
            case3://需要180度旋转
              rotateImg(img,'right',canvas);//转两次  
              rotateImg(img,'right',canvas);
              break;  
          }
        }else{
          //不做旋转
          rotateImg(img,'no',canvas); 
        }
      }elseif (navigator.userAgent.match(/Android/i)) {// 修复androidif(Orientation != "" && Orientation != 1){
            switch(Orientation){
              case6://需要顺时针(向左)90度旋转
                rotateImg(img,'left',canvas);
                break;
              case8://需要逆时针(向右)90度旋转
                rotateImg(img,'right',canvas);
                break;
              case3://需要180度旋转
                rotateImg(img,'right',canvas);//转两次
                rotateImg(img,'right',canvas);
                break;
            }
          }else{
            //不做旋转
            rotateImg(img,'no',canvas);  
          }
      }else{
        if(Orientation != "" && Orientation != 1){
          switch(Orientation){
            case6://需要顺时针(向左)90度旋转
              rotateImg(img,'left',canvas);
              break;
            case8://需要逆时针(向右)90度旋转
              rotateImg(img,'right',canvas);
              break;
            case3://需要180度旋转
              rotateImg(img,'right',canvas);//转两次
              rotateImg(img,'right',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画布对象
     */functionrotateImg(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;
      }

      canvas.width = width;  
      canvas.height = height;   
      
      // 铺底色
      ctx.fillStyle = "#fff";
      ctx.fillRect(0, 0, width, height);

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

      //旋转角度以弧度值为参数  var degree = step * 90 * Math.PI / 180; 
      
      switch (step) {
        case0:
          ctx.drawImage(img, 0, 0,width,height);   
          break;  
        case1:
          ctx.rotate(degree);  
          ctx.drawImage(img, 0, -height,width,height);  
          break;
        case2:
          ctx.rotate(degree);  
          ctx.drawImage(img, -width, -height,width,height);  
          break;  
        case3:
          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}
     */functiondataURItoBlob(dataURI) {
      var byteString = atob(dataURI.split(',')[1]);
      var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
      var ab = newArrayBuffer(byteString.length);
      var ia = newUint8Array(ab);
      for (var i = 0; i < byteString.length; i++) {
        ia[i] = byteString.charCodeAt(i);
      }
      returnnew Blob([ab], {type: mimeString});
    }




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


  }
})(window)

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

Exif.js官方github 仓库地址: github /exif-js/exif-js/>


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

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

由于实际项目中,每个项目的UI样式设计可能不一样,开发者可以根据自己的UI样式来预览图片。

//获取压缩后的图片functiongetCompressiveFileList(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对象中读取二进制数据流functionformUpData(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 仓库地址( github /legend-li/img_compress_rotate_preview_upload)

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐