移动端图片压缩及角度修正

我们使用uploader组件来进行测试,用ios手机去拍照,手机竖着去拍照时,得到的图片会逆时针旋转90度,横着拍没问题。下面图中的左一即为横拍,右边的两个竖拍就很明显不对劲了,手机上我们拍的照片所呈现的都是竖屏照片,上传后就变成横屏的了,这无疑给用户增加了不适感,不合适,那我们需要针对这去改一下。

解决思路:获取到照片拍摄的方向角度,然后使用canvas去进行修正


image.png

随着这个思路,我们需要了解一下EXIF这个概念。EXIF可交换图像文件格式(英语:Exchangeable image file format,官方简称Exif),是专门为数码相机的照片设定的,可以记录数码照片的属性信息和拍摄数据。

google了一下,有个exif.js可以让我们轻松的取到图片的Orientation,即为照片的拍摄方向,它的值为1-8,默认竖拍为1。

//获取照片方向角属性,用户旋转控制
  EXIF.getData(file, function() {
    // alert(EXIF.pretty(this));
    EXIF.getAllTags(this); 
    //alert(EXIF.getTag(this, 'Orientation')); 
    Orientation = EXIF.getTag(this, 'Orientation');
    console.log(Orientation, '===')
  });
orientation值 旋转角度
1
3 180°
6 顺时针90°
8 逆时针90°

为了解决一个获取Orientation值问题去引入一个js库,不太值得。weui.js中也有个upload组件,并且它单独处理了image的这种旋转,提供了方法。

function getOrientation(buffer){
    var view = new DataView(buffer); // buffer是图片字节码流
    // 每一个JPEG文件的内容都开始于一个二进制的值 '0xFFD8', 并结束于二进制值'0xFFD9', 是个标记
    // 标记的格式  0xFF+标记号(1个字节)+数据大小描述符(2个字节)+数据内容(n个字节)
    if (view.getUint16(0, false) != 0xFFD8) return -2; 
    var length = view.byteLength, offset = 2;
    while (offset < length) {
        var marker = view.getUint16(offset, false);
        offset += 2;
      // Exif 使用 APP1(0xFFE1)标记来避免与JFIF格式的 冲突. 且每一个 Exif 文件格式都开始于它
      // 图片的数据域就存在APP1中
        if (marker == 0xFFE1) {
            if (view.getUint32(offset += 2, false) != 0x45786966) return -1;
            var little = view.getUint16(offset += 6, false) == 0x4949;
            offset += view.getUint32(offset + 4, little);
            var tags = view.getUint16(offset, little);
            offset += 2;
            for (var i = 0; i < tags; i++)
              // Orientation 存在 0x0112 中
                if (view.getUint16(offset + (i * 12), little) == 0x0112)
                    return view.getUint16(offset + (i * 12) + 8, little);
        }
        else if ((marker & 0xFF00) != 0xFF00) break;
        else offset += view.getUint16(offset, false);
    }
    return -1;
}

