1. 程式人生 > >FW:使用libjpeg解碼jpeg圖片

FW:使用libjpeg解碼jpeg圖片

    多媒體應用在現在電子產品中的地位越來越重要,尤其是在嵌入式裝置中。本系列文章將會介紹利用libjpeg解碼jpeg檔案,libpng解碼png檔案,libgif解碼gif檔案。本文為第一篇,介紹使用libjpeg解碼jpeg檔案。

libjpeg簡介

    libjpeg是一個完全用C語言編寫的,包含了被廣泛使用的JPEG解碼、JPEG編碼和其他的JPEG功能的實現。這個庫由獨立JPEG工作組維護。最新版本號是6b,於1998年釋出。可以參考維基百科關於libjpeg的介紹

libjpeg庫的資料結構

    用libjpeg庫解碼jpeg資料的時候,最重要的資料型別為struct jpeg_decompress_struct,一般變數定義成cinfo變數,該變數儲存著jpeg資料的詳細資訊,也儲存著解碼之後輸出資料的詳細資訊。一般情況下,每次呼叫libjpeg庫API的時候都需要把這個變數作為第一個引數傳入。另外使用者也可以通過修改該變數來修改libjpeg行為,比如輸出資料格式,libjpeg庫可用的最大記憶體等等。

libjpeg庫的使用

1、設定出錯處理函式

    “天有不測風雲”,我們使用libjpeg庫的時候難免會產生錯誤,所以我們在使用libjpeg解碼之前,首先要做好錯誤處理。在libjpeg庫中,實現了預設錯誤處理函式,當錯誤發生時,比如如果記憶體不足(非常可能發生,後面會介紹)等,則預設錯誤處理函式將會呼叫exit函式結束整個程序,詳細內容可以參考jerror.c檔案。這個對於很多使用者來說這樣的特性是不合適的,不過libjpeg提供了介面讓我們註冊自定義錯誤處理函式。

    在C語言中沒有C++的異常處理機制,但是提供了setjmp和longjmp機制來實現類似的功能,如果你對這個機制不熟悉的話,請查閱C語言手冊。本文下面的程式碼片段都是出自libjpeg的example.c檔案,可以查閱之。

   1: 
   2: cinfo.err = jpeg_std_error(&jerr.pub);
   3: jerr.pub.error_exit = my_error_exit;
   4: 
   5: if (setjmp(jerr.setjmp_buffer)) {
   6:   
   9:   jpeg_destroy_decompress(&cinfo);
  10:   fclose(infile);
  11:   return 0;
  12: }

    上述程式碼中的重點在於我們向libjpeg註冊了一個my_error_exit回撥函式,當發生錯誤的時候,該回調函式將會被呼叫。然後我們呼叫setjmp函式,設定一個返回點。這樣我們在my_error_exit回撥函式處理完錯誤資訊之後,就可以呼叫longjmp函式返回到這裡,在這個返回點進行資源的釋放(非常重要,否則將會記憶體洩漏)。我們再看看my_error_exit回撥函式的實現:

   1: 
   4: METHODDEF(void)
   5: my_error_exit (j_common_ptr cinfo)
   6: {
   7:   
   8:   my_error_ptr myerr = (my_error_ptr) cinfo->err;
   9: 
  10:   
  11:   
  12:   (*cinfo->err->output_message) (cinfo);
  13: 
  14:   
  15:   longjmp(myerr->setjmp_buffer, 1);
  16: }

    可以通過檢查cinfo->err->msg_code的值來判斷錯誤型別,進行相應的處理。本例中只是簡單的列印一個錯誤資訊。最後呼叫longjmp跳轉到setjmp呼叫的地方。

2、初始化解碼物件

    要使用libjpeg解碼jpeg資料,這步是必須要做的。

   1: 
   2: jpeg_create_decompress(&cinfo);

    這步之後,如果結束解碼或者出錯之後,需要呼叫jpeg_destroy_decompress銷燬解碼物件,否則將會記憶體洩漏。

