使用libjpeg進行圖片壓縮

簡介

由於工作原因,boss下達的任務就大概說了對圖片進行壓縮尋找比較合理的方式,還舉了一個項目中的坑,就是系統原生的Bitmap.compress設置質量參數為100生成圖片會變大的坑。所以我打算用一點時間研究研究Bitmap在內存和外存中的情況。首先需要對圖片進行壓縮,大家都知道圖片是Android裡面一個大坑,具體的問題有:

OOM,一不留神就用OOM來沖沖喜,所以網上就有了很多解決oom問題的建議,但是由於網友的水平不一也導致建議參差不齊。(內存)圖片壓縮再加載失真嚴重,或者壓縮率不夠達不到項目要求的效果。(外存)那我今天就要解決的就是通過今天查閱的資料和自己的判斷,還有實踐歸檔一下圖片在Android上的問題。並且給出自己解決圖片壓縮問題的解決方案和實際操作。

1、為什麼Android上的圖片就不如IOS上的?

libjpeg是廣泛使用的開源JPEG圖像庫,安卓也依賴libjpeg來壓縮圖片。但是安卓並不是直接封裝的libjpeg,而是基於了另一個叫Skia的開源項目來作為的圖像處理引擎。Skia是谷歌自己維護著的一個大而全的引擎,各種圖像處理功能均在其中予以實現,並且廣泛的應用於谷歌自己和其它公司的產品中(如:Chrome、Firefox、 Android等)。Skia對libjpeg進行了良好的封裝,基於這個引擎可以很方便為操作系統、瀏覽器等開發圖像處理功能。

libjpeg在壓縮圖像時,有一個參數叫optimize_coding,關於這個參數,libjpeg.doc有如下解釋:如果設置optimize_coding為TRUE,將會使得壓縮圖像過程中基於圖像數據計算哈弗曼表(關於圖片壓縮中的哈弗曼表,請自行查閱相關資料),由於這個計算會顯著消耗空間和時間,默認值被設置為FALSE。

谷歌的Skia項目工程師們最終沒有設置這個參數,optimize_coding在Skia中默認的等於了FALSE,這就意味著更差的圖片質量和更大的圖片文件,而壓縮圖片過程中所耗費的時間和空間其實反而是可以忽略不計的。那麼,這個參數的影響究竟會有多大呢?經我們實測,使用相同的原始圖片,分別設置optimize_coding=TRUE和FALSE進行壓縮,想達到接近的圖片質量(用Photoshop 放大到像素級逐塊對比),FALSE時的圖片大小大約是TRUE時的5-10倍。換句話說,如果我們想在FALSE和TRUE時壓縮成相同大小的JPEG 圖片,FALSE的品質將大大遜色於TRUE的(雖然品質很難量化,但我們不妨說成是差5-10倍)。

什麼意思呢?意思就是現在設備發達啦,是時候將optimize_coding設置成true了,但是問題來了,Android系統代碼對於APP來說修改不了,我們有沒有什麼辦法將這個參數進行設置呢?答案肯定是有的,那就是自己使用自己的so庫,不用系統的不就完了。

分析源碼Android系統集成了這個庫,但是參數沒設置好,咱也不明白為啥Android就是不改…

那我們就從Bitmap.compress這個方法說起

<code>public boolean compress (Bitmap.CompressFormat format, int quality, OutputStream stream)
/<code>

這個方法進行質量壓縮,而且可能失去alpha精度

<code>public boolean compress(CompressFormat format, int quality, OutputStream stream) {
checkRecycled("Can't compress a recycled bitmap");
// do explicit check before calling the native method
if (stream == null) {
throw new NullPointerException();
}
if (quality < 0 || quality > 100) {
throw new IllegalArgumentException("quality must be 0..100");
}
return nativeCompress(mNativeBitmap, format.nativeInt, quality,
stream, new byte[WORKING_COMPRESS_STORAGE]);
}
/<code>

我們看到quality只能是0-100的值

