写在开头
本文只是介绍,通过安卓原生的方式将一张原始图片缩放到合适的大小,严格来说是缩放图片,而非压缩图片的技术
并且由于缩放后的图片占空间还是较大,并且算法耗时较长,所以对于我的使用场景(压缩上传图片)不是很好用,但是拿来做图片墙的话还行
若想压缩上传图片的话,还是推荐使用现成的库,下面先推荐两个图片压缩库,传送门:
AiYaCompressHelper
Luban
图片压缩
生产环境用户反映,上传身份证经常会失败,而且很费时间,检查代码发现前辈的代码写的浆糊一般,净出现一些w,ww,www的变量名,我知道他们都指代宽width,但原谅后辈脑子笨,实在记不住谁是谁,只能删掉重来。
压缩图片的思路,无非就是以下几步:
- 通过inSampleSize减少取样点,先将图片大概压缩一下
- 通过Matrix,对图片大小进行精确调整
- 改变编码格式,将图片转存为PNG或者JPG
减少采样点
减少采样点是BitmapFactory中提供的一个方法,主要是用到了inSampleSize参数。若inSampleSize为1时,采样后的图片就是原始大小的图片,若inSampleSize为2,则采样后的图片的宽高为原图的1/2,像素面积为原图的1/4,占空间也为1/4;若inSampleSize为4,则采样后的图片的宽高为原图的1/4,像素面积为原图的1/16,占空间也为1/16以此类推。
但是inSampleSize参数不能为浮点数,以及小于1的数,小于1时作为1来处理,即不能将图片放大,因为原理上不允许。
这里有一个特殊情况,官方文档中指出,inSampleSize参数取值应该为2的幂,即1、2、4、8等,若给的值不为2的幂,则会取一个比给的值小的最大的2的幂来代替。例如inSampleSize参数取值为10,则会用8来代替。但这个结论并非在所有系统上成立,因此此处应该严格控制,否则会得到意想不到的结果。
获取采样率的步骤遵循以下流程:
- BitmapFactory.Options.inJustDecodeBounds = true,若此时加载图片,只会加载图片的宽高信息
- 加载图片,然后从BitmapFactory.Options中取出宽高信息
- 根据目标大小计算采样率
- 可选步骤,BitmapFactory.Options.inPreferredConfig = Config.RGB_565,将图像设为565模式,此时图像深度为2字节。安卓中默认模式为RGB_8888,图像深度为4字节,但大部分情况不需要图像透明属性
- BitmapFactory.Options.inJustDecodeBounds = false,重新加载图片
需要注意的是,降低采样点加载图片耗时较长,一次处理大概需要200-400ms,大量图片请使用线程池。
Matrix变换
由于修改采样点无法将图片缩放到一个准确的大小,所以还需要Matrix做后续处理。因为图片在内存中存储就是一个个像素点,Matrix可以对每个像素点进行相应的变换,即可完成对图像的变换。
需要注意的是,为什么有了Matrix变换,还需要用更改采样点的方式做一次预处理?因为Matrix变换需要将图片加载进内存操作,而现在手机相机,拍一张图像大约16M像素点,若使用RGB_8888模式加载,一张图片大概需要60M的内存,虽然现在手机内存够大,但一个应用程序可用内存也就几百M,加载大量图片还是会OOM。所以应该先减少采样点加载图片,做好预处理之后再用Matrix微调。
顺便说一下Matrix,Matrix基本上就是一个用来操作图片的类,但是不止是缩放,还有其他功能,比如反转,位移,倾斜等
setTranslate(float dx,float dy):位移操作
setSkew(float kx,float ky):倾斜操作,kx、ky为X、Y方向上的比例
setSkew(float kx,float ky,float px,float py):倾斜操作,以px、py为轴心进行倾斜,kx、ky为X、Y方向上的倾斜比例
setRotate(float degrees):旋转操作,轴心为(0,0)
setRotate(float degrees,float px,float py):旋转操作,轴心为(px,py)
setScale(float sx,float sy):缩放操作,sx、sy为X、Y方向上的缩放比例。
setScale(float sx,float sy,float px,float py):缩放操作,以(px,py)为轴心进行缩放,sx、sy为X、Y方向上的缩放比例
不过这不是本文的重点,不再详述。
完整代码贴出:
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.Base64;
import java.io.ByteArrayOutputStream;
/**
* Created by ZhangXuan
* 图像缩放工具类
*/
public class ImageScalingUtil {
/**
* 通过降低取样点压缩图片,不推荐直接使用<br/>
* 压缩后图像使用RGB_565模式,即每个像素占位2字节,限定宽高压缩<br/>
* 由于inSampleSize压缩比这个参数在不同手机表现不同,有的手机可以取任意整数,有的手机只能取2的幂数,则取2的幂数保证所有手机表现一致。<br/>
* 需注意,由于inSampleSize的特性,若限定宽为1000x1000,实际图片宽为1010x600,则该图片会被压缩为505x300,图片会较小
*
* @param imgPath 原图片路径
* @param reqWidth 最大宽度
* @param reqHeight 最大高度
* @return 压缩后的bitmap
*/
public static Bitmap reducingBitmapSampleFromPath(String imgPath, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;// 读取大小不读取内容
options.inPreferredConfig = Config.RGB_565;// 设置图片每个像素占2字节,没有透明度
BitmapFactory.decodeFile(imgPath, options);// options读取图片
double outWidth = options.outWidth;
double outHeight = options.outHeight;// 获取到当前图片宽高
int inSampleSize = 1;
/*
先计算原图片宽高比ratio=width/height,再计算限定的范围的宽高比比reqRatio,
若reqRatio > ratio,则说明限定的范围更加细长,则以高为标准计算inSampleSize
否则,则说明限定范围更加粗矮,则以宽为计算标准
*/
double ratio = outWidth / outHeight;
double reqRatio = reqWidth / reqHeight;
if (reqRatio > ratio)
while (outHeight / inSampleSize > reqHeight) inSampleSize *= 2;
else
while (outWidth / inSampleSize > reqWidth) inSampleSize *= 2;
options.inSampleSize = inSampleSize;
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(imgPath, options);
}
/**
* 通过降低取样点压缩图片,不推荐直接使用<br/>
* 压缩后图像使用RGB_565模式,即每个像素占位2字节,限定大小压缩<br/>
* 由于inSampleSize压缩比这个参数在不同手机表现不同,有的手机可以取任意整数,有的手机只能取2的幂数,则取2的幂数保证所有手机表现一致。<br/>
* 需注意,由于inSampleSize的特性,若限定大小为500k,而原图为501k,则压缩后的图片为125.25k,图片会较小
*
* @param imgPath 原图片路径
* @param reqSize 目标文件大小,单位为kb
* @return 压缩后的bitmap
*/
public static Bitmap reducingBitmapSampleFromPath(String imgPath, int reqSize) {
long area = reqSize * 1024 / 2;// 每个像素占2字节,将需求大小转为像素面积
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;// 读取大小不读取内容
options.inPreferredConfig = Config.RGB_565;// 设置图片每个像素占2字节,没有透明度
BitmapFactory.decodeFile(imgPath, options);// options读取图片
double outWidth = options.outWidth;
double outHeight = options.outHeight;// 获取到当前图片宽高
int inSampleSize = 1;
while ((outHeight / inSampleSize) * (outWidth / inSampleSize) > area)
inSampleSize *= 2;
options.inSampleSize = inSampleSize;
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(imgPath, options);
}
/**
* 压缩图片<br/>
* 通过设定压缩后的宽高的最大像素,将图片等比例缩小<br/>
* 先通过降低取样点,将图片压缩到比目标宽高稍大一点,然后再通过Matrix将图片精确调整到目标大小<br/>
* 压缩后图像使用RGB_565模式,即每个像素占位2字节,限定大小压缩<br/>
* 若被压缩图片本身就小于限定大小,则不改变其大小,只更改图像颜色模式为RGB_565<br/>
* 由于inSampleSize压缩比这个参数在不同手机表现不同,有的手机可以取任意整数,有的手机只能取2的幂数,则通过混合压缩的方式保证压缩的结果一致<br/>
*
* @param imgPath 原图片路径
* @param reqWidth 最大宽度
* @param reqHeight 最大高度
* @return 压缩后的bitmap
*/
public static Bitmap compressBitmapFromPath(String imgPath, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;// 读取大小不读取内容
options.inPreferredConfig = Config.RGB_565;// 设置图片每个像素占2字节,没有透明度
BitmapFactory.decodeFile(imgPath, options);// options读取图片
double outWidth = options.outWidth;
double outHeight = options.outHeight;// 获取到当前图片宽高
int inSampleSize = 1;
/*
先计算原图片宽高比ratio=width/height,再计算限定的范围的宽高比比reqRatio,
若reqRatio > ratio,则说明限定的范围更加细长,则以高为标准计算inSampleSize
否则,则说明限定范围更加粗矮,则以宽为计算标准
*/
double ratio = outWidth / outHeight;
double reqRatio = reqWidth / reqHeight;
if (reqRatio > ratio)
while (outHeight / inSampleSize > reqHeight) inSampleSize *= 2;
else
while (outWidth / inSampleSize > reqWidth) inSampleSize *= 2;
options.inSampleSize = inSampleSize;
options.inJustDecodeBounds = false;
if (1 == inSampleSize) {
// inSampleSize == 1,就说明原图比要求的尺寸小或者相等,那么不用继续压缩,直接返回。
return BitmapFactory.decodeFile(imgPath, options);
}
/*
否则的话,先将图片通过减少采样点的方式,以一个比限定范围稍大的尺寸读入内存,
防止因为图片太大而OOM,以及太大的图片加载时间过长
然后继续进行压缩的步骤
*/
options.inSampleSize = inSampleSize / 2;
Bitmap baseBitmap = BitmapFactory.decodeFile(imgPath, options);
/*
使用之前计算过的宽高比,
若reqRatio > ratio,则说明限定的范围更加细长,则以高为标准计算压缩比
否则,则说明限定范围更加粗矮,则以宽为计算标准
*/
float compressRatio = 1;
if (reqRatio > ratio)
compressRatio = reqHeight * 1.0f / baseBitmap.getHeight();
else
compressRatio = reqWidth * 1.0f / baseBitmap.getWidth();
Bitmap afterBitmap = Bitmap.createBitmap(
(int) (baseBitmap.getWidth() * compressRatio),
(int) (baseBitmap.getHeight() * compressRatio),
baseBitmap.getConfig());
Canvas canvas = new Canvas(afterBitmap);
// 初始化Matrix对象
Matrix matrix = new Matrix();
// 根据传入的参数设置缩放比例
matrix.setScale(compressRatio, compressRatio);
Paint paint = new Paint();
// 消除锯齿
paint.setAntiAlias(true);
// 根据缩放比例,把图片draw到Canvas上
canvas.drawBitmap(baseBitmap, matrix, paint);
return afterBitmap;
}
/**
* 压缩图片<br/>
* 通过设定压缩后的大小,将图片等比例缩小<br/>
* 先通过降低取样点,将图片压缩到比目标宽高稍大一点,然后再通过Matrix将图片精确调整到目标大小<br/>
* 压缩后图像使用RGB_565模式,即每个像素占位2字节<br/>
* 若被压缩图片本身就小于限定大小,则不改变其大小,只更改图像颜色模式为RGB_565<br/>
* 由于inSampleSize压缩比这个参数在不同手机表现不同,有的手机可以取任意整数,有的手机只能取2的幂数,则通过混合压缩的方式保证压缩的结果一致<br/>
*
* @param imgPath 原图片路径
* @param reqSize 压缩后文件大小,单位为kb
* @return 压缩后的bitmap
*/
public static Bitmap compressBitmapFromPath(String imgPath, int reqSize) {
long area = reqSize * 1024 / 2;// 每个像素占2字节,将需求大小转为像素面积
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;// 读取大小不读取内容
options.inPreferredConfig = Config.RGB_565;// 设置图片每个像素占2字节,没有透明度
BitmapFactory.decodeFile(imgPath, options);// options读取图片
double outWidth = options.outWidth;
double outHeight = options.outHeight;// 获取到当前图片宽高
int inSampleSize = 1;
while ((outHeight / inSampleSize) * (outWidth / inSampleSize) > area)
inSampleSize *= 2;
options.inSampleSize = inSampleSize;
options.inJustDecodeBounds = false;
if (1 == inSampleSize) {
// inSampleSize == 1,就说明原图比要求的尺寸小或者相等,那么不用继续压缩,直接返回。
return BitmapFactory.decodeFile(imgPath, options);
}
/*
否则的话,先将图片通过减少采样点的方式,以一个比限定范围稍大的尺寸读入内存,
防止因为图片太大而OOM,以及太大的图片加载时间过长
然后继续进行压缩的步骤
*/
options.inSampleSize = inSampleSize / 2;
Bitmap baseBitmap = BitmapFactory.decodeFile(imgPath, options);
/*
目标大小的面积与现在图片大小的面积的比的平方根,就是缩放比
java Math.sqrt() 函数不能开小数,而且先计算除法,再计算开放,再对结果求反误差很大,所以做两次开方计算
*/
float compressRatio = 1;
compressRatio = (float) (Math.sqrt(area) / Math.sqrt(baseBitmap.getWidth() * baseBitmap.getHeight()));
Bitmap afterBitmap = Bitmap.createBitmap(
(int) (baseBitmap.getWidth() * compressRatio),
(int) (baseBitmap.getHeight() * compressRatio),
baseBitmap.getConfig());
Canvas canvas = new Canvas(afterBitmap);
// 初始化Matrix对象
Matrix matrix = new Matrix();
// 根据传入的参数设置缩放比例
matrix.setScale(compressRatio, compressRatio);
Paint paint = new Paint();
// 消除锯齿
paint.setAntiAlias(true);
// 根据缩放比例,把图片draw到Canvas上
canvas.drawBitmap(baseBitmap, matrix, paint);
return afterBitmap;
}
/**
* 将一张图片 以PNG的格式 转换成 base64 编码
*
* @param bitmap
* @return
*/
public static String savePNGAndToBase64(Bitmap bitmap) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();// outputstream
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
byte[] pngByte = baos.toByteArray();// 转为byte数组
return Base64.encodeToString(pngByte, Base64.DEFAULT);
}
/**
* 将一张图片 以JPEG的格式 转换成 base64 编码
*
* @param bitmap
* @return
*/
public static String saveJPEGAndToBase64(Bitmap bitmap) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();// outputstream
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
byte[] pngByte = baos.toByteArray();// 转为byte数组
return Base64.encodeToString(pngByte, Base64.DEFAULT);
}
}