目前照片动辄5-10M,这样原图上传会对服务器和流量造成很大的负担,所以我们一般在上传服务器的时候会进行压缩再上传(除非用户选择原图上传)。这里介绍几种常见的压缩方法,质量压缩,尺寸压缩,采样率压缩,JNI调用libjpeg库来进行压缩,最后我会借鉴鲁班压缩(号称最接近微信压缩的方法)写一个结合采样率和质量压缩的方法。有个小建议,Android项目本地资源图片可以采用webp格式,webp是一种同时提供了有损压缩和无损压缩的图片格式,无损webp平均比png小26%,有损的webp平均比jpeg小25%~34%,采用webp能够在保持图片清晰度的情况下,可以有效减小图片所占有的磁盘空间大小。
1.质量压缩
质量压缩是在不改变分辨率的前提下,改变图片的位深和透明度来达到压缩的效果,因此质量压缩所占内存不会变小,但是所占的磁盘会变小。如果图片是png格式,则设置quality无效,因为png是无损压缩。
/**
* 质量压缩
* 第一个参数为需要压缩的bitmap对象,第二个参数为压缩后图片保存的位置
* 设置options 属性0-100,来实现压缩(因为png是无损压缩,所以该属性对png是无效的)
*
* @param bmp
* @param file
*/
public static void qualityCompress(Bitmap bmp, File file, int quality) {
// 0-100 100为不压缩
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 把压缩后的数据存放到baos中
bmp.compress(Bitmap.CompressFormat.JPEG, quality, baos);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
2.尺寸压缩
通过减少像素值来降低图片的占用内存大小和磁盘大小,可以用来缓存缩略图。
/**
* 尺寸压缩(通过缩放图片像素来减少图片占用内存大小和磁盘空间)
*
* @param bmp
* @param file
*/
public static void sizeCompress(Bitmap bmp, File file) {
// 尺寸压缩倍数,值越大,图片尺寸越小
int ratio = 6;
// 压缩Bitmap到对应尺寸
Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Config.ARGB_8888);
Canvas canvas = new Canvas(result);
Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio);
canvas.drawBitmap(bmp, null, rect, null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 把压缩后的数据存放到baos中
result.compress(Bitmap.CompressFormat.JPEG, 100, baos);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
3.采样率压缩
采样率压缩是通过设置BitmapFactory.Options.inSampleSize,来减小图片的分辨率,从而减小图片所占用的内存大小和磁盘大小。这里要注意的一点是,采样率是2的幂次方,这样就可以会导致最佳压缩比例在两个2的幂次方之间,采取小的那个值图片可能会过大,采取大的值图片质量会严重下降,如果你设置的采样率不是2的幂次方,会自动选用比这个值小的那个幂次方值。
/**
* 采样率压缩
*
* @param filePath
* @param file
*/
public static void sampleSizeCompress(String filePath, File file) {
// 数值越高,图片像素越低
int inSampleSize = 2;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
// options.inJustDecodeBounds = true;//为true的时候不会真正加载图片,而是得到图片的宽高信息。
//设置采样率
options.inSampleSize = inSampleSize;
Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 把压缩后的数据存放到baos中
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
4.通过JIN调用libjpeg库压缩
由于Android的图片引擎使用的是阉割版的skia引擎,去掉了图片压缩中的哈夫曼算法——一种耗CPU但是能在保持图片清晰度的情况下很大程度降低图片的磁盘空间大小的算法,所以这就是ios拍出的1M的照片比Android5M的还要清晰的原因。
优化方案:绕过安卓Bitmap API层,自己编码实现——哈夫曼算法,目前笔者没有尝试过,后续有深入研究的话会更新。
总结
尺寸压缩和采样率压缩都是通过改变图片的像素来压缩图片的大小,那么有什么区别呢:采样率压缩的比例会受到限制,尺寸压缩不会,但是采样率压缩的清晰度会比尺寸压缩的清晰度要好一些。
最后我把自己写的一个借鉴鲁班压缩算法的图片压缩方法贴出来,供大家参考也可直接复制使用,鲁班压缩主要是根据3个图片比例区间和边界值去算出相应的采样率,再加上60的质量压缩。有兴趣的同学可以去看鲁班压缩的源码。
/**
* 采用鲁班压缩算法压缩
*
* @param filePath
* @param file
*/
public static void imgCompress(String filePath, File file) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
// options.inJustDecodeBounds = true;//为true的时候不会真正加载图片,而是得到图片的宽高信息。
//采样率
options.inSampleSize = computeSize(options.outWidth, options.outHeight);
Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 把压缩后的数据存放到baos中
bitmap.compress(Bitmap.CompressFormat.JPEG, 60, baos);
try {
if (file.exists()) {
file.delete();
} else {
file.createNewFile();
}
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static int computeSize(int srcWidth,int srcHeight) {
srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth;
srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight;
int longSide = Math.max(srcWidth, srcHeight);
int shortSide = Math.min(srcWidth, srcHeight);
float scale = ((float) shortSide / longSide);
if (scale <= 1 && scale > 0.5625) {
if (longSide < 1664) {
return 1;
} else if (longSide < 4990) {
return 2;
} else if (longSide > 4990 && longSide < 10240) {
return 4;
} else {
return longSide / 1280 == 0 ? 1 : longSide / 1280;
}
} else if (scale <= 0.5625 && scale > 0.5) {
return longSide / 1280 == 0 ? 1 : longSide / 1280;
} else {
return (int) Math.ceil(longSide / (1280.0 / scale));
}
}