<code>static bool Bitmap_compress(JNIEnv* env, jobject clazz, SkBitmap* bitmap,
int format, int quality,
jobject jstream, jbyteArray jstorage) {
SkImageEncoder::Type fm; //創建類型變量
//將java層類型變量轉換成Skia的類型變量
switch (format) {
case kJPEG_JavaEncodeFormat:
fm = SkImageEncoder::kJPEG_Type;
break;
case kPNG_JavaEncodeFormat:
fm = SkImageEncoder::kPNG_Type;
break;
case kWEBP_JavaEncodeFormat:
fm = SkImageEncoder::kWEBP_Type;
break;
default:
return false;
}
//判斷當前bitmap指針是否為空
bool success = false;

if (NULL != bitmap) {
SkAutoLockPixels alp(*bitmap);

if (NULL == bitmap->getPixels()) {
return false;
}

//創建SkWStream變量用於將壓縮後的圖片數據輸出
SkWStream* strm = CreateJavaOutputStreamAdaptor(env, jstream, jstorage);
if (NULL == strm) {
return false;
}
//根據編碼類型,創建SkImageEncoder變量,並調用encodeStream對bitmap
//指針指向的圖片數據進行編碼,完成後釋放資源。
SkImageEncoder* encoder = SkImageEncoder::Create(fm);
if (NULL != encoder) {
success = encoder->encodeStream(strm, *bitmap, quality);
delete encoder;
}
delete strm;
}
return success;
}
/<code>

利用流和byte數組生成SkJavaOutputStream對象

<code>SkWStream* CreateJavaOutputStreamAdaptor(JNIEnv* env, jobject stream, jbyteArray storage) {
static bool gInited;
if (!gInited) {
gInited = true;
}
return new SkJavaOutputStream(env, stream, storage);
}
bool SkImageEncoder::encodeStream(SkWStream* stream, const SkBitmap& bm,
int quality) {
quality = SkMin32(100, SkMax32(0, quality));
return this->onEncode(stream, bm, quality);
}
/<code>

在SkImageEncoder中定義如下:

<code>/**
* Encode bitmap 'bm' in the desired format, writing results to
* stream 'stream', at quality level 'quality' (which can be in
* range 0-100).
*
* This must be overridden by each SkImageEncoder implementation.
*/
virtual bool onEncode(SkWStream* stream, const SkBitmap& bm, int quality) = 0;
/<code>

但是總體來說,Android是使用skia庫的,我們同樣在源碼目錄下也能找到對應位置:

external\\skia

同樣我們觀察一個現象:

就是在SkImageEncoder中定義的onEncode函數,是個virtual的,那我們應該把她所有的實現類都找出來。

<code>class SkKTXImageEncoder : public SkImageEncoder {}
class SkImageEncoder_CG : public SkImageEncoder {}
class SkPNGImageEncoder : public SkImageEncoder {}
class SkWEBPImageEncoder : public SkImageEncoder {}
class SkImageEncoder_WIC : public SkImageEncoder {}
class SkARGBImageEncoder : public SkImageEncoder {}
/<code>

這麼多類實現了這個接口而且他們都有個共同的路徑:

\\external\\skia\\src\\images那我們就看看SkPNGImageEncoder中的onEncode方法是什麼樣子

