用過ios手機的同學應該很明顯感覺到,ios拍照1M的圖片要比安卓拍照排出來的5M的圖片還要清晰。這是為什么呢?
這得了解android底層是如何對圖片進行處理的.
當時谷歌開發Android的時候,考慮了大部分手機的配置並沒有那么高,所以對圖片處理是使用的Skia這個開源庫。當然這個庫的底層還是是用的jpeg對圖片進行壓縮處理。但是為了能夠適配低端的手機(這里的低端是指以前的硬件配置不高的手機,CPU和內存在手機上都非常吃緊 性能差),由於哈夫曼算法非常吃CPU,被迫用了其他的算法。所以Skia在進行圖片處理並沒有去使用壓縮圖像過程中基於圖像數據計算哈弗曼表(關於圖片壓縮中的哈弗曼表,請自行查閱相關資料),但是解碼還是保留了哈夫曼算法。這就導致了圖片處理后文件變大了。
我們使用微信發圖片的時候,會發現發出去的圖片明顯比原圖小很大,但是效果好像差不多,那又是為什么呢,經過了怎樣的壓縮呢,其實他們是用了哈夫曼算法進行圖片壓縮;接下來我們就用這算法進行圖片壓縮:
仿微信終級壓縮
1.下載JPEG引擎使用的庫—libjpeg庫
libjpeg是一個被廣泛使用的JPEG解碼、JPEG編碼和其他的JPEG功能的實現庫.說它使用廣泛,是因為它跨了很多平台。比如Linux平台、JDK、Android和其他庫如tess-two等等。libjpeg庫下載地址;
2.編譯android中libjpeg庫使用庫
編譯android中libjpeg庫使用庫,這里我暫時先不做詳細的請解,后期我會做一個ndk開發專題進行詳細請解,編譯后生成的libjpegbither.so和.h頭文件,看如下圖:
3.導入libjpeg庫libjpegbither.so及頭文件
在項目文件夾新建jni文件夾,把剛才生成的libjpegbither.so及頭文件放在此文件夾下;
4.新建BitmapCompressUtils
-
寫一個native方法,調用c方法
/** * 調用底層 bitherlibjni.c中的方法 * * @param bit * @param w * @param h * @param quality * @param fileNameBytes * @param optimize * @return * @Description:函數描述 */ public static native String compressBitmap(Bitmap bit, int w, int h, int quality, byte[] fileNameBytes, boolean optimize);
引入庫lib下二個so文件
/** * 加載lib下兩個so文件 */ static { System.loadLibrary("jpegbither"); System.loadLibrary("bitherjni"); }
寫一個方法,調用native方法,便於java層調用
/** * @param image bitmap對象 * @param filePath 要保存的指定目錄 * @Description: 通過JNI圖片壓縮把Bitmap保存到指定目錄 */ public static void compressBitmap(Bitmap image, String filePath) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 質量壓縮方法,這里100表示不壓縮,把壓縮后的數據存放到baos中 int options = 20; // JNI調用保存圖片到SD卡 這個關鍵 NativeUtil.saveBitmap(image, options, filePath, true); }
4.編寫bitherjni.cpp
#include "bitherlibjni.h" #include <string.h> #include <android/bitmap.h> #include <android/log.h> #include <stdio.h> #include <setjmp.h> #include <math.h> #include <stdint.h> #include <time.h> //統一編譯方式 extern "C" { #include "jpeg/jpeglib.h" #include "jpeg/cdjpeg.h" /* Common decls for cjpeg/djpeg applications */ #include "jpeg/jversion.h" /* for version message */ #include "jpeg/android/config.h" } #define LOG_TAG "jni" #define LOGW(...) __android_log_write(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) #define true 1 #define false 0 typedef uint8_t BYTE; char *error; struct my_error_mgr { struct jpeg_error_mgr pub; jmp_buf setjmp_buffer; }; typedef struct my_error_mgr * my_error_ptr; METHODDEF(void) my_error_exit (j_common_ptr cinfo) { my_error_ptr myerr = (my_error_ptr) cinfo->err; (*cinfo->err->output_message) (cinfo); error=(char*)myerr->pub.jpeg_message_table[myerr- >pub.msg_code]; LOGE("jpeg_message_table[%d]:%s", myerr- >pub.msg_code,myerr->pub.jpeg_message_table[myerr- >pub.msg_code]); // LOGE("addon_message_table:%s", myerr->pub.addon_message_table); // LOGE("SIZEOF:%d",myerr->pub.msg_parm.i[0]); // LOGE("sizeof:%d",myerr->pub.msg_parm.i[1]); longjmp(myerr->setjmp_buffer, 1); } int generateJPEG(BYTE* data, int w, int h, int quality, const char* outfilename, jboolean optimize) { //jpeg的結構體,保存的比如寬、高、位深、圖片格式等信息,相當於java的類 struct jpeg_compress_struct jcs; //當讀完整個文件的時候就會回調my_error_exit這個退出方法。setjmp是一個系統級函數,是一個回調。 struct my_error_mgr jem; jcs.err = jpeg_std_error(&jem.pub); jem.pub.error_exit = my_error_exit; if (setjmp(jem.setjmp_buffer)) { return 0; } //初始化jsc結構體 jpeg_create_compress(&jcs); //打開輸出文件 wb:可寫byte FILE* f = fopen(outfilename, "wb"); if (f == NULL) { return 0; } //設置結構體的文件路徑 jpeg_stdio_dest(&jcs, f); jcs.image_width = w;//設置寬高 jcs.image_height = h; // if (optimize) { // LOGI("optimize==ture"); // } else { // LOGI("optimize==false"); // } //看源碼注釋,設置哈夫曼編碼:/* TRUE=arithmetic coding, FALSE=Huffman */ jcs.arith_code = false; int nComponent = 3; /* 顏色的組成 rgb,三個 # of color components in input image */ jcs.input_components = nComponent; //設置結構體的顏色空間為rgb jcs.in_color_space = JCS_RGB; // if (nComponent == 1) // jcs.in_color_space = JCS_GRAYSCALE; // else // jcs.in_color_space = JCS_RGB; //全部設置默認參數/* Default parameter setup for compression */ jpeg_set_defaults(&jcs); //是否采用哈弗曼表數據計算 品質相差5-10倍 jcs.optimize_coding = optimize; //設置質量 jpeg_set_quality(&jcs, quality, true); //開始壓縮,(是否寫入全部像素) jpeg_start_compress(&jcs, TRUE); JSAMPROW row_pointer[1]; int row_stride; //一行的rgb數量 row_stride = jcs.image_width * nComponent; //一行一行遍歷 while (jcs.next_scanline < jcs.image_height) { //得到一行的首地址 row_pointer[0] = &data[jcs.next_scanline * row_stride]; //此方法會將jcs.next_scanline加1 jpeg_write_scanlines(&jcs, row_pointer, 1);//row_pointer就是一行的首地址,1:寫入的行數 } jpeg_finish_compress(&jcs);//結束 jpeg_destroy_compress(&jcs);//銷毀 回收內存 fclose(f);//關閉文件 return 1; } /** * byte數組轉C的字符串 */ char* jstrinTostring(JNIEnv* env, jbyteArray barr) { char* rtn = NULL; jsize alen = env->GetArrayLength( barr); jbyte* ba = env->GetByteArrayElements( barr, 0); if (alen > 0) { rtn = (char*) malloc(alen + 1); memcpy(rtn, ba, alen); rtn[alen] = 0; } env->ReleaseByteArrayElements( barr, ba, 0); return rtn; } jstring Java_net_bither_util_BitmapCompressUtils_compressBitmap(JNIEnv* env, jclass thiz, jobject bitmapcolor, int w, int h, int quality, jbyteArray fileNameStr, jboolean optimize) { BYTE *pixelscolor; //1.將bitmap里面的所有像素信息讀取出來,並轉換成RGB數據,保存到二維byte數組里面 //處理bitmap圖形信息方法1 鎖定畫布 AndroidBitmap_lockPixels(env,bitmapcolor,(void**)&pixelscolor); //2.解析每一個像素點里面的rgb值(去掉alpha值),保存到一維數組data里面 BYTE *data; BYTE r,g,b; data = (BYTE*)malloc(w*h*3);//每一個像素都有三個信息RGB BYTE *tmpdata; tmpdata = data;//臨時保存data的首地址 int i=0,j=0; int color; for (i = 0; i < h; ++i) { for (j = 0; j < w; ++j) { //解決掉alpha //獲取二維數組的每一個像素信息(四個部分a/r/g/b)的首地址 color = *((int *)pixelscolor);//通過地址取值 //0~255: // a = ((color & 0xFF000000) >> 24); r = ((color & 0x00FF0000) >> 16); g = ((color & 0x0000FF00) >> 8); b = ((color & 0x000000FF)); //改值!!!----保存到data數據里面 *data = b; *(data+1) = g; *(data+2) = r; data = data + 3; //一個像素包括argb四個值,每+4就是取下一個像素點 pixelscolor += 4; } } //處理bitmap圖形信息方法2 解鎖 AndroidBitmap_unlockPixels(env,bitmapcolor); char* fileName = jstrinTostring(env,fileNameStr); //調用libjpeg核心方法實現壓縮 int resultCode = generateJPEG(tmpdata,w,h,quality,fileName,optimize); if(resultCode ==0){ jstring result = env->NewStringUTF("-1"); return result; } return env->NewStringUTF("1"); }
JavanetbitherutilBitmapCompressUtils_compressBitmap就是java調用的方法,通過此方法就可以進行圖片壓縮,c代碼我就不再講解,注釋寫得很清楚了;
接下我再講一下另外幾個方案進行圖片壓縮;
質量壓縮
質量壓縮,這個只是降低了圖片的質量,但是像素是不會減小的
/** 質量壓縮, * @param bitmap 要壓縮的圖片 * @param file //壓縮的圖片保存地址 * Hint to the compressor, 0-100. 0 meaning compress for small size, 100 meaning compress for max quality. Some * formats, like PNG which is lossless, will ignore the quality setting * quality (0-100) 100是不壓縮,值越小,壓縮得越厲害 */ public static void qualityCompressBitmap(Bitmap bitmap,File file){ //字節數組輸出流 ByteArrayOutputStream stream =new ByteArrayOutputStream(); int quality=20; //圖片壓縮后把數據放在stream中 bitmap.compress(Bitmap.CompressFormat.JPEG,quality, stream); try { FileOutputStream fileOutputStream=new FileOutputStream(file); //不斷把stream的數據寫文件輸出流中去 fileOutputStream.write(stream.toByteArray()); fileOutputStream.flush(); fileOutputStream.close(); } catch (Exception e) { e.printStackTrace(); } }
尺寸壓縮
尺寸壓縮,通過縮放圖片的像素,減小圖片占用內存大小,這個比如用於縮略圖,今日頭條文章上顯示的小圖就是這樣實現的;
/**尺寸壓縮 * @param bitmap 要壓縮的圖片 * @param ratio 壓縮比例,值越大,圖片的尺寸就越小 * @param file 壓縮的圖片保存地址 */ public static void sizeCompressBitmap(Bitmap bitmap,int ratio,File file){ if (ratio<=0){ return; } Bitmap result=Bitmap.createBitmap(bitmap.getWidth()/ratio,bitmap.getHeight()/ratio, Bitmap.Config.ARGB_8888); Canvas canvas =new Canvas(); Rect rect=new Rect(0,0,bitmap.getWidth()/ratio,bitmap.getHeight()/ratio); canvas.drawBitmap(bitmap,null,rect,null); ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 把壓縮后的數據存放到baos中 result.compress(Bitmap.CompressFormat.JPEG, 100 ,baos); try { FileOutputStream fos = new FileOutputStream(file); fos.write(baos.toByteArray()); fos.flush(); fos.close(); } catch (Exception e) { e.printStackTrace(); } }
采樣率壓縮
/** 采樣率壓縮 * @param filePath 壓縮圖 * @param file 壓縮的圖片保存地址 */ public static void pixeCompressBitmap(String filePath, File file){ //采樣率,數值越高,圖片像素越低 int inSampleSize=8; BitmapFactory.Options osts=new BitmapFactory.Options(); osts.inSampleSize=inSampleSize; //inJustDecodeBounds設為True時,不會真正加載圖片,而是得到圖片的寬高信息。 osts.inJustDecodeBounds=false; Bitmap bitmap= BitmapFactory.decodeFile(filePath,osts); ByteArrayOutputStream stream =new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG,100,stream); try { if (file.exists()){ file.delete(); }else{ file.createNewFile(); } FileOutputStream fileOutputStream=new FileOutputStream(file); fileOutputStream.write(stream.toByteArray()); fileOutputStream.flush(); fileOutputStream.close(); } catch (Exception e) { e.printStackTrace(); } }
總結:以上幾種方案對圖片進行壓縮,各有優缺點,根據實際場景來選擇方案來進行圖片壓縮.