1. libjpeg介紹
libJPEG庫是一款功能強大的JPEG圖像處理開源庫,它支持將圖像數據壓縮編碼爲JPEG格式和對原有的JPEG圖像解壓縮,Android系統底層處理圖片壓縮就是用得libJPEG庫。但有一點需要注意的是,爲了適配低版本的Android手機,Android系統在內部的壓縮算法並沒有採用普通的哈夫曼(Huffman)算法,因爲哈夫曼算法比較佔CPU,從而導致Android在壓縮的同時保持較高的圖像質量和色彩豐富時,壓縮的性能不是很好。基於此,本文將使用AS的Cmake工具編譯libJPEG-turbo源碼,然後通過JNI/NDK技術編寫採樣哈夫曼算法壓縮接口,以提高在Android中圖片壓縮質量。
注:libjpeg-turbo是針對libjpeg庫的一個優化版本。
1.1 哈夫曼編碼
Haffman編碼是Huffman於1952年提出的一種高效的無損壓縮編碼
方法(注:壓縮20%~90%
),該方法的編碼的碼長是可變的,它完全依據字符出現概率(頻率)來構造異字頭的平均長度最短的碼字,即對於出現概率(頻率)高的信息,編碼的碼長較短;對於出現概率(頻率)高的信息,編碼的碼長較長。在圖像壓縮的應用場景中,Haffman編碼的基本方法是先對圖像數據掃描一遍,計算出各種像素出現的概率,按概率的大小指定不同長度的唯一碼字,由此得到一張該圖像的Haffman碼錶。編碼後的圖像數據記錄的是每個像素的碼字,而碼字與實際像素值的對應關係記錄在碼錶中。
- Haffman樹
假設有n個權值{w1,w2,…,wn},構造一棵有n個葉子結點的二叉樹,每個葉子結點帶權爲wk,每個葉子的路徑長度爲lk,則其中樹的帶權路徑長度WPL=∑(wk*lk)
最小的二叉樹稱爲赫夫曼樹,也稱最優二叉樹。舉個栗子:
該樹的帶權路徑長度:WPL=∑(wklk) = 110+270+315+3*5=210
- Haffman算法原理
假設需要編碼的字符集爲{d1,d2,…dn},各個字符在電文中出現的次數或頻率集合爲{w1,w2,…,wn},以d1,d2,…,dn作爲葉子結點,以w1,w2,…,wn作爲相應葉子結點的權值來構造一棵赫夫曼樹。規定:赫夫曼樹的左分支代表0,右分支代表1,則從根結點到葉子結點所經過的路徑分支組成的0和1的序列便爲該結點對應字符的編碼,這就是Haffman編碼。
舉個栗子:對字符串“BADCADFEED”進行Haffman編碼?
首先,計算每個字母出現的概率A 27%,B 8%,C 15%,D 15%,E 30%,F 5%;
其次,構造一顆哈夫曼樹(左小,右大),並將每個葉子節點的左右權值分別改爲0,1;
第三,將每個字母從根節點到葉子結點所經過的路徑0或1來編碼;
最後,得到字符串的Haffman編碼。
即“BADCADFEED”的Haffman編碼爲“1001010010101001000111100”。
1.2 libjpeg編碼與解碼
(1) 分配和初始化JPEG壓縮對象jpeg_compress_struct
,並設置錯誤處理模塊。
// JPEG壓縮編碼的核心結構體,位於源碼jpeglib.h
// 它包含了各種壓縮參數和指向工作空間的指針(JPEG庫所需內存)等
struct jpeg_compress_struct cinfo;
// JPEG錯誤處理結構體,位於源碼jpeglib.h
struct jpeg_error_mgr jerr;
// 設置錯誤處理對象,以防初始化失敗,比如內存溢出
cinfo.err = jpeg_std_error(&jerr);
// 初始化JPEG壓縮對象(結構體對象)
jpeg_create_compress(&cinfo);
(2) 指定壓縮數據輸出。這裏假設指定一個文件路徑,然後將壓縮後的JPEG數據存儲到該文件中。
// 打開指定路徑的文件
if ((outfile = fopen(filename, "wb")) == NULL) {
fprintf(stderr, "can't open %s\n", filename);
exit(1);
}
// 指定JPEG壓縮數據保存位置
jpeg_stdio_dest(&cinfo, outfile);
(3) 設置壓縮參數。當然,首先我們需要填充輸入圖像的相關信息,比如寬高、顏色空間等。
// 獲取輸入圖像信息
cinfo.image_width = image_width; // 寬度
cinfo.image_height = image_height; // 高度
cinfo.input_components = 3; // 每個像素佔的顏色分量數量
cinfo.in_color_space = JCS_RGB; // 顏色空間,RGB
// 設置壓縮參數
cinfo.optimize_coding = true; // 壓縮優化
cinfo.arith_code = false; // 使用哈夫曼編碼
jpeg_set_defaults(&cinfo);
// 設置壓縮質量,0~100
jpeg_set_quality(&cinfo, quality, true);
(4) 啓動壓縮引擎,並按行處理數據。由於圖像數據在內存中是以字節爲單位按順序存儲的,對於一張尺寸爲wxh圖像來說,它在內存中是按行存儲的,共h行,至於每行佔多少個字節由w和每個像素大小決定。假如這裏有張分辨率爲640x480且顏色空間爲RGB的圖像(每個像素佔三個分量,即R分量、G分量、B分量,每個分量佔1個字節
),那麼,在內存中每行佔640x3=1920字節,共480行,因此這張圖片在內存中總共佔[wx像素)xh]=[(640x3)x480]=921600字節
。
// 開啓壓縮引擎
jpeg_start_compress(&cinfo, TRUE);
extern JSAMPLE *image_buffer; // 存儲要壓縮的圖像數據,按R、G、B分量順序
extern int image_height; // 圖像行數
extern int image_width; // 圖像列數
// 存儲行起始地址
JSAMPROW row_pointer[1];
// 圖像緩衝區中的物理行寬度,其中,對於RGB來說,每個像素佔3個顏色分量
// 每個分量佔一個字節,那麼圖像中一行的寬度爲:(width * 3)
// 即cinfo.image_width * cinfo.input_components
int row_stride = cinfo.image_width * cinfo.input_components;
// 按行讀取圖像數據
// 然後進行壓縮,並存儲到目的地址中
while (cinfo.next_scanline < cinfo.image_height) {
// 從數據源緩存區image_buffer讀取一行數據
// 並將起始地址賦值給row_pointer[0]
row_pointer[0] = &image_buffer[cinfo.next_scanline * row_stride];
// 將image_buffer中的數據寫到JPEG編碼器中
(void)jpeg_write_scanlines(&cinfo, row_pointer, 1);
}
(5) 結束壓縮,釋放資源。
// 停止壓縮
jpeg_finish_compress(&cinfo);
// 關閉文件描述符
fclose(outfile);
// 釋放引擎所佔資源
jpeg_destroy_compress(&cinfo);
2. 解碼JPEG
(1) 分配、初始化JPEG解壓對象
// JPEG解壓結構體
struct jpeg_decompress_struct cinfo;
// 1. 設置程序錯誤處理
// 這裏對錯誤處理做了優化,對標準的error_exit方法做了處理
//
// 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);
// longjmp(myerr->setjmp_buffer, 1);
// }
struct my_error_mgr {
struct jpeg_error_mgr pub; // 錯誤處理結構體
jmp_buf setjmp_buffer; // 異常信息,回調給調用者
};
struct my_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr.pub); // 設置錯誤處理標準程序
jerr.pub.error_exit = my_error_exit; // 使用自定義的error_exit函數
if (setjmp(jerr.setjmp_buffer)) {
jpeg_destroy_decompress(&cinfo);
fclose(infile);
return 0;
}
// 2. 初始化JPEG解壓對象
jpeg_create_decompress(&cinfo);
(2) 指定數據源(待解壓JPEG圖像)
// 打開待解壓的JPEG文件
if ((infile = fopen(filename, "rb")) == NULL) {
fprintf(stderr, "can't open %s\n", filename);
return 0;
}
// 將JPEG文件指定爲數據源
jpeg_stdio_src(&cinfo, infile);
(3) 讀取JPEG圖像文件頭部參數
(void)jpeg_read_header(&cinfo, TRUE);
(4) 設置解壓參數,開始解壓。這裏我們無需改變JPEG圖像文件的頭部信息,因此,不設置解壓參數。
// 開始解壓
(void)jpeg_start_decompress(&cinfo);
(5) 讀取圖像數據存儲到緩存區buffer中。
// 計算圖像存儲在物理內存中每一行佔的大小(字節)
// 即圖像的寬*每個像素所佔分量數
int row_stride = cinfo.output_width * cinfo.output_components;
// 分配存儲解壓數據的緩存區
JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray)
((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1);
// 循環讀取output_height行字節數據,存儲到buffer緩存區中
while (cinfo.output_scanline < cinfo.output_height) {
(void)jpeg_read_scanlines(&cinfo, buffer, 1);
// 將得到的解壓數據作進一步處理
// 這裏如要自己實現,如定義一個函數
// put_scanline_someplace(buffer[0], row_stride);
}
(6) 結束解壓,釋放資源
// 停止解壓
(void)jpeg_finish_decompress(&cinfo);
// 釋放內存資源
jpeg_destroy_decompress(&cinfo);
fclose(infile);
1.3 libjpeg源碼分析
- jpeg_compress_struct結構體
JPEG壓縮編碼的核心結構體,它被聲明在jpeglib.h
頭文件中,其包含了存儲輸入圖像參數信息、壓縮參數以及指向工作空間的指針(JPEG庫所需內存)等。jpeg_compress_struct結構體聲明(部分)如下:
struct jpeg_compress_struct {
struct jpeg_destination_mgr *dest; // 已壓縮數據存儲地址
JDIMENSION image_width; // 輸入圖像寬度,unsigned int類型
JDIMENSION image_height; // 輸入圖像高度
int input_components; // 輸入圖像顏色分量數量
J_COLOR_SPACE in_color_space; // 輸入圖像顏色空間,如JCS_RGB
int data_precision; // 壓縮後圖像數據的位精度
int num_components; // 壓縮後JPEG圖像的顏色分量數量
J_COLOR_SPACE jpeg_color_space; // 壓縮後JPEG圖像的顏色空間
peg_component_info *comp_info;
boolean raw_data_in; // 爲TRUE,表示向下採樣數據
boolean arith_code; // 爲FALSE,表示使用Huffman編碼,否則爲算術編碼
boolean optimize_coding; // 爲TRUE,表示優化熵編碼參數
int smoothing_factor; // 1~100,其中,0表示平滑輸入
J_DCT_METHOD dct_method; // DCT算法選擇器,
JDIMENSION next_scanline; // 逐行掃描圖像時下一行行號,0~(image_h-1)
...
// 與壓縮相關的結構體
struct jpeg_comp_master *master;
struct jpeg_c_main_controller *main;
struct jpeg_c_prep_controller *prep;
struct jpeg_c_coef_controller *coef;
struct jpeg_marker_writer *marker;
struct jpeg_color_converter *cconvert;
struct jpeg_downsampler *downsample;
struct jpeg_forward_dct *fdct;
struct jpeg_entropy_encoder *entropy;
jpeg_scan_info *script_space;
int script_space_size;
};
- jpeg_decompress_struct結構體
該結構體是libjpeg解壓縮JPEG圖像的核心結構體,位於它被聲明在jpeglib.h
頭文件中,其包含了待解壓縮JPEG圖像的基本信息,如圖像的寬高、每個像素所佔顏色分量數目、顏色空間等,同時也包括解壓所需配置的各種參數等等。jpeg_decompress_struct結構體聲明(部分)如下:
struct jpeg_decompress_struct {
jpeg_common_fields; // 與jpeg_compress_strue共用成員變量,包括err等
struct jpeg_source_mgr *src; // 待解壓的壓縮數據源
/** 待解壓JPEG圖像的基本信息
* 通過jpeg_read_header()函數獲取填充
*/
JDIMENSION image_width; // 圖像的寬度
JDIMENSION image_height; // 圖像的高度
int num_components; // 圖像每個像素所佔分量數量
J_COLOR_SPACE jpeg_color_space; // JPEG圖像顏色空間
/* 解壓處理參數,需要調用jpeg_start_decompress()之前設置
* 注意:調用jpeg_read_header()函數後這些參數會被賦予初始值
*/
J_COLOR_SPACE out_color_space; // 輸出顏色空間
unsigned int scale_num, scale_denom; // 縮放圖像factor
double output_gamma; /* image gamma wanted in output */
boolean buffered_image; /* TRUE=multiple output passes */
boolean raw_data_out; /* TRUE=downsampled data wanted */
J_DCT_METHOD dct_method; // IDCT算法選擇器
boolean do_fancy_upsampling; /* TRUE=apply fancy upsampling */
boolean do_block_smoothing; /* TRUE=apply interblock smoothing */
boolean quantize_colors; /* TRUE=colormapped output wanted */
...
/* 描述輸入待解壓圖像的基本信息,當調用jpeg_start_decompress()
* 時,會被自動計算賦值,當然,我們也在調用jpeg_start_decompress()
* 之前通過調用jpeg_calc_output_dimensions()
*/
JDIMENSION output_width; // 輸出圖像寬度
JDIMENSION output_height; // 輸出圖像高度
int out_color_components; // out_color_components中的顏色分量數目
int output_components; // 顏色分量數目
// 從jpeg_read_scanlines()中讀取下一個掃描行的行索引
// 大小爲 0 .. output_height-1
JDIMENSION output_scanline;
...
// 與解壓縮相關的結構體對象
struct jpeg_decomp_master *master;
struct jpeg_d_main_controller *main;
struct jpeg_d_coef_controller *coef;
struct jpeg_d_post_controller *post;
struct jpeg_input_controller *inputctl;
struct jpeg_marker_reader *marker;
struct jpeg_entropy_decoder *entropy;
struct jpeg_inverse_dct *idct;
struct jpeg_upsampler *upsample;
struct jpeg_color_deconverter *cconvert;
struct jpeg_color_quantizer *cquantize;
};
- jpeg_error_mgr結構體
該結構體用於異常處理,被聲明於jpeglib.h頭文件中,它的部分聲明如下:
struct jpeg_error_mgr {
void (*error_exit) (j_common_ptr cinfo); // 異常退出捕獲函數
void (*emit_message) (j_common_ptr cinfo, int msg_level);
void (*output_message) (j_common_ptr cinfo);
void (*format_message) (j_common_ptr cinfo, char *buffer);
void (*reset_error_mgr) (j_common_ptr cinfo);
...
const char * const *jpeg_message_table; // library錯誤信息
const char * const *addon_message_table; // 非library錯誤信息
};
- *函數:jpeg_std_error(struct jpeg_error_mgr err)
該函數實現在jerror.c
源文件中,它的作用就是初始化jpeg_error_mgr
結構體對象err,即對對象中的成員賦初始值,比如將err對象error_exit字段賦值爲error_exit
函數,該函數是jerror.c源文件已經實現的函數,作用在於當引擎異常退出(如分配內存失敗)時釋放引擎所佔的資源。jpeg_std_error函數實現如下:
GLOBAL(struct jpeg_error_mgr *)
jpeg_std_error(struct jpeg_error_mgr *err)
{
err->error_exit = error_exit; // 異常退出處理函數
err->emit_message = emit_message;
err->output_message = output_message;
err->format_message = format_message;
err->reset_error_mgr = reset_error_mgr;
err->trace_level = 0; /* default = no tracing */
err->num_warnings = 0; /* no warnings emitted yet */
err->msg_code = 0; /* may be useful as a flag for "no error" */
/* Initialize message table pointers */
err->jpeg_message_table = jpeg_std_message_table;
err->last_jpeg_message = (int)JMSG_LASTMSGCODE - 1;
err->addon_message_table = NULL;
err->first_addon_message = 0; /* for safety */
err->last_addon_message = 0;
return err;
}
// error_exit()函數
METHODDEF(void)
error_exit(j_common_ptr cinfo)
{
/* Always display the message */
(*cinfo->err->output_message) (cinfo);
/* Let the memory manager delete any temp files before we die */
jpeg_destroy(cinfo);
exit(EXIT_FAILURE);
}
- 函數:jpeg_create_compress(cinfo)
該函數的作用爲結構體jpeg_compress_struct分配內存資源,並初始化相關成員變量,需要注意的是,在調用該函數之前,我們需要設置該結構體的err
字段,以便處理初始化失敗時異常情況。它被聲明在jpeglib.h頭文件中,它的具體實現實際上是jpeg_CreateCompress()
函數,該函數位於jcapimin.c源文件中。
// #define jpeg_create_compress(cinfo) \
// jpeg_CreateCompress((cinfo), JPEG_LIB_VERSION, \
// (size_t)sizeof(struct jpeg_compress_struct))
GLOBAL(void)
jpeg_CreateCompress(j_compress_ptr cinfo, int version, size_t structsize)
{
int i;
cinfo->mem = NULL;
if (version != JPEG_LIB_VERSION)
ERREXIT2(cinfo, JERR_BAD_LIB_VERSION, JPEG_LIB_VERSION, version);
if (structsize != sizeof(struct jpeg_compress_struct))
ERREXIT2(cinfo, JERR_BAD_STRUCT_SIZE,
(int)sizeof(struct jpeg_compress_struct), (int)structsize);
{
struct jpeg_error_mgr *err = cinfo->err;
void *client_data = cinfo->client_data;
MEMZERO(cinfo, sizeof(struct jpeg_compress_struct));
cinfo->err = err;
cinfo->client_data = client_data;
}
cinfo->is_decompressor = FALSE;
// 初始化cinfo對象的內存管理器實例
jinit_memory_mgr((j_common_ptr)cinfo);
/* Zero out pointers to permanent structures. */
cinfo->progress = NULL;
cinfo->dest = NULL;
cinfo->comp_info = NULL;
for (i = 0; i < NUM_QUANT_TBLS; i++) {
cinfo->quant_tbl_ptrs[i] = NULL;
#if JPEG_LIB_VERSION >= 70
cinfo->q_scale_factor[i] = 100;
#endif
}
for (i = 0; i < NUM_HUFF_TBLS; i++) {
cinfo->dc_huff_tbl_ptrs[i] = NULL;
cinfo->ac_huff_tbl_ptrs[i] = NULL;
}
#if JPEG_LIB_VERSION >= 80
/* Must do it here for emit_dqt in case jpeg_write_tables is used */
cinfo->block_size = DCTSIZE;
cinfo->natural_order = jpeg_natural_order;
cinfo->lim_Se = DCTSIZE2 - 1;
#endif
cinfo->script_space = NULL;
cinfo->input_gamma = 1.0; /* in case application forgets */
// 設置初始化完畢標誌
cinfo->global_state = CSTATE_START;
}
- *函數:jpeg_stdio_dest(j_compress_ptr cinfo, FILE outfile)
該函數的作用是將輸入待壓縮圖像文件流賦值給my_destination_mgr結構體
的outfile字段,它被聲明在libjpeg.h頭文件中,具體實現在jdatadst.c源文件中。jpeg_stdio_dest函數源碼如下:
GLOBAL(void)
jpeg_stdio_dest(j_compress_ptr cinfo, FILE *outfile)
{
/**my_destination_mgr結構體:
*
* typedef struct {
* struct jpeg_destination_mgr pub;
* FILE *outfile; // 目標文件流
* JOCTET *buffer; // buffer緩存
* } my_destination_mgr;
* typedef my_destination_mgr *my_dest_ptr;
*/
my_dest_ptr dest;
if (cinfo->dest == NULL) {
cinfo->dest = (struct jpeg_destination_mgr *)
(*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_PERMANENT,
sizeof(my_destination_mgr));
} else if (cinfo->dest->init_destination != init_destination) {
ERREXIT(cinfo, JERR_BUFFER_SIZE);
}
// 將cinfo的dest成員變量賦值給結構體dest
dest = (my_dest_ptr)cinfo->dest;
dest->pub.init_destination = init_destination;
dest->pub.empty_output_buffer = empty_output_buffer;
dest->pub.term_destination = term_destination;
// 將文件流存儲地址賦值給my_dest_ptr結構體的outfile成員變量
dest->outfile = outfile;
}
- 函數:jpeg_write_scanlines(j_compress_ptr cinfo, JSAMPARRAY scanlines,JDIMENSION num_lines)
該函數的作用時從待壓縮圖像數據源緩存區scanlines,讀出num_lines行數據寫入到編碼壓縮引擎中,並進行編碼壓縮處理。當然,在寫入數據之前該函數會去判斷當前引擎的狀態是否爲啓動,且讀取的行數是否超限。jpeg_write_scanlines函數被聲明在jpeglib.h中,實現在jcapistd.c源文件中,具體源碼如下:
// typedef char JSAMPLE;
// typedef JSAMPLE *JSAMPROW;
// typedef JSAMPROW *JSAMPARRAY;
GLOBAL(JDIMENSION)
jpeg_write_scanlines(j_compress_ptr cinfo, JSAMPARRAY scanlines,
JDIMENSION num_lines)
{
JDIMENSION row_ctr, rows_left;
// 判斷編碼引擎的狀態是否爲CSTATE_SCANNING
if (cinfo->global_state != CSTATE_SCANNING)
ERREXIT1(cinfo, JERR_BAD_STATE, cinfo->global_state);
// 判斷當前行數是否超過待壓縮圖像的高度
if (cinfo->next_scanline >= cinfo->image_height)
WARNMS(cinfo, JWRN_TOO_MUCH_DATA);
if (cinfo->progress != NULL) {
cinfo->progress->pass_counter = (long)cinfo->next_scanline;
cinfo->progress->pass_limit = (long)cinfo->image_height;
(*cinfo->progress->progress_monitor) ((j_common_ptr)cinfo);
}
if (cinfo->master->call_pass_startup)
(*cinfo->master->pass_startup) (cinfo);
rows_left = cinfo->image_height - cinfo->next_scanline;
if (num_lines > rows_left)
num_lines = rows_left;
row_ctr = 0;
// 將num_lines行待壓縮數據傳入jpeg_c_main_controller結構體的process_data函數中
// 至於是如何處理的,我們這裏就不繼續分析了
(*cinfo->main->process_data) (cinfo, scanlines, &row_ctr, num_lines);
cinfo->next_scanline += row_ctr;
return row_ctr;
}
- 函數:jpeg_read_scanlines(j_decompress_ptr cinfo, JSAMPARRAY scanlines,
JDIMENSION max_lines)
該函數的作用是從解壓器中讀取max_lines行解壓數據存儲到scanlines指向的緩存中。它被聲明在jpeglib.h頭文件中,實現在jdapistd.c源文件,具體源碼如下:
GLOBAL(JDIMENSION)
jpeg_read_scanlines(j_decompress_ptr cinfo, JSAMPARRAY scanlines,
JDIMENSION max_lines)
{
JDIMENSION row_ctr;
// 處理邊界
if (cinfo->global_state != DSTATE_SCANNING)
ERREXIT1(cinfo, JERR_BAD_STATE, cinfo->global_state);
if (cinfo->output_scanline >= cinfo->output_height) {
WARNMS(cinfo, JWRN_TOO_MUCH_DATA);
return 0;
}
if (cinfo->progress != NULL) {
cinfo->progress->pass_counter = (long)cinfo->output_scanline;
cinfo->progress->pass_limit = (long)cinfo->output_height;
(*cinfo->progress->progress_monitor) ((j_common_ptr)cinfo);
}
// 從解壓器中讀取解壓數據,存儲到scanlines緩存中
row_ctr = 0;
(*cinfo->main->process_data) (cinfo, scanlines, &row_ctr, max_lines);
cinfo->output_scanline += row_ctr;
return row_ctr;
}
2. libjpeg編譯與移植
2.1 使用Cmake編譯libJPEG-turbo源碼
(1) 新建Android工程libjpeg,並將libjpeg-turbo源碼全部拷貝到src/main/cpp目錄下;
(3) 修改Android工程的build.gradle,配置libjpeg-turbo的CmakeLists.txt;
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.jiangdg.libjpeg"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
// 配置編譯的平臺版本
abiFilters "armeabi", "armeabi-v7a", "arm64-v8a"
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "src\\main\\cpp\\CMakeLists.txt"//路徑改爲cpp文件夾下CMakeList的路徑
}
// cmake {
// path file('CMakeLists.txt')
// }
}
}
(3) 編譯Android項目,得到對應平臺架構的libjpeg.so文件,以及jconfig.h、jconfigint.h頭文件。
Github項目地址:libjpeg4Android,歡迎star or issues.
2.2 使用libjpeg壓縮編碼JPEG圖像
(1) 新建Android項目HandleJpeg,拷貝頭文件jconfig.h、jconfigint.h、jpeglib.h和jmorecfg.h到src/main/cpp目錄,同時拷貝動態庫libjpeg.so到src/main/jniLibs目錄下(如果沒有創建一個)。
(2) 配置CmakeList.txt,導入libjpeg.so
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_VERBOSE_MAKEFILE on)
# 設置so輸出路徑
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/libs)
# 指定libjpeg動態庫路徑
set(jpeglibs "${CMAKE_SOURCE_DIR}/src/main/jniLibs")
# 導入第三方庫:libjpeg.so
add_library(libjpeg SHARED IMPORTED)
set_target_properties(libjpeg PROPERTIES
IMPORTED_LOCATION "${jpeglibs}/${ANDROID_ABI}/libjpeg.so")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11 -fexceptions -frtti")
# 配置、鏈接動態庫
add_library(
jpegutil
SHARED
src/main/cpp/NativeJPEG.cpp)
# 查找NDK原生庫log,android
find_library(log-lib log)
find_library(android-lib android)
# 鏈接所有庫到jpegutil
# AndroidBitmapInfo需要庫jnigraphics
target_link_libraries(jpegutil
libjpeg
jnigraphics
${log-lib}
${android-lib})
(3) 編寫Java層native方法
/**
* 使用libjpeg實現JPEG編碼壓縮、解壓
*
* @author Jiangdg
* @since 2019-08-12 09:54:00
* */
public class JPEGUtils {
static {
System.loadLibrary("jpegutil");
}
public native static int nativeCompressJPEG(Bitmap bitmap, int quality, String outPath);
}
(4) 編寫native層實現
// JPEG圖形編碼壓縮、解壓
// 採用libjpeg庫(libjpeg_turbo版本)實現
//
// Created by Jiangdg on 2019/8/12.
//
#include <jni.h>
#include <android/bitmap.h>
#include <android/log.h>
#include <malloc.h>
#include "jpeglib.h"
#include <stdio.h>
#include <csetjmp>
#include <string.h>
#include <setjmp.h>
#define TAG "NativeJPEG"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)
#define UNSUPPORT_BITMAP_FORMAT -99
#define FAILED_OPEN_OUTPATH -100
#define SUCCESS_COMPRESS 1
typedef uint8_t BYTE;
// 自定義error結構體
struct my_error_mgr {
struct jpeg_error_mgr pub;
jmp_buf setjmp_buffer;
};
int compressJPEG(BYTE *data, int width, int height, jint quality, const char *path) {
int nComponent = 3;
FILE *f = fopen(path, "wb");
if(f == NULL) {
return FAILED_OPEN_OUTPATH;
}
// 初始化JPEG對象,爲其分配空間
struct my_error_mgr my_err;
struct jpeg_compress_struct jcs;
jcs.err = jpeg_std_error(&my_err.pub);
if(setjmp(my_err.setjmp_buffer)) {
return 0;
}
jpeg_create_compress(&jcs);
// 指定壓縮數據源,設定壓縮參數
// 使用哈夫曼算法壓縮編碼
jpeg_stdio_dest(&jcs, f);
jcs.image_width = width;
jcs.image_height = height;
jcs.arith_code = false; // false->哈夫曼編碼
jcs.input_components = nComponent;
jcs.in_color_space = JCS_RGB;
jpeg_set_defaults(&jcs);
jcs.optimize_coding = quality; // 壓縮質量 0~100
jpeg_set_quality(&jcs, quality, true);
// 開始壓縮,一行一行處理
jpeg_start_compress(&jcs, true);
JSAMPROW row_point[1];
int row_stride;
row_stride = jcs.image_width * nComponent;
while(jcs.next_scanline < jcs.image_height) {
row_point[0] = &data[jcs.next_scanline * row_stride];
jpeg_write_scanlines(&jcs, row_point, 1);
}
// 結束壓縮,釋放資源
if(jcs.optimize_coding != 0) {
LOGI("使用哈夫曼壓縮編碼完成");
}
jpeg_finish_compress(&jcs);
jpeg_destroy_compress(&jcs);
fclose(f);
return SUCCESS_COMPRESS;
}
const char *jstringToString(JNIEnv *env, jstring jstr) {
char *ret;
const char * c_str = env->GetStringUTFChars(jstr, NULL);
jsize len = env->GetStringLength(jstr);
if(c_str != NULL) {
ret = (char *)malloc(len+1);
memcpy(ret, c_str, len);
ret[len] = 0;
}
env->ReleaseStringUTFChars(jstr, c_str);
return ret;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_jiangdg_natives_JPEGUtils_nativeCompressJPEG(JNIEnv *env, jclass type, jobject bitmap,
jint quality, jstring outPath_) {
// 獲取bitmap的屬性信息
int ret;
int width, height, format;
int color;
BYTE r, g, b;
BYTE *pixelsColor;
BYTE *data, *tmpData;
AndroidBitmapInfo androidBitmapInfo;
const char *outPath = jstringToString(env, outPath_);
LOGI("outPath=%s", outPath);
if((ret = AndroidBitmap_getInfo(env, bitmap, &androidBitmapInfo)) < 0) {
LOGI("AndroidBitmap_getInfo failed, error=%d", ret);
return ret;
}
if((ret = AndroidBitmap_lockPixels(env, bitmap, reinterpret_cast<void **>(&pixelsColor))) < 0) {
LOGI("AndroidBitmap_lockPixels failed, error=%d", ret);
return ret;
}
width = androidBitmapInfo.width;
height = androidBitmapInfo.height;
format = androidBitmapInfo.format;
LOGI("open image:w=%d, h=%d, format=%d", width, height, format);
// 將bitmap轉換爲rgb數據,只處理RGBA_8888格式
// 一行一行的處理,每個像素佔4個字節,包括a、r、g、b三個分量,每個分量佔8位
data = (BYTE *)malloc(width * height * 3);
tmpData = data;
for(int i=0; i<height; ++i) {
for(int j=0; j<width; ++j) {
if(format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
color = *((int *)pixelsColor);
b = (color >> 16) & 0xFF;
g = (color >> 8) & 0xFF;
r = (color >> 0) & 0xFF;
*data = r;
*(data + 1) = g;
*(data + 2) = b;
data += 3;
// 處理下一個像素,在內存中即佔4個字節
pixelsColor += 4;
} else {
return UNSUPPORT_BITMAP_FORMAT;
}
}
}
if((ret = AndroidBitmap_unlockPixels(env, bitmap)) < 0) {
LOGI("AndroidBitmap_unlockPixels failed,error=%d", ret);
return ret;
}
// 編碼壓縮圖片
ret = compressJPEG(tmpData, width, height, quality, outPath);
free((void *)tmpData);
return ret;
}
當然,如果你需要使用本工程生成的so運用到其他項目,需要編譯本項目,AS會自動在.externalNativeBuild/…/libs目錄下生成libjpegtil.so文件,然後,將libjpegtil.so和libjpeg.so同時拷貝到目標工程中即可。
Github項目地址:HandleJpeg,歡迎star or issues.