<code>class SkJPEGImageEncoder : public SkImageEncoder {
protected:
virtual bool onEncode(SkWStream* stream, const SkBitmap& bm, int quality) {
#ifdef TIME_ENCODE
SkAutoTime atm("JPEG Encode");
#endif

SkAutoLockPixels alp(bm);
if (NULL == bm.getPixels()) {

return false;
}

jpeg_compress_struct cinfo;//申請並初始化jpeg壓縮對象,同時要指定錯誤處理器
skjpeg_error_mgr sk_err;// 聲明錯誤處理器,並賦值給jcs.err域
skjpeg_destination_mgr sk_wstream(stream);

// allocate these before set call setjmp
SkAutoMalloc oneRow;
SkAutoLockColors ctLocker;

cinfo.err = jpeg_std_error(&sk_err);
sk_err.error_exit = skjpeg_error_exit;
if (setjmp(sk_err.fJmpBuf)) {
return false;
}

// Keep after setjmp or mark volatile.
const WriteScanline writer = ChooseWriter(bm);
if (NULL == writer) {
return false;
}

jpeg_create_compress(&cinfo);
cinfo.dest = &sk_wstream;
cinfo.image_width = bm.width();
cinfo.image_height = bm.height();
cinfo.input_components = 3;
#ifdef WE_CONVERT_TO_YUV
cinfo.in_color_space = JCS_YCbCr;
#else
cinfo.in_color_space = JCS_RGB;
#endif
cinfo.input_gamma = 1;
/**
jpeg_set_defaults函數一定要等設置好圖像寬、高、色彩通道數計色彩空間四個參數後才能調用,
因為這個函數要用到這四個值,調用jpeg_set_defaults函數後,jpeglib庫採用默認的設置對圖像進行壓縮,
如果需要改變設置,如壓縮質量,調用這個函數後,可以調用其它設置函數,如jpeg_set_quality函數。

其實圖像壓縮時有好多參數可以設置,但大部分我們都用不著設置,只需調用jpeg_set_defaults函數值為默認值即可。
*/
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);//給cinfo中設置quality
#ifdef DCT_IFAST_SUPPORTED
cinfo.dct_method = JDCT_IFAST;
#endif


/*
上面的工作準備完成後,就可以壓縮了,壓縮過程非常簡單,首先調用jpeg_start_compress,然後可以對每一行進行壓縮,
也可以對若干行進行壓縮,甚至可以對整個的圖像進行一次壓縮,壓縮完成後,記得要調用jpeg_finish_compress函數
*/

jpeg_start_compress(&cinfo, TRUE);//設置開始壓縮的必要天劍

const int width = bm.width();
uint8_t* oneRowP = (uint8_t*)oneRow.reset(width * 3);

const SkPMColor* colors = ctLocker.lockColors(bm);
const void* srcRow = bm.getPixels();
//下面是對每一行進行壓縮
while (cinfo.next_scanline < cinfo.image_height) {
JSAMPROW row_pointer[1]; //一行位圖

writer(oneRowP, srcRow, width, colors);
row_pointer[0] = oneRowP;
(void) jpeg_write_scanlines(&cinfo, row_pointer, 1);//向壓縮容器中寫數據
srcRow = (const void*)((const char*)srcRow + bm.rowBytes());
}
//最後就是釋放壓縮工作過程中所申請的資源了,主要就是jpeg壓縮對象

jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);

return true;
}
};
/<code>

裡面牽扯到JCS_RGB,JCS_YCbCr

Definition

source

<code>00206 typedef enum {
00207 JCS_UNKNOWN, /* error/unspecified */
00208 JCS_GRAYSCALE, /* monochrome */
00209 JCS_RGB, /* red/green/blue */
00210 JCS_YCbCr, /* Y/Cb/Cr (also known as YUV) */
00211 JCS_CMYK, /* C/M/Y/K */
00212 JCS_YCCK /* Y/Cb/Cr/K */
00213 } J_COLOR_SPACE;
//Definition at line 206 of file jpeglib.h.
/<code>

而且我們看出來裡面使用:

<code>00217 typedef enum {
00218 JDCT_ISLOW, /* slow but accurate integer algorithm */
00219 JDCT_IFAST, /* faster, less accurate integer method */
00220 JDCT_FLOAT /* floating-point: accurate, fast on fast HW */
00221 } J_DCT_METHOD;
/<code>

一種快但是不精準的方法進行變換。按照網上有關基友的說法:

1.Skia默認先將圖片轉為YUV444格式,再進行編碼(WE_CONVERT_TO_YUV宏默認打開狀態,否則就是先轉為RGB888格式,再傳入Jpeg編碼時轉YUV)2.默認使用JDCT_IFAST方法做傅立葉變換,很明顯會造成一定的圖片質量損失(即使quality設成100也存在,是計算精度的問題)jpeg_start_compress:

