寫在開頭
本文只是介紹,通過安卓原生的方式將一張原始圖片縮放到合適的大小,嚴格來說是縮放圖片,而非壓縮圖片的技術
并且由于縮放后的圖片占空間還是較大,并且算法耗時較長,所以對于我的使用場景(壓縮上傳圖片)不是很好用,但是拿來做圖片墻的話還行
若想壓縮上傳圖片的話,還是推薦使用現成的庫,下面先推薦兩個圖片壓縮庫,傳送門:
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);
}
}