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

发布于 2022-11-25 12:48:49 字数 14172 浏览 166 评论 0

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

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

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

文中用到的一些 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.com/exif-js/exi…


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

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

由于实际项目中,每个项目的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 仓库地址

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

复古式

暂无简介

文章
评论
668 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文