背景

最近在做一个需求是对页面某一块区域截图,本身没什么,但是发现需要截图的区域非常大,然后里面的内容通常只有一小点,这样我把整张图片拿出去就会发现只有中间一点点有东西,其他地方都是空白的,用户体验就会比较差了,记得PhotoShop里面有一个功能是能截取整张图片的最小有效像素区域,本来以为canvas自带有这种功能,没想到还是要自己实现的,虽然功能很简单,但是网上也没找着相应类库,可能是我搜索的姿势不对,或者这个需求实在太简单了,没人封装成类库了,虽然完全没做过图像处理,也开始找一些资料。

文字描述比较抽象,放上一张图片。

原始图片

处理后图片

canvas

网页内容是用Html2Canvas把DOM截图成了canvas,原理不是很清楚,大概是读取DOM样式然后在canvas里面重新画一遍,这不是重点,这个时候我们已经拿到画成了的canvas了,改怎么处理呢?

1
var canvas  = document.getElementById( 'canvas' );
var ctx     = canvas.getContext( '2d' );
var imgData = ctx.getImageData( 0, 0, canvas.width, canvas.height );

通过getImageData方法就能够拿到canvas的原始像素点信息了,四个参数描述了一个矩形,返回的像素点信息就是这个矩形内的信息。

返回的是一个Uint8ClampedArrayUint8ClampedArrayJavaScript中的类型数组,具体的可以参看这里,简单来看就是这个数组中的元素都是unsigned int并且都是8位的,所以取值范围是0~255,非常适合用来存放ImageData,而且类型数组因为类型都是确定的,使用起来比普通数组速度上会快很多。

ImageData

拿到了原始的像素点数据,来看看数据长什么样子,首先看数组长度,假设是一个100X100的图片,那么理论上像素点的数量是100100,也就是10000个,拿到ImageData的数组看长度其实有40000,简单猜测一下大概是10000 rgba,看具体的数组内容很容易就能够验证我们的猜想。

一个典型的ImageData大概长这个样子,这是一个从左往右,从上到下按顺序排列的一个数组:

1
imageData = [ 
              0,   0,   0,   0,
              255, 255, 255, 1,
              122, 122, 122, 0.5,
              ....
            ];

如何获取最小像素区域?

要实现这个功能的本质,其实就是如何寻找最上、最右、最下以及最左四个点(P1,P2,P3,P4),有了这四个点,我们就能画出一个最小矩形,把这个图像框在里面。

一维组数映射到二维空间

根据数组下标计算当前点在二维空间的坐标,方法其实很简单。

看下面这张图,可以认为这是一张 5X4 的一张图片,那么总共就有20个像素点,每个格子里的0-19数字就意味着这是在一维数组里面的下标N,由此可得

X = N % width;
Y = Math.floor( N / width );

pixel

全数组扫描

首先能想到的最简单的办法,就是循环整个数组,当alpha值大于0的时候,即可判断这个点是一个有效像素点,然后整个数组循环完成之后,即可知道得到P1,P2,P3,P4四个点。

以下是实现代码:

1
function getEffectiveRect( imageData, width, height ) {
  // 上点, Y值最小
  var p1 = [ 0, height ];
  // 右点, X值最大
  var p2 = [ 0, 0 ];
  // 下点, Y值最大
  var p3 = [ 0, 0 ];
  // 左点, X值最小
  var p4 = [ width, 0 ];

  var data    = imageData.data;
  var number  = 0;
  console.time( 'image1' );
  for ( var i = 0; i < data.length; i ) {
    number++;
    var r = data[ i++ ];
    var g = data[ i++ ];
    var b = data[ i++ ];
    var a = data[ i++ ];
    if ( a === 0 ) {
      continue;
    }
    var x = number % width;
    var y = ( number - x ) / width;
    if ( p1[ 1 ] > y ) {
      p1[ 0 ] = x;
      p1[ 1 ] = y;
    }

    if ( p2[ 0 ] < x ) {
      p2[ 0 ] = x;
      p2[ 1 ] = y;
    }

    if ( p3[ 1 ] < y ) {
      p3[ 0 ] = x;
      p3[ 1 ] = y;
    }

    if ( p4[ 0 ] > x ) {
      p4[ 0 ] = x;
      p4[ 1 ] = y;
    }
  }

  return { p1, p2, p3, p4 };
}

从矩形的四个角分别去寻找

具体的思路如下图:

from-rect

理论上只要有效像素区域越大,那么需要计算的量就越少,速度也就越快,以下是我的实现代码:

1
function getEffectiveRect2( imageData, width, height ) {
  var data = imageData.data;

  console.time( 'image2' );
  var p1 = [];
  var p2 = [];
  var p3 = [];
  var p4 = [];

  // 寻找P1
  var len = 0
  for ( var i = 0; i < data.length / 4; i ++ ) {
    len   += 7;
    var a = data[ len ];
    if ( a !== 0 ) {
      var x = i % width;
      var y = ( i - x ) / width;
      p1    = [ x, y ];
      break;
    }
  };

  //寻找P3
  var len = 0;
  for ( var i = data.length / 4 - 1; i >= 0; i -- ) {
    len  += 7;
    var a = data[ len ];
    if ( a !== 0 ) {
      var x = i % width;
      var y = ( i - x ) / width;
      p3    = [ x, y ];
      break;
    }
  }

  // 寻找P2
  for ( var i   = width - 1; i >= 0; i -- ) {
    if ( p2.length ) {
      break;
    }
    var a = p1[ 1 ];
    var b = p3[ 1 ];
    var len = i + a * width;
    for ( var j = a; j < b; j ++ ) {
      len += width;
      // var point = i + j * width;
      var point = len;
      var a = data[ point * 4 + 3 ];
      if ( a !== 0 ) {
        var x = point % width;
        var y = ( point - x ) / height;
        p2    = [ x, y ];
        break;
      }
    }
  };

  // 寻找P4
  for ( var i = 0; i < width; i ++ ) {
    if ( p4.length ) {
      break;
    }
    for ( var j = p1[ 1 ]; j < p3[ 1 ]; j ++ ) {
      var point = i + j * width;
      var a = data[ point * 4 + 3 ];
      if ( a !== 0 ) {
        var x = point % width;
        var y = ( point - x ) / height;
        p4    = [ x, y ];
        break;
      }
    }
  }

  return { p1, p2, p3, p4 };
}

将两种方法分别去计算最开头的图片,两种方式的计算时间分别是:

1
image1: 10.83ms
image2: 15.32ms

结论

方法2引入的复杂的逻辑反而降低了计算时间,逻辑最简单的全数组扫描比第二种方式快了30%,因为涉及到的东西比较多,目前还不清楚到底是什么原因导致第二种方式速度如此慢,测试了一下,只有当一整张图片几乎全部有像素点的时候,两种方法的速度才差不多。后续我会继续寻找原因,猜测可能是方法2的计算量比方法1多,而方法1更多的是比较和赋值。