目前作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();
}
}
複製代碼