目前作App开发总绕不开图片这个元素。可是随着手机拍照分辨率的提高,图片的压缩成为一个很重要的问题。单纯对图片进行裁切,压缩已经有不少文章介绍。可是裁切成多少,压缩成多少却很难控制好,裁切过头图片过小,质量压缩过头则显示效果太差。java
因而天然想到App巨头“微信”会是怎么处理,Luban(鲁班)就是经过在微信朋友圈发送近100张不一样分辨率图片,对比原图与微信压缩后的图片逆向推算出来的压缩算法。android
由于有其余语言也想要实现Luban,因此描述了一遍算法步骤。git
由于是逆向推算,效果还无法跟微信如出一辙,可是已经很接近微信朋友圈压缩后的效果,具体看如下对比!github
Luban内部采用IO线程进行图片压缩,外部调用只需设置好结果监听便可:算法
Luban.with(this)
.load(photos)
.ignoreBy(100)
.setTargetDir(getPath())
.filter(new CompressionPredicate() {
@Override
public boolean apply(String path) {
return !(TextUtils.isEmpty(path) || path.toLowerCase().endsWith(".gif"));
}
})
.setCompressListener(new OnCompressListener() {
@Override
public void onStart() {
// TODO 压缩开始前调用,能够在方法内启动 loading UI
}
@Override
public void onSuccess(File file) {
// TODO 压缩成功后调用,返回压缩后的图片文件
}
@Override
public void onError(Throwable e) {
// TODO 当压缩过程出现问题时调用
}
}).launch();
复制代码
同步方法请尽可能避免在主线程调用以避免阻塞主线程,下面以rxJava调用为例缓存
Flowable.just(photos)
.observeOn(Schedulers.io())
.map(new Function<List<String>, List<File>>() {
@Override public List<File> apply(@NonNull List<String> list) throws Exception {
// 同步方法直接返回压缩后的文件
return Luban.with(MainActivity.this).load(list).get();
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
复制代码
/**
* 用来判断是不是jpg图片
*
*/
private boolean isJPG(byte[] data) {
if (data == null || data.length < 3) {
return false;
}
byte[] signatureB = new byte[]{data[0], data[1], data[2]};
return Arrays.equals(JPEG_SIGNATURE, signatureB);
}
/**
* 图片是否须要压缩
*
*/
boolean needCompress(int leastCompressSize, String path) {
if (leastCompressSize > 0) {
File source = new File(path);
return source.exists() && source.length() > (leastCompressSize << 10);
}
return true;
}
/**
* android camera源码 用来获取图片的角度
*
*/
private int getOrientation(byte[] jpeg) {
if (jpeg == null) {
return 0;
}
int offset = 0;
int length = 0;
// ISO/IEC 10918-1:1993(E)
while (offset + 3 < jpeg.length && (jpeg[offset++] & 0xFF) == 0xFF) {
int marker = jpeg[offset] & 0xFF;
// Check if the marker is a padding.
if (marker == 0xFF) {
continue;
}
offset++;
// Check if the marker is SOI or TEM.
if (marker == 0xD8 || marker == 0x01) {
continue;
}
// Check if the marker is EOI or SOS.
if (marker == 0xD9 || marker == 0xDA) {
break;
}
// Get the length and check if it is reasonable.
length = pack(jpeg, offset, 2, false);
if (length < 2 || offset + length > jpeg.length) {
Log.e(TAG, "Invalid length");
return 0;
}
// Break if the marker is EXIF in APP1.
if (marker == 0xE1 && length >= 8
&& pack(jpeg, offset + 2, 4, false) == 0x45786966
&& pack(jpeg, offset + 6, 2, false) == 0) {
offset += 8;
length -= 8;
break;
}
// Skip other markers.
offset += length;
length = 0;
}
// JEITA CP-3451 Exif Version 2.2
if (length > 8) {
// Identify the byte order.
int tag = pack(jpeg, offset, 4, false);
if (tag != 0x49492A00 && tag != 0x4D4D002A) {
Log.e(TAG, "Invalid byte order");
return 0;
}
boolean littleEndian = (tag == 0x49492A00);
// Get the offset and check if it is reasonable.
int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
if (count < 10 || count > length) {
Log.e(TAG, "Invalid offset");
return 0;
}
offset += count;
length -= count;
// Get the count and go through all the elements.
count = pack(jpeg, offset - 2, 2, littleEndian);
while (count-- > 0 && length >= 12) {
// Get the tag and check if it is orientation.
tag = pack(jpeg, offset, 2, littleEndian);
if (tag == 0x0112) {
int orientation = pack(jpeg, offset + 8, 2, littleEndian);
switch (orientation) {
case 1:
return 0;
case 3:
return 180;
case 6:
return 90;
case 8:
return 270;
}
Log.e(TAG, "Unsupported orientation");
return 0;
}
offset += 12;
length -= 12;
}
}
return 0;
}
复制代码
断言是否须要压缩接口bash
public interface CompressionPredicate {
/**
* 断言的路径是否要压缩,并返回boolean值
* @param path input path
* @return the boolean result
*/
boolean apply(String path);
}
复制代码
用于操做,开始压缩,管理活动,缓存资源微信
/**
* 构造函数
*
*/
Engine(InputStreamProvider srcImg, File tagImg, boolean focusAlpha) throws IOException {
this.tagImg = tagImg;
this.srcImg = srcImg;
this.focusAlpha = focusAlpha;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
options.inSampleSize = 1;
BitmapFactory.decodeStream(srcImg.open(), null, options);
this.srcWidth = options.outWidth;
this.srcHeight = options.outHeight;
}
/**
* 计算压缩比例
*
*/
private int computeSize() {
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));
}
}
/**
* 压缩
*
*/
File compress() throws IOException {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = computeSize();
Bitmap tagBitmap = BitmapFactory.decodeStream(srcImg.open(), null, options);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
if (Checker.SINGLE.isJPG(srcImg.open())) {
tagBitmap = rotatingImage(tagBitmap, Checker.SINGLE.getOrientation(srcImg.open()));
}
tagBitmap.compress(focusAlpha ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, 60, stream);
tagBitmap.recycle();
FileOutputStream fos = new FileOutputStream(tagImg);
fos.write(stream.toByteArray());
fos.flush();
fos.close();
stream.close();
return tagImg;
}
复制代码
获取输入流兼容文件、FileProvider方式获取图片接口app
public interface InputStreamProvider {
InputStream open() throws IOException;
void close();
String getPath();
}
复制代码
InputStreamProvider类的实现异步
@Override
public InputStream open() throws IOException {
close();
inputStream = openInternal();
return inputStream;
}
public abstract InputStream openInternal() throws IOException;
@Override
public void close() {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException ignore) {
}finally {
inputStream = null;
}
}
}
复制代码
public interface OnCompressListener {
/**
* 开始压缩
*/
void onStart();
/**
* 压缩成功
*/
void onSuccess(File file);
/**
* 压缩异常
*/
void onError(Throwable e);
}
复制代码
public interface OnRenameListener {
/**
* 压缩前调用该方法用于修改压缩后文件名
*
*/
String rename(String filePath);
}
复制代码
加载图片的几种方式
/**
* 加载图片
*
*/
public Builder load(InputStreamProvider inputStreamProvider) {
mStreamProviders.add(inputStreamProvider);
return this;
}
/**
* 加载图片
*
*/
public Builder load(final File file) {
mStreamProviders.add(new InputStreamAdapter() {
@Override
public InputStream openInternal() throws IOException {
return new FileInputStream(file);
}
@Override
public String getPath() {
return file.getAbsolutePath();
}
});
return this;
}
/**
* 加载图片
*
*/
public Builder load(final String string) {
mStreamProviders.add(new InputStreamAdapter() {
@Override
public InputStream openInternal() throws IOException {
return new FileInputStream(string);
}
@Override
public String getPath() {
return string;
}
});
return this;
}
/**
* 加载图片
*
*/
public Builder load(final Uri uri) {
mStreamProviders.add(new InputStreamAdapter() {
@Override
public InputStream openInternal() throws IOException {
return context.getContentResolver().openInputStream(uri);
}
@Override
public String getPath() {
return uri.getPath();
}
});
return this;
}
/**
* 加载图片列表
*
*/
public <T> Builder load(List<T> list) {
for (T src : list) {
if (src instanceof String) {
load((String) src);
} else if (src instanceof File) {
load((File) src);
} else if (src instanceof Uri) {
load((Uri) src);
} else {
throw new IllegalArgumentException("Incoming data type exception, it must be String, File, Uri or Bitmap");
}
}
return this;
}
复制代码
压缩可设置的先行条件
/**
* 压缩的最小单位值,单位kB,默认100kb
*
*/
public Builder ignoreBy(int size) {
this.mLeastCompressSize = size;
return this;
}
/**
* 压缩断言
*
*/
public Builder filter(CompressionPredicate compressionPredicate) {
this.mCompressionPredicate = compressionPredicate;
return this;
}
复制代码
压缩其余配置
/**
* 压缩后目录
*
*/
public Builder setTargetDir(String targetDir) {
this.mTargetDir = targetDir;
return this;
}
/**
* 是否开启透明通道,true为png格式压缩,false为jpg格式压缩
*
*/
public Builder setFocusAlpha(boolean focusAlpha) {
this.focusAlpha = focusAlpha;
return this;
}
复制代码
压缩启动
/**
* 开启压缩
*
*/
private void launch(final Context context) {
if (mStreamProviders == null || mStreamProviders.size() == 0 && mCompressListener != null) {
mCompressListener.onError(new NullPointerException("image file cannot be null"));
}
Iterator<InputStreamProvider> iterator = mStreamProviders.iterator();
while (iterator.hasNext()) {
final InputStreamProvider path = iterator.next();
AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
try {
mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_START));
File result = compress(context, path);
mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_SUCCESS, result));
} catch (IOException e) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_ERROR, e));
}
}
});
iterator.remove();
}
}
复制代码