Android直播開發之旅(15):libjpeg庫的編譯移植與使用

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

(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目錄下;

[外鏈圖片轉存失敗(img-ApcDwFzD-1566984787883)(C:\Users\Jiangdg\AppData\Roaming\Typora\typora-user-images\1566980310024.png)]

(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頭文件。

[外鏈圖片轉存失敗(img-zqXPHR1Q-1566984787884)(C:\Users\Jiangdg\AppData\Roaming\Typora\typora-user-images\1566980655243.png)]

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.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章