Canvas 实现图片上传压缩

背景

收集用户上传的图片是一件很常见的需求,然而随着现在设备像素越来越高,照片尺寸通常也越来越大。因此,在前端对用户上传的图片进行压缩变为一个刚需,尤其是在移动设备上,用户通常希望直接选择拍摄图片上传,由此有了本文。原理很简单,主要就是通过canvas读取图片重绘,所以以贴代码为主。
在线demo戳这儿

上代码

/**
* 图片压缩
* @param source {String|File}     传入待压缩图片来源,支持两种方式:
*                                   1直接传入一个网络图片url 
*                                   2传入img file对象
* @param options {Object}   压缩配置,示例如下:
*                           {
*                               width: 100, 设置width固定值,高度随比例缩放
*                               height: 100, 设置height固定值,宽度随比例缩放,同时设置则固定宽高
*                               maxWidth: 100, 设置最大宽度不大于100,高度随比例缩放
*                               maxHeight: 100, 设置最大高度不大于100,宽度随比例缩放
*                               maxSide: 100, 设置最大边不大于100,另外边随比例缩放
*                               ratio: 0.5, 设置尺寸宽高同时缩放到指定比例,取值范围 0-1
*                               //上述参数如果同时设置,优先级由上到下生效
*                               quality: 0.5, 图片输出质量,取值范围0-1
*                               type: 'image/jpeg' 图片输出格式
*                           }
* @param cb {Function}    处理结果的回调,参数为(err, file),第一个参数为出错时的error对象,第二个为处理后的图片file对象
* @return void
*/
function compressImg(source, options, cb) {
  var img = new Image();
  var finalFileName = 'newImage';

  img.onerror = function(err) {
    cb(err);
  };

  img.onload = function() {
    try {
      options = options ? options: {};

      // default width: origin width
      var finalWidth = this.width || 100;
      // default height: origin height
      var finalHeight = this.height || 100;

      // defalut quality: 0.6
      var finalQuality = options.quality || 0.6;

      // default type: 'image/jpeg'
      var finalType = options.type || 'image/jpeg';

      // calculate finalWidth & finalHeight
      if (options.width && options.height) {
        finalWidth = options.width;
        finalHeight = options.height;
      } else if (options.width && !options.height) {
        finalHeight = parseInt(finalHeight * (options.width / finalWidth), 10);
        finalWidth = options.width;
      } else if (options.height && !options.width) {
        finalWidth = parseInt(finalWidth * (options.height / finalHeight), 10);
        finalHeight = options.height;
      } else if (options.maxWidth) {
        if (finalWidth > options.maxWidth) {
          finalHeight = parseInt(finalHeight * (options.maxWidth / finalWidth), 10);
          finalWidth = options.maxWidth;
        }
      } else if (options.maxHeight) {
        if (finalHeight > options.maxHeight) {
          finalWidth = parseInt(finalWidth * (options.maxHeight / finalHeight), 10);
          finalHeight = options.maxHeight;
        }
      } else if (options.maxSide) {
        if (finalHeight >= finalWidth && finalHeight > options.maxSide) {
          finalWidth = parseInt(finalWidth * (options.maxSide / finalHeight), 10);
          finalHeight = options.maxSide;
        } else if (finalWidth > finalHeight && finalWidth > options.maxSide) {
          finalHeight = parseInt(finalHeight * (options.maxSide / finalWidth), 10);
          finalWidth = options.maxSide;
        }
      } else if (options.ratio) {
        finalWidth = parseInt(finalWidth * options.ratio, 10);
        finalHeight = parseInt(finalHeight * options.ratio, 10);
      }

      var canvas = document.createElement('canvas');
      canvas.width = finalWidth;
      canvas.height = finalHeight;
      var context = canvas.getContext('2d');
      context.clearRect(0, 0, finalWidth, finalHeight);
      context.drawImage(this, 0, 0, finalWidth, finalHeight);
      // 可以直接用canvas.toBlob,但是移动端兼容性一般
      var newBlob = dataURItoBlob(canvas.toDataURL(finalType, finalQuality), finalType);
      // 这一行ios设备可能会有兼容性问题(安卓OK),建议直接返回blob对象,不要调用new file生成新的file 对象
      var newFile = new File([newBlob], finalFileName, {
        type: finalType
      });
      console.log('After compress:' + newFile.size / 1024 + 'kb');
      cb(null, newFile);
    } catch(err) {
      cb(err);
    }

  };

  if (typeof source === 'string') {
    img.setAttribute('crossOrigin', 'anonymous');
    img.src = source;
    finalFileName = source;
  } else if (typeof source === 'object' && source.toString().indexOf('File') >= 0) {
    console.log('Before compress:' + source.size / 1024 + 'kb');
    var reader = new FileReader();
    reader.readAsDataURL(source);
    reader.onload = function(e) {
      img.src = e.target && e.target.result;
      if (source.name) {
        finalFileName = source.name;
      }
    };
  } else {
    cb(new Error('Error image source context'));
  }
};

function dataURItoBlob(dataURI, type) {
  var img = dataURI.split(',')[1];
  var decode = window.atob(img);
  /* eslint-disable */
  var ab = new ArrayBuffer(decode.length);
  var ib = new Uint8Array(ab);
  /* eslint-enable */
  for (var i = 0; i < decode.length; i++) {
    ib[i] = decode.charCodeAt(i);
  }
  return new Blob([ib], {
    type: type
  });
}

使用方法无比详细,就不多解释了。这里多说一点兼容性相关的问题。这个工具方法的callback返回的是一个新new的file对象,然而由于ios上存在的一些兼容性问题,方法最后返回的file对象在ios上大概率为空或者有各种问题,因此如果你是在移动端ios上使用此工具方法,建议修改一下,直接返回blob即可。另外,dataURLtoBlob方法其实有canvas的原生支持canvas.toBlob,只是兼容性较差,可以酌情考虑使用。
下面上几个使用的例子:

<input type="file" id="test">
//Test upload
var input = document.querySelector('#test');
input.addEventListener('change', function(e) {
  var file = e.target.files[0];
  compressImg(file, {
    quality: 0.6,
    type: 'image/jpeg'
  },
  function(err, blob) {
    if (err) {
      console.log(err);
    } else {
      var url = URL.createObjectURL(blob);
      console.log(url);
    }
  });
});

//Test web url
compressImg("https://img.yzcdn.cn/upload_files/2017/11/27/FuBfCauOKWsx6aWKCEqRkB3RKXu7.png", {
    quality: 0.6,
    type: 'image/jpeg'
  },
  function(err, blob) {
    if (err) {
      console.log(err);
    } else {
      var url = URL.createObjectURL(blob);
      console.log(url);
    }
  });

The end

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

推荐阅读更多精彩内容