Android 中图片的缩放和压缩问题

最近接的一个外包项目中有拍照上传的操作,而且坑爹的是需要连续拍 16 张后一起上传!这样就不得不对图片进行压缩处理了,而且客户对尺寸和大小都有要求。

大小压缩比较常用的是通过 bitmap.compress() 方法中的 quality 参数控制,而尺寸压缩的方法就比较多了:

  1. 直接调用系统裁剪组件 com.android.camera.action.CROP

  2. 调用 Bitmap createScaledBitmap() 方法;

  3. 调用 ThumbnailUtils 工具类中的 extractThumbnail() 方法;

  4. 通过指定 BitmapFactory.Options 中的 inSampleSize 实现;

第 1 种简单,而且尺寸大小都能压缩,但客户不想看到裁剪页面;

第 2 种以前通过友盟统计发现有 OOM 的风险;

第 3 种虽然也方便,但是图片是从中间截取的,而且通过源码可以看到,中见产生了一些临时 Bitmap ,这无疑也增大了 OOM 的风险;

第 4 种以前虽然知道这个用法,但更多的只是用于 decode 本地图片的时候进行采样避免 OOM,并没有来做过精确的尺寸压缩。于是正好想尝试下这种方法。

思路如下:

  1. 首次 decode 的时候指定 inJustDecodeBounds = true ,只读取边界参数得到图片的宽度和高度;

  2. 通过得到的宽高值和指定的数值计算 inSampleSize ,并指定 inJustDecodeBounds = false 得到缩放后的 Bitmap ;

  3. 通过 bitmap.compress() ,指定合适的 quality 压缩保存为文件。

/**
 * 
 * @param srcPath
 * @param dstPath
 * @param maxWidth
 * @param maxHeight
 * @param maxSize
 * @param format
 */
public static void compress(String srcPath, String dstPath, int maxWidth, int maxHeight, long maxSize, CompressFormat format) {
	BitmapFactory.Options opts=new BitmapFactory.Options();
	opts.inJustDecodeBounds=true;
	Bitmap bitmap=BitmapFactory.decodeFile(srcPath, opts);
	opts.inJustDecodeBounds=false;
	int w=opts.outWidth;
	int h=opts.outHeight;
	int size=0;
	if(w<=maxWidth&&h<=maxHeight){
		size=1;
	}else{
		size=w>=h?w/maxWidth:h/maxHeight;
	}
	opts.inSampleSize=size;
	bitmap=BitmapFactory.decodeFile(srcPath, opts);
	ByteArrayOutputStream baos=new ByteArrayOutputStream();
	int quality=100;
	bitmap.compress(format, quality, baos);
	while(baos.toByteArray().length>maxSize){		
		baos.reset();
		bitmap.compress(format, quality, baos);
		quality-=10;
	}
	try {
		baos.writeTo(new FileOutputStream(dstPath));
	}catch(FileNotFoundException e) {
		e.printStackTrace();
	}catch (IOException e) {
		e.printStackTrace();
	}finally{
		try {
			baos.flush();
			baos.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

但测试发现,大小压缩和尺寸缩放比例都没问题,但有时候尺寸大小有问题:可能宽度和高度稍微超过限制。

这个 inSampleSize 取大于等于 1 的整数,默认为 1,即不采样;表示取 N 分之一的缩略图。按理说上面处理应该没问题。

正在仔细琢磨上面算法的时候,脑海突然想起来:记得以前貌似在哪里看到资料说这个 inSampleSize 好像是 2 的 N 次方。

看了官方文档,果然这么说的:默认会找一个最接近的 2 的 N 次方的整数。

很显然问题就出在这里,它所找的最接近的很可能并不是我们所需要的,可能已经超过了限制。

我们需要先对这个压缩比取 2 的对数,然后向上取整,最后再取2的指数得到正确的 inSampleSize

最终代码如下:

/**
 * 
 * @param srcPath
 * @param dstPath
 * @param maxWidth
 * @param maxHeight
 * @param maxSize
 * @param format
 */
public static void compress(String srcPath, String dstPath, int maxWidth, int maxHeight, long maxSize, CompressFormat format) {
	BitmapFactory.Options opts=new BitmapFactory.Options();
	opts.inJustDecodeBounds=true;
	Bitmap bitmap=BitmapFactory.decodeFile(srcPath, opts);
	opts.inJustDecodeBounds=false;
	int w=opts.outWidth;
	int h=opts.outHeight;
	int size=0;
	if(w<=maxWidth&&h<=maxHeight){
		size=1;
	}else{
		//The decoder uses a final value based on powers of 2, 
		//any other value will be rounded down to the nearest power of 2.
		//So we use a ceil log value to keep both of them under limits.
		//See doc: http://developer.android.com/reference/android/graphics/BitmapFactory.Options.html#inSampleSize
		double scale=w>=h?w/maxWidth:h/maxHeight;
		double log=Math.log(scale)/Math.log(2);
		double logCeil=Math.ceil(log);
		size=(int) Math.pow(2, logCeil);
	}
	opts.inSampleSize=size;
	bitmap=BitmapFactory.decodeFile(srcPath, opts);
	ByteArrayOutputStream baos=new ByteArrayOutputStream();
	int quality=100;
	bitmap.compress(format, quality, baos);
	while(baos.toByteArray().length>maxSize){		
		baos.reset();
		bitmap.compress(format, quality, baos);
		quality-=10;
	}
	try {
		baos.writeTo(new FileOutputStream(dstPath));
	}catch(FileNotFoundException e) {
		e.printStackTrace();
	}catch (IOException e) {
		e.printStackTrace();
	}finally{
		try {
			baos.flush();
			baos.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}