3、初始化源資料

    在libjpeg庫中僅僅提供了檔案作為輸入資料的介面,在example.c中程式碼如下:

   1: 
   2: jpeg_stdio_src(&cinfo, infile);

    這個設計我個人覺得非常不合理,我覺得一個友好的庫,需要能夠接受各式各樣的輸入(記憶體資料,網路資料等等)。比較友好的做法是提供幾種常用的輸入資料支援(在libjpeg中如:檔案輸入等)。然後還要提供一個使用者註冊自定義輸入函式(回撥函式)的介面,這樣庫就可以適配現實生活中各式各樣的輸入資料型別了。Simon也在以前的博文中寫過怎樣修改libjpeg庫,使之能夠解碼記憶體buffer中的jpeg資料,請參考《LibJpeg解碼記憶體中的Jpeg資料》。當然Simon沒有擴充套件libjpeg庫讓其支援使用者註冊自定義輸入函式(回撥函式),有興趣的朋友可以自行實現。

4、讀取jpeg檔案的頭資訊

    這個和初始化解碼物件一樣,是必須要呼叫的,是約定,沒什麼好說的。

   1: 
   2: (void) jpeg_read_header(&cinfo, TRUE);
5、設定解碼引數

    很多情況下,這步非常重要。比如設定輸出格式,設定scale(縮放)等等功能都是在這一步設定。引數設定通過修改上步得到cinfo的值來實現。這裡簡單介紹一下一些常用的欄位。

out_color_space:輸出的顏色格式,libjpeg定義如下:

   1: 
   2: typedef enum {
   3:     JCS_UNKNOWN,        
   4:     JCS_GRAYSCALE,        
   5:     JCS_RGB,        
   6:     JCS_YCbCr,        
   7:     JCS_CMYK,        
   8:     JCS_YCCK,        
   9: #ifdef ANDROID_RGB
  10:     JCS_RGBA_8888,  
  11:     JCS_RGB_565     
  12: #endif
  13: } J_COLOR_SPACE;

我們可以看出谷歌在Android擴充套件了幾種輸出格式,那麼如果你需要的顏色格式輸出格式libjpeg不支援(比如:YUYV等顏色格式),那麼請參考Android對libjpeg的擴充套件自行修改,不用擔心複雜性,實現起來比較easy。請重點研究jdcolor.c檔案中的jinit_color_deconverter函式。

scale_num,scale_denom:因為實際的顯示裝置千變萬化,我們可能需要根據實際情況對輸出資料進行一些縮放才能夠顯示。libjpeg支援對輸出資料進行縮放(scale),這個變數就是用來設定縮放的引數。目前libjpeg支援1/2,1/4,1/8三種縮放。

mem:可以指定記憶體管理相關的內容,比如分配和釋放記憶體,指定libjpeg可以使用的最大記憶體。預設情況下不同的平臺下面都有一個libjpeg預設最大可用記憶體值,比如Android平臺上面該值為10000000L(10M),請參考jmemxxxx.c檔案中的DEFAULT_MAX_MEM,瞭解不同平臺的預設最大記憶體值。通過修改mem->pub.max_memory_to_use的值,庫的使用者可以自定義libjpeg可以使用的最大記憶體值。

6、開始解碼

    經過前面的引數設定,我們可以開始解碼了,沒有什麼好說的。

   1: 
   2: (void) jpeg_start_decompress(&cinfo);
7、讀取解碼資料
   1: 
   4: while (cinfo.output_scanline < cinfo.output_height) {
   5:   
   9:   (void) jpeg_read_scanlines(&cinfo, buffer, 1);
  10:   
  11:   put_scanline_someplace(buffer[0], row_stride);
  12: }

    請注意雖然函式jpeg_read_scanlines可以指定一次讀多少行,但是目前該函式還是隻能支援一次只讀1行。

8、結束解碼
   1: 
   2: (void) jpeg_finish_decompress(&cinfo);
9、釋放解碼物件
   1: 
   2: 
   3: 
   4: jpeg_destroy_decompress(&cinfo);

   至此一個jpeg資料已經解析完成了。雖然步驟不少但是對於常規的使用還是比較簡單的。

總結

    libjpeg對於baseline的jpeg資料解碼比較好,但是解碼progressive的jpeg資料的時候,對記憶體的需求比較大(我測試過的progressive的圖片曾經發現過消耗70M記憶體)。如果你的硬體能夠有硬體解碼jpeg的能力,請儘可能使用硬體解碼jpeg資料。