看文檔還是這隻一些安全檢查所需要的參數為壓縮做準備

<code>/*
* Compression initialization.
* Before calling this, all parameters and a data destination must be set up.
*
* We require a write_all_tables parameter as a failsafe check when writing
* multiple datastreams from the same compression object. Since prior runs
* will have left all the tables marked sent_table=TRUE, a subsequent run
* would emit an abbreviated stream (no tables) by default. This may be what
* is wanted, but for safety's sake it should not be the default behavior:
* programmers should have to make a deliberate choice to emit abbreviated
* images. Therefore the documentation and examples should encourage people
* to pass write_all_tables=TRUE; then it will take active thought to do the
* wrong thing.
*/

jpeg_start_compress (j_compress_ptr cinfo, boolean write_all_tables)
{
if (cinfo->global_state != CSTATE_START)
ERREXIT1(cinfo, JERR_BAD_STATE, cinfo->global_state);

if (write_all_tables)
jpeg_suppress_tables(cinfo, FALSE); /* mark all tables to be written */

/* (Re)initialize error mgr and destination modules */
(*cinfo->err->reset_error_mgr) ((j_common_ptr) cinfo);
(*cinfo->dest->init_destination) (cinfo);
/* Perform master selection of active modules */
jinit_compress_master(cinfo);
/* Set up for the first pass */
(*cinfo->master->prepare_for_pass) (cinfo);
/* Ready for application to drive first pass through jpeg_write_scanlines
* or jpeg_write_raw_data.
*/
cinfo->next_scanline = 0;
cinfo->global_state = (cinfo->raw_data_in ? CSTATE_RAW_OK : CSTATE_SCANNING);
}
/<code>

至此壓縮就完成了,我們也就看出Android系統是通過libjpeg進行壓縮的。

但是Android集成的libjpeg和我們使用的也有一些不一樣,所以我建議使用自己編譯so進行操作,這樣可以根據我們需求來定製參數達到更好的符合我們項目的目的。

小結:

我們已經知道Android系統中是使用skia庫進行壓縮的,skia庫中又是使用其他開元庫進行壓縮對於jpg的壓縮就是使用libjpeg這個庫。

2、Android中有圖片所佔內存因素分析

有個大仙分析的很好借用成果

我們經常因為圖片太大導致oom,但是很多小夥伴,只是借鑑網上的建議和方法,並不知道原因,那麼我們接下來就大致分析一下圖片在Android中加載由那些因素決定呢?

getByteCount():表示存儲bitmap像素所佔內存

<code>public final int getByteCount() {
return getRowBytes() * getHeight();
}
/<code>

getAllocationByteCount():返回bitmap所佔像素已經分配的大小

如果一個bitmap被複用更小尺寸的bitmap編碼,或者手工重新配置。那麼實際尺寸可能偏小。具體看reconfigure(int, int, Config), setWidth(int), setHeight(int), setConfig(Bitmap.Config), and BitmapFactory.Options.inBitmap.如果不牽扯複用否是新產生的,納悶就和getByteContent()相同。

這個值在bitmap生命週期內不會改變

所以從代碼看mBuffer.length就是緩衝區真是長度

<code>public final int getAllocationByteCount() {
if (mBuffer == null) {
//mBuffer 代表存儲 Bitmap 像素數據的字節數組。
return getByteCount();
}
return mBuffer.length;
}
/<code>

然後我們看看佔用內存如何計算的

Bitamp 佔用內存大小 = 寬度像素 x (inTargetDensity / inDensity) x 高度像素 x (inTargetDensity / inDensity)x 一個像素所佔的內存

那麼一個像素佔用的內存多大呢?這個就和配置的規格有關係

SkBitmap.cpp

