luban 鲁班算法是号称最接近微信朋友圈图片压缩算法的一种图片压缩算法,GitHub 地址:
https://github.com/Curzibn/Luban
根据作者提供的数据,压缩效果如下:
内容 | 原图 | Luban |
Wechat |
---|---|---|---|
截屏 720P | 720*1280,390k | 720*1280,87k | 720*1280,56k |
截屏 1080P | 1080*1920,2.21M | 1080*1920,104k | 1080*1920,112k |
拍照 13M(4:3) | 3096*4128,3.12M | 1548*2064,141k | 1548*2064,147k |
拍照 9.6M(16:9) | 4128*2322,4.64M | 1032*581,97k | 1032*581,74k |
滚动截屏 | 1080*6433,1.56M | 1080*6433,351k | 1080*6433,482k |
这里的压缩,指的是,不影响分辨率的情况下,对图片进行压缩,那么图片压缩的原理是什么呢?假设一张原始,例如小米五相机拍摄的图片,分辨率是 3456 X 4608 px,每个像素使用 32位表示,那么所占内存如下:
3456 X 4608 X 4 = 63700992 字节 =60.75 MB,然而实际上,却不是,我们看到的原图大小,可能是5 MB 左右,那么实际上, jpg 格式采用了特殊的编码格式,所以实际上,我们可以对 jpg 格式进行多次压缩,根据维基百科资料显示,在原始 jpg 图片压缩为 六分一的 质量之后,人眼是无法察觉有损失 差距的。
压缩一个图片的代码如下:
Luban.with(this)
.load(photos)//设置压缩图片文件路径,全路径
.ignoreBy(100)//设置忽略压缩的大小上限
.setTargetDir(getPath())//设置压缩输出文件目录
.setCompressListener(new OnCompressListener() {
@Override
public void onStart() {
}
@Override
public void onSuccess(File file) {
showResult(photos, file);
}
@Override
public void onError(Throwable e) {
}
}).launch();
主要看 launch() 方法,如下:
@UiThread private void launch(final Context context) {
//检查路径准确性以及是否设置了回调监听器
if (mPaths == null || mPaths.size() == 0 && mCompressListener != null) {
mCompressListener.onError(new NullPointerException("image file cannot be null"));
}
Iterator iterator = mPaths.iterator();
while (iterator.hasNext()) {
final String path = iterator.next();
if (Checker.isImage(path)) {
// 使用单线程池执行器,一次只能执行一个线程
AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
@Override public void run() {
try {
mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_START));//发送开始压缩的信息
//开始压缩,实际起压缩作用的是 Engine 类的 compress() 方法
File result = Checker.isNeedCompress(mLeastCompressSize, path) ?
new Engine(path, getImageCacheFile(context, Checker.checkSuffix(path))).compress() :
new File(path);
mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_SUCCESS, result));
} catch (IOException e) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_ERROR, e));
}
}
});
} else {
mCompressListener.onError(new IllegalArgumentException("can not read the path : " + path));
}
iterator.remove();
}
}
然后这里实际起作用的是 Engine 的 compress() 方法,首先会创建一个新的 Engine() 对象,调用方法如下:
Engine(String srcImg, File tagImg) throws IOException {
if (Checker.isJPG(srcImg)) {//判断格式是否 jpg 或者 jpeg
this.srcExif = new ExifInterface(srcImg);//创建 ExifInterface 对象,这个对象用于图片读取,旋转,生成缩略图
}
this.tagImg = tagImg;
this.srcImg = srcImg;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;//设置读取方法为只读取边界大小
options.inSampleSize = 1;
BitmapFactory.decodeFile(srcImg, options);
this.srcWidth = options.outWidth;//设置为图片原始宽高
this.srcHeight = options.outHeight;
}
Engine 类里面的值都是通过 Luban.Builder 传递过来的。
具体的压缩比例计算,在 computeSize() 方法里面,computeSize() 方法主要是根据图片的宽高比,对图片进行压缩,这个方法的返回值,设置的是
private int computeSize() {
//将 srcWidth 和 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 >= 1664 && 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));
}
}
判断图片比例值,是否处于以下区间内;
判断图片最长边是否过边界值;
计算压缩图片实际边长值,以第2步计算结果为准,超过某个边界值则:width / pow(2, n-1),height/pow(2, n-1)
size = (newW * newH) / (width * height) * m;
[0.5, 0) 则 width = 1280,height = 1280 / scale,m = 500;注:scale为比例值
[1, 0.5625) 则最小 size 对应 60,60,100
[0.5, 0) 则最小 size 都为 100
那么为什么会采用这种压缩方式呢?
我们很简单的可以理解,移动设备的分辨率有限,我们只需要保证,能够压缩的图片能够在主流分辨率上能够达到合适的显示效果,并且最大可能的去压缩图片分辨率,这就是图片压缩的初衷。所以,合理之处便是在于设置这个分辨率了,luban 算法就是起了这个效果吧。
最终执行压缩操作的方法 cmopress() 方法
File compress() throws IOException {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = computeSize();//根据图片比例,设置压缩比例
Bitmap tagBitmap = BitmapFactory.decodeFile(srcImg, options);//压缩图片
ByteArrayOutputStream stream = new ByteArrayOutputStream();
tagBitmap = rotatingImage(tagBitmap);
tagBitmap.compress(Bitmap.CompressFormat.JPEG, 60, stream);
tagBitmap.recycle();//将 bitmap 写入到输入流
FileOutputStream fos = new FileOutputStream(tagImg);//将输入流写入文件
fos.write(stream.toByteArray());
fos.flush();
fos.close();
stream.close();
return tagImg;
}
这里比较简单,就不多阐述了。