看懂这段代码是很难的,如果不知道exif内容的话。Exif 信息就是由数码相机在拍摄过程中采集一系列的信息,然后把信息放置在我们熟知的 JPEG/TIFF 文件的头部,也就是说 Exif信息是镶嵌在 JPEG/TIFF 图像文件格式内的一组拍摄参数,它就好像是傻瓜相机的日期打印功能一样,只不过 Exif信息所记录的资讯更为详尽和完备。Exif 所记录的元数据信息非常丰富,主要包含了以下几类信息:

  • 拍摄日期
  • 摄器材(机身、镜头、闪光灯等)
  • 拍摄参数(快门速度、光圈F值、ISO速度、焦距、测光模式等
  • 图像处理参数(锐化、对比度、饱和度、白平衡等)
  • 图像描述及版权信息
  • GPS定位数据
  • 缩略图

这里面就包含了图片的角度信息,就是说你用手机拍照时是不是倒着拍还是侧着拍,这些都是有记录的。下图就是exif所携带的照片信息,而我们所关注的角度就在0x0112。

image.png
// base64转arrayBuffer字节码 
function dataURItoBuffer(dataURI){
    var byteString = atob(dataURI.split(',')[1]);
    var buffer = new ArrayBuffer(byteString.length);
    var view = new Uint8Array(buffer);
    for (var i = 0; i < byteString.length; i++) {
        view[i] = byteString.charCodeAt(i);
    }
    return buffer;
}

最重要的图片旋转在这里,结合上面我们所了解的exif信息,对canvas绘制的image进行相应的旋转调整。

function orientationHelper(canvas, ctx, orientation) {
    const w = canvas.width, h = canvas.height;
    if(orientation > 4){
        canvas.width = h;
        canvas.height = w;
    }
    switch (orientation) {
        case 2:
            ctx.translate(w, 0);
            ctx.scale(-1, 1);
            break;
        case 3:
            ctx.translate(w, h);
            ctx.rotate(Math.PI);
            break;
        case 4:
            ctx.translate(0, h);
            ctx.scale(1, -1);
            break;
        case 5:
            ctx.rotate(0.5 * Math.PI);
            ctx.scale(1, -1);
            break;
        case 6:
            ctx.rotate(0.5 * Math.PI);
            ctx.translate(0, -h);
            break;
        case 7:
            ctx.rotate(0.5 * Math.PI);
            ctx.translate(w, -h);
            ctx.scale(-1, 1);
            break;
        case 8:
            ctx.rotate(-0.5 * Math.PI);
            ctx.translate(-w, 0);
            break;
    }
}

到这,图片角度修正是完成了。我们还需要对图片进行压缩,因为现在移动端手机相机像素高,随手一拍动辄4-5m,可能会对服务器造成极大压力。不仅如此,移动端input 选取文件然后渲染成图片,通常这种都是将获取到的文件流转成base64,可能一个文件是1m,转成base64就变成了4m甚至更多,这对移动端渲染也是个性能消耗。

因此前端还需要对图片进行一些压缩处理,压缩图片也并不是直接把图片绘制到canvas再调用一下toDataURL就行的,我们需要考虑一些方面的因素。

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

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

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

应对第一种限制,处理办法就是瓦片绘制了。瓦片绘制,也就是将图片分割成多块绘制到canvas上,我代码里的做法是把图片分割成100万像素一块的大小,再绘制到canvas上。

而应对第二种限制,我的处理办法是对图片的宽高进行适当压缩,我代码里为了保险起见,设的上限是四百万像素,如果图片大于四百万像素就压缩到小于四百万像素。四百万像素的图片应该够了,算起来宽高都有2000X2000了。

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

除了上面所述的限制,还有两个坑,一个就是canvastoDataURL是只能压缩jpg的,当用户上传的图片是png的话,就需要转成jpg,也就是统一用canvas.toDataURL('image/jpeg', 0.1), 类型统一设成jpeg,而压缩比就自己控制了。

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

function compressImage (img, quatity) {
  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
  var canvas = document.createElement("canvas");
  var ctx = canvas.getContext('2d');
  // 瓦片canvas
  var tCanvas = document.createElement("canvas");
  var tctx = tCanvas.getContext("2d");

  canvas.width = width;
  canvas.height = height;

  // 铺底色
  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++) {
        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);
  }

  //进行最小压缩
  var ndata = canvas.toDataURL("image/jpeg", quatity);

  console.log("压缩前:" + initSize);
  console.log("压缩后:" + ndata.length);
  console.log("压缩率:" + ~~(100 * (initSize - ndata.length) / initSize) + "%");

  tCanvas.width = tCanvas.height = canvas.width = canvas.height = 0;

  return ndata;

}

weui采用了另外的处理方式,看起来更加简洁优雅。

/**
 * 压缩图片
 */
function compress(file, options, callback) {
    const reader = new FileReader();
    reader.onload = function (evt) {
        // 启用压缩
        const img = new Image();
        img.onload = function () {
            // 拍照在IOS7或以下的机型会出现照片被压扁的bug
            const ratio = detectVerticalSquash(img); 
            // 获取拍摄角度
            const orientation = getOrientation(dataURItoBuffer(img.src));
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
                        // 可配置宽高压缩
            const maxW = options.compress.width;
            const maxH = options.compress.height;
            let w = img.width;
            let h = img.height;
            let dataURL;

            if(w < h && h > maxH){
                w = parseInt(maxH * img.width / img.height);
                h = maxH;
            }else if(w >= h && w > maxW){
                h = parseInt(maxW * img.height / img.width);
                w = maxW;
            }

            canvas.width = w;
            canvas.height = h;

            if(orientation > 0){
                // 对图片进行角度修正
                orientationHelper(canvas, ctx, orientation);
            }
            ctx.drawImage(img, 0, 0, w, h / ratio);

          // 源码只转jpeg,jpg,我加了png的转化
            if(/image\/(jpeg|jpg|png)/i.test(file.type)){
                dataURL = canvas.toDataURL('image/jpeg', options.compress.quality);
            }else{
                dataURL =  canvas.toDataURL(file.type);
            }

            if(options.type == 'file'){
                if(/;base64,null/.test(dataURL) || /;base64,$/.test(dataURL)){
                    // 压缩出错,以文件方式上传的,采用原文件上传
                    console.warn('Compress fail, dataURL is ' + dataURL + '. Next will use origin file to upload.');
                    callback(file);
                }else{
                    let blob = dataURItoBlob(dataURL);
                    blob.id = file.id;
                    blob.name = file.name;
                    blob.lastModified = file.lastModified;
                    blob.lastModifiedDate = file.lastModifiedDate;
                    callback(blob);
                }
            }else{
                if(/;base64,null/.test(dataURL) || /;base64,$/.test(dataURL)){
                    // 压缩失败,以base64上传的,直接报错不上传
                    options.onError(file, new Error('Compress fail, dataURL is ' + dataURL + '.'));
                    callback();
                }else{
                    file.base64 = dataURL;
                    callback(file);
                }
            }
        };
        img.src = evt.target.result;
    };
    reader.readAsDataURL(file);
}

给文件处理添加了压缩,角度修正之后,上传所得到的图片就如下图一样正常了。

image.png

相关知识链接:

weui image处理

exif.js 在线测试

图片的Exif信息

推荐阅读更多精彩内容