<code>static int SkColorTypeBytesPerPixel(SkColorType ct) {
static const uint8_t gSize[] = {
0, // Unknown
1, // Alpha_8
2, // RGB_565
2, // ARGB_4444
4, // RGBA_8888
4, // BGRA_8888
1, // kIndex_8
};
/<code>

常用的就是RGBA_8888也就是一個像素佔用四個字節大小

  • ARGB_8888:每個像素佔四個字節,A、R、G、B 分量各佔8位,是 Android 的默認設置;
  • RGB_565:每個像素佔兩個字節,R分量佔5位,G分量佔6位,B分量佔5位;
  • ARGB_4444:每個像素佔兩個字節,A、R、G、B分量各佔4位,成像效果比較差;
  • Alpha_8: 只保存透明度,共8位,1字節;

於此同時呢,在BitmapFactory 的內部類 Options 有兩個成員變量 inDensity 和 inTargetDensity其中inDensity 就 Bitmap 的像素密度,也就是 Bitmap 的成員變量 mDensity默認是設備屏幕的像素密度,可以通過 Bitmap#setDensity(int) 設置inTargetDensity 是圖片的目標像素密度,在加載圖片時就是 drawable 目錄的像素密度當資源加載的時候會進行這兩個值的初始化調用的是 BitmapFactory#decodeResource 方法,內部調用的是 decodeResourceStream 方法

<code>public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
//實際上,我們這裡的opts是null的,所以在這裡初始化。
/**
public Options() {
inDither = false;
inScaled = true;
inPremultiplied = true;
}
*/
if (opts == null) {
opts = new Options();
}

if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;//這裡density的值如果對應資源目錄為hdpi的話,就是240

}
}
//請注意,inTargetDensity就是當前的顯示密度,比如三星s6時就是640
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}

return decodeStream(is, pad, opts);
}
/<code>

會根據設備屏幕像素密度到對應 drawable 目錄去尋找圖片,這個時候 inTargetDensity/inDensity = 1,圖片不會做縮放,寬度和高度就是圖片原始的像素規格,如果沒有找到,會到其他 drawable 目錄去找,這個時候 drawable 的屏幕像素密度就是 inTargetDensity,會根據 inTargetDensity/inDensity 的比例對圖片的寬度和高度進行縮放。

所以歸結上面影響圖片內存的原因有:

  • 色彩格式,前面我們已經提到,如果是 ARGB8888 那麼就是一個像素4個字節,如果是 RGB565 那就是2個字節
  • 原始文件存放的資源目錄
  • 目標屏幕的密度
  • 圖片本身的大小

3、圖片的幾種壓縮辦法

  • 質量壓縮注意這種方式,是通過改變alpha通道,改變色彩度等方式達到壓縮圖片的目的,壓縮使得存儲大小變小,但是並不改變加載到內存的大小,也就是說,如果你從1M壓縮到了1K,解壓縮出來在內存中大小還是1M。而且有個很坑的問題,就是如果設置quality=100,這個圖片存儲大小會增大,而且會小幅度失真。具體原因,我在上面分析源碼的時候還沒仔細研究,初步判斷可能是利用傅里葉變換導致。public boolean compress (Bitmap.CompressFormat format, int quality, OutputStream stream)
  • 尺寸壓縮尺寸壓縮在使用的時候BitmapFactory.Options 類型的參數當置 BitmapFactory.Options.inJustDecodeBounds=true只讀取圖片首行寬高等信息,並不會將圖片加載到內存中。設置 BitmapFactory.Options 的 inSampleSize 屬性可以真實的壓縮 Bitmap 佔用的內存,加載更小內存的 Bitmap。設置 inSampleSize 之後,Bitmap 的寬、高都會縮小 inSampleSize 倍。inSampleSize 比1小的話會被當做1,任何 inSampleSize 的值會被取接近2的冪值
  • 色彩模式壓縮也就是我們在色彩模式上進行變換,通過設置通過 BitmapFactory.Options.inPreferredConfig改變不同的色彩模式,使得每個像素大小改變,從而圖片大小改變
  • Matrix 矩陣變換使用:
<code>int bitmapWidth = bitmap.getWidth();
int bitmapHeight = bitmap.getHeight();
Matrix matrix = new Matrix();
float rate = computeScaleRate(bitmapWidth, bitmapHeight);
matrix.postScale(rate, rate);
Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, bitmapWidth, bitmapHeight, matrix, true);

/<code>

其實這個操作並不是節省內存,他只是結合我們對尺寸壓縮進行補充,我們進行尺寸壓縮之後難免不會滿足我們對尺寸的要求,所以我們就藉助Matrix進行矩陣變換,改變圖片的大小。

Bitmap#createScaledBitmap這個也是和Matrix一個道理,都是進行縮放。不改變內存。

4、圖片壓縮的最終解決方案

我們通過上面的總結我們歸納出,圖片的壓縮目的有兩種:

  • 壓縮內存,防止產生OOM壓縮存儲空間,目的節約空間,但是解壓到內存中大小不變。還是原來沒有壓縮圖片時候的大小。那麼我們應該怎麼壓縮才合理呢,其實這個需要根據需求來定,可能有人就會說我說的是廢話,但是事實如此。我提供一些建議:
  • 使用libjpeg開源項目,不使用Android集成的libjpeg,因為我們可以根據需要修改參數,更符合我們項目的效果。
  • 合理通過尺寸變換和矩陣變換在內存上優化。
  • 對不同屏幕分辨率的機型壓縮進行壓縮的程度不一樣。
  • 那麼我們就開始我們比較難的一個環節就是集成開源庫。

5、編譯libjpeg生成so庫

libjpeg項目下載地址

首先確保我們安裝了ndk環境,不管是Linux還是windows還是macOs都可以編譯,只要我們有NDK,我們必須知道我們NDK能夠使用,並且可以調用到我們ndk裡面的工具,這就要求我們要配置環境變量,當然Linux和windows不一樣,macOS由於我這種窮逼肯定買不起所以我也布吉島怎麼弄。但是思想就是要能用到ndk工具

windows是在我們環境變量中進行配置

Linux呢

<code>echo "export ANDROID_HOME='Your android ndk path'" >> ~/.bash_profile
source ~/.bash_profile
/<code>

當然Linux還可以寫.sh來個腳本豈不更好

<code>NDK=/opt/ndk/android-ndk-r12b/
PLATFORM=$NDK/platforms/android-15/arch-arm/
PREBUILT=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86/
CC=$PREBUILT/bin/arm-linux-androideabi-gcc
./configure --prefix=/home/linc/jpeg-9b/jni/dist --host=arm CC="$CC --sysroot=$PLATFORM"
/<code>

最執行寫的.sh

這個腳本是根據config文件寫的,那裡面有我們需要的參數還有註釋,所以我們要能看懂那個才可以。一般情況出了問題我們在研究那個吧引薦大牛方法

構建libjpeg-turbo.so

<code>cd ../libjpeg-turbo-android/libjpeg-turbo/jni
ndk-build APP_ABI=armeabi-v7a,armeabi
/<code>

這個時候就可以得到libjpegpi.so在…/libjpeg-turbo-android/libjpeg-turbo/libs/armeabi和armeabi-v7a目錄下

複製我們的libjpegpi.so到 …/bither-android-lib/libjpeg-turbo-android/use-libjpeg-turbo-android/jni

<code>cd ../bither-android-lib/libjpeg-turbo-android/use-libjpeg-turbo-android/jni
ndk-build
/<code>

得到 libjpegpi.so and libpijni.so

jni使用的時候一定java的類名要和jni裡面方法前面的單詞要對上

<code> static {

System.loadLibrary("jpegpi");

System.loadLibrary("pijni");

}
/<code>

所以如果不改項目的話類名必須為com.pi.common.util.NativeUtil

6、庫函數的介紹

<code>net.bither.util.NativeUtil:

package net.bither.util;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;


import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.media.ExifInterface;
import android.util.Log;

public class NativeUtil {
private static String Tag = NativeUtil.class.getSimpleName();

private static int DEFAULT_QUALITY = 95;

/**
* @Description: JNI基本壓縮
* @param bit
* bitmap對象
* @param fileName
* 指定保存目錄名
* @param optimize
* 是否採用哈弗曼表數據計算 品質相差5-10倍
* @author XiaoSai
* @date 2016年3月23日 下午6:32:49
* @version V1.0.0
*/
public static void compressBitmap(Bitmap bit, String fileName, boolean optimize) {
saveBitmap(bit, DEFAULT_QUALITY, fileName, optimize);
}

/**
* @Description: 通過JNI圖片壓縮把Bitmap保存到指定目錄
* @param image
* bitmap對象
* @param filePath
* 要保存的指定目錄
* @author XiaoSai
* @date 2016年3月23日 下午6:28:15
* @version V1.0.0
*/
public static void compressBitmap(Bitmap image, String filePath) {
// 最大圖片大小 150KB

int maxSize = 150;
// 獲取尺寸壓縮倍數
int ratio = NativeUtil.getRatioSize(image.getWidth(),image.getHeight());
// 壓縮Bitmap到對應尺寸
Bitmap result = Bitmap.createBitmap(image.getWidth() / ratio,image.getHeight() / ratio,Config.ARGB_8888);
Canvas canvas = new Canvas(result);
Rect rect = new Rect(0, 0, image.getWidth() / ratio, image.getHeight() / ratio);
canvas.drawBitmap(image,null,rect,null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 質量壓縮方法,這裡100表示不壓縮,把壓縮後的數據存放到baos中
int options = 100;
result.compress(Bitmap.CompressFormat.JPEG, options, baos);
// 循環判斷如果壓縮後圖片是否大於100kb,大於繼續壓縮
while (baos.toByteArray().length / 1024 > maxSize) {
// 重置baos即清空baos
baos.reset();
// 每次都減少10
options -= 10;
// 這裡壓縮options%,把壓縮後的數據存放到baos中
result.compress(Bitmap.CompressFormat.JPEG, options, baos);
}
// JNI保存圖片到SD卡 這個關鍵
NativeUtil.saveBitmap(result, options, filePath, true);
// 釋放Bitmap
if (!result.isRecycled()) {
result.recycle();
}
}

/**
* @Description: 通過JNI圖片壓縮把Bitmap保存到指定目錄
* @param curFilePath
* 當前圖片文件地址
* @param targetFilePath
* 要保存的圖片文件地址
* @author XiaoSai
* @date 2016年9月28日 下午17:43:15

* @version V1.0.0
*/
public static void compressBitmap(String curFilePath, String targetFilePath,int maxSize) {
//根據地址獲取bitmap
Bitmap result = getBitmapFromFile(curFilePath);
if(result==null){
Log.i(Tag,"result is null");
return;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 質量壓縮方法,這裡100表示不壓縮,把壓縮後的數據存放到baos中
int quality = 100;
result.compress(Bitmap.CompressFormat.JPEG, quality, baos);
// 循環判斷如果壓縮後圖片是否大於100kb,大於繼續壓縮
while (baos.toByteArray().length / 1024 > maxSize) {
// 重置baos即清空baos
baos.reset();
// 每次都減少10
quality -= 10;
// 這裡壓縮quality,把壓縮後的數據存放到baos中
result.compress(Bitmap.CompressFormat.JPEG, quality, baos);
}
// JNI保存圖片到SD卡 這個關鍵
NativeUtil.saveBitmap(result, quality, targetFilePath, true);
// 釋放Bitmap
if (!result.isRecycled()) {
result.recycle();
}

}

/**
* 計算縮放比
* @param bitWidth 當前圖片寬度
* @param bitHeight 當前圖片高度
* @return int 縮放比
* @author XiaoSai
* @date 2016年3月21日 下午3:03:38
* @version V1.0.0

*/
public static int getRatioSize(int bitWidth, int bitHeight) {
// 圖片最大分辨率
int imageHeight = 1280;
int imageWidth = 960;
// 縮放比
int ratio = 1;
// 縮放比,由於是固定比例縮放,只用高或者寬其中一個數據進行計算即可
if (bitWidth > bitHeight && bitWidth > imageWidth) {
// 如果圖片寬度比高度大,以寬度為基準
ratio = bitWidth / imageWidth;
} else if (bitWidth < bitHeight && bitHeight > imageHeight) {
// 如果圖片高度比寬度大,以高度為基準
ratio = bitHeight / imageHeight;
}
// 最小比率為1
if (ratio <= 0)
ratio = 1;
return ratio;
}

/**
* 通過文件路徑讀獲取Bitmap防止OOM以及解決圖片旋轉問題
* @param filePath
* @return
*/
public static Bitmap getBitmapFromFile(String filePath){
BitmapFactory.Options newOpts = new BitmapFactory.Options();
newOpts.inJustDecodeBounds = true;//只讀邊,不讀內容
BitmapFactory.decodeFile(filePath, newOpts);
int w = newOpts.outWidth;
int h = newOpts.outHeight;
// 獲取尺寸壓縮倍數
newOpts.inSampleSize = NativeUtil.getRatioSize(w,h);
newOpts.inJustDecodeBounds = false;//讀取所有內容
newOpts.inDither = false;
newOpts.inPurgeable=true;//不採用抖動解碼
newOpts.inInputShareable=true;//表示空間不夠可以被釋放,在5.0後被釋放

// newOpts.inTempStorage = new byte[32 * 1024];
Bitmap bitmap = null;
FileInputStream fs = null;
try {
fs = new FileInputStream(new File(filePath));
} catch (FileNotFoundException e) {
Log.i(Tag,"bitmap :"+e.getStackTrace());
e.printStackTrace();
}
try {
if(fs!=null){
bitmap = BitmapFactory.decodeFileDescriptor(fs.getFD(),null,newOpts);

//旋轉圖片
int photoDegree = readPictureDegree(filePath);
if(photoDegree != 0){
Matrix matrix = new Matrix();
matrix.postRotate(photoDegree);
// 創建新的圖片
bitmap = Bitmap.createBitmap(bitmap, 0, 0,
bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
}else{
Log.i(Tag,"fs :null");
}
} catch (IOException e) {
e.printStackTrace();
} finally{
if(fs!=null) {
try {
fs.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return bitmap;
}

/**
*
* 讀取圖片屬性:旋轉的角度
* @param path 圖片絕對路徑
* @return degree旋轉的角度
*/

public static int readPictureDegree(String path) {

int degree = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
} catch (IOException e) {
e.printStackTrace();
}
return degree;
}

/**
* 調用native方法
* @Description:函數描述
* @param bit
* @param quality
* @param fileName
* @param optimize
* @author XiaoSai
* @date 2016年3月23日 下午6:36:46
* @version V1.0.0
*/
private static void saveBitmap(Bitmap bit, int quality, String fileName, boolean optimize) {
compressBitmap(bit, bit.getWidth(), bit.getHeight(), quality, fileName.getBytes(), optimize);
}

/**
* 調用底層 bitherlibjni.c中的方法
* @Description:函數描述
* @param bit
* @param w
* @param h
* @param quality
* @param fileNameBytes
* @param optimize
* @return

* @author XiaoSai
* @date 2016年3月23日 下午6:35:53
* @version V1.0.0
*/
private static native String compressBitmap(Bitmap bit, int w, int h, int quality, byte[] fileNameBytes,
boolean optimize);
/**
* 加載lib下兩個so文件
*/
static {
System.loadLibrary("jpegbither");
System.loadLibrary("bitherjni");
}

}
/<code>

所以我們最後的核心就是使用saveBitmap就會將圖片壓縮並且保存在sd卡上。而且我們獲取圖片的時候也對內存做了判斷,防止產生OOM

7、壓縮結果

下面第一張圖5M,第二張圖是140k,但是我截圖看上去效果差不多。看下效果:

使用libjpeg進行圖片壓縮

使用libjpeg進行圖片壓縮

原創作者:我叫王菜鳥,原文鏈接:https://www.jianshu.com/p/072b6defd938


分享到:


相關文章: