FW:使用libjpeg解码jpeg图片

原文地址:FW:使用libjpeg解码jpeg图片 作者:拔剑

图像解码之一——使用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数据。

标签:imagelibraryopen-sourceprogram

LibJpeg解码内存中的Jpeg数据

熟悉libjpeg的朋友都知道libjpeg是一个开源的库。Linux和Android都是用libjpeg来支持jpeg文件的,可见其功能多么强大。但是默认情况下libjpeg只能处理jpeg文件的解码,或者把图像编码到jpeg文件。在嵌入式设备中没有文件系统也是很正常的事情,难道我们就不能利用libjpeg的强大功能了吗?当然不是!本文将会介绍怎样扩展libjpeg让其能够解码内存中的jpeg数据。

     在介绍主题之前,请允许我讨论一下公共代码库的数据输入的一些问题。因为一个公共代码库是开放给大家用的,这个世界的输入方式也是多种多样的,比如可以通过文件输入,shell用户手工输入,内存缓存输入,网络socket输入等等。所以实现库的时候,千万不要假定用户只有一种输入方式。

     通用的做法是实现一个输入的中间层。如果库是以支持面向对象语言实现的话,可以实现一套流机制,实现各式各样的流(文件流,缓存流,socket流等)。公共代码库的输入为流对象。这样库就可以实现各式各样的输入了。一个例子请参考Android图形引擎Skia的实现。

     假如库是用非面向对象的语言实现的话,那么怎样来实现多种输入方式呢?可以通过定义输入对象的数据结构,该数据结构中让用户注册读写数据的函数和数据。因为只有调用者最清楚他的数据来源,数据读取方式。在公共代码库中,只需要调用用户注册的回调函数对数据进行读写就可以了。这样的话,也可以实现公共代码库对多种输入方式的支持。

     回到本文的主题,libjpeg对多种输入的支持就不好,它假设了用户只会用文件作为输入,没有考虑其他的输入方式。经过研究他的源代码发现其内部也是非常容易扩展,进而实现对多种输入的支持的,但是libjpeg没有更这样做,不明白为什么。请看jpeglib.h中如下定义:

struct jpeg_source_mgr {
  const JOCTET * next_input_byte; 
  size_t bytes_in_buffer;       

  JMETHOD(void, init_source, (j_decompress_ptr cinfo));
  JMETHOD(boolean, fill_input_buffer, (j_decompress_ptr cinfo));
  JMETHOD(void, skip_input_data, (j_decompress_ptr cinfo, long num_bytes));
  JMETHOD(boolean, resync_to_restart, (j_decompress_ptr cinfo, int desired));
  JMETHOD(void, term_source, (j_decompress_ptr cinfo));
};

可以看出source manager对象可以注册多个回调函数来对数据进行读写。在看jdatasrc.c中的代码:

typedef struct {
  struct jpeg_source_mgr pub;      

  FILE * infile;                
  JOCTET * buffer;              
  boolean start_of_file;        
} my_source_mgr;

该文件为jpeglib的source manger初始化和管理的地方。上面的数据结构是内部使用的源数据。可以看出其源数据只支持文件输入(infile变量),并提供缓存功能(buffer变量)。

     其对source manager初始化的接口定义子jpeglib.h中,定义如下:

EXTERN(void) jpeg_stdio_src JPP((j_decompress_ptr cinfo, FILE * infile));

通过这个接口我们可以看出它的source manager只能接收文件作为输入。该函数的实现在jdatasrc.c文件中。

     为了支持内存jpeg数据输入,我的设计是在jdatasrc.c中实现一个新的接口来初始化jpeglib的source manger对象。并完成注册其读写的回调函数给source manager。

     说干就干,首先我们需要让source manager对象支持内存数据。修改my_source_mgr数据结构如下:

typedef struct{
  UINT8* img_buffer;
  UINT32 buffer_size;
  UINT32 pos;
}BUFF_JPG;


typedef struct {
  struct jpeg_source_mgr pub;      
  union{
    BUFF_JPG jpg;               
    VFS_FILE * infile;          
  };
  JOCTET * buffer;              
  boolean start_of_file;        
} my_source_mgr;

可以看出我们通过union来支持内存数据(jpg变量)或者文件输入。因为需要负责读写必须要标识出当前内存读写的位置,所以必须要在BUFF_JPG数据结构中定义pos变量。

     下一步我们需要实现读写内存jpeg数据的回调函数了。经过分析对文件数据读写的回调函数,发现我们只需要实现jpeg_source_mgr数据结构中的fill_input_buffer回调函数就可以了,其他的回调函数可以延用对文件读取的回调函数。在jdatasrc.c文件中,定义回调函数如下:

METHODDEF(boolean)
jpg_fill_input_buffer (j_decompress_ptr cinfo)
{
  my_src_ptr src = (my_src_ptr) cinfo->src;
  size_t nbytes;

  if(src->jpg.img_buffer == NULL || src->jpg.pos >= src->jpg.buffer_size){
    nbytes = -1;
  }
  else {
    nbytes = (src->jpg.pos + INPUT_BUF_SIZE > src->jpg.buffer_size ?       
        src->jpg.buffer_size - src->jpg.pos : INPUT_BUF_SIZE);
    MEMCPY(src->buffer, src->jpg.img_buffer + src->jpg.pos, nbytes);
    src->jpg.pos += nbytes;
  }

  if (nbytes <= 0) {
    if (src->start_of_file) 
      ERREXIT(cinfo, JERR_INPUT_EMPTY);
    WARNMS(cinfo, JWRN_JPEG_EOF);
    
    src->buffer[0] = (JOCTET) 0xFF;
    src->buffer[1] = (JOCTET) JPEG_EOI;
    nbytes = 2;
  }

  src->pub.next_input_byte = src->buffer;
  src->pub.bytes_in_buffer = nbytes;
  src->start_of_file = FALSE;

  return TRUE;
}

可以看出我们读取数据都是从内存缓存中读取,如果到达缓存末尾就返回-1。

     经过调试分析还发现jdatasrc.c文件中skip_input_data函数有一个不严谨的地方。原来代码中如下:

METHODDEF(void)
skip_input_data (j_decompress_ptr cinfo, long num_bytes)
{
  my_src_ptr src = (my_src_ptr) cinfo->src;

  
  if (num_bytes > 0) {
    while (num_bytes > (long) src->pub.bytes_in_buffer) {
      num_bytes -= (long) src->pub.bytes_in_buffer;
      (void) fill_input_buffer(cinfo);
      
    }
    src->pub.next_input_byte += (size_t) num_bytes;
    src->pub.bytes_in_buffer -= (size_t) num_bytes;
  }
}

请注意显示地调用了fill_input_buffer,而不是调用注册给source manager的回调函数。这样做是不严谨的,虽然只支持文件输入的情况下,这样写没有任何问题,但是如果我们增加其他的输入方式的话(比如内存数据输入),这样写将不会调用到我们注册给Source manager的fill_input_buffer回调函数。所以如上的代码修改为:

METHODDEF(void)
skip_input_data (j_decompress_ptr cinfo, long num_bytes)
{
  my_src_ptr src = (my_src_ptr) cinfo->src;

  
  if (num_bytes > 0) {
    while (num_bytes > (long) src->pub.bytes_in_buffer) {
      num_bytes -= (long) src->pub.bytes_in_buffer;
      //(void) fill_input_buffer(cinfo);
      (void) src->pub.fill_input_buffer(cinfo);
      
    }
    src->pub.next_input_byte += (size_t) num_bytes;
    src->pub.bytes_in_buffer -= (size_t) num_bytes;
  }
}

调用我们注册的回调函数来读取数据。

     最好我们需要实现一个供用户用内存jpeg数据初始化source manager的接口。我的定义如下:

GLOBAL(void)
jpeg_stdio_buffer_src (j_decompress_ptr cinfo, UINT8 * buffer, UINT32 size)
{
  my_src_ptr src;

  if (cinfo->src == NULL) { 
    cinfo->src = (struct jpeg_source_mgr *)
      (*cinfo->mem->alloc_small) ((j_common_ptr) cinfo, JPOOL_PERMANENT,
                                  SIZEOF(my_source_mgr));
    src = (my_src_ptr) cinfo->src;
    src->buffer = (JOCTET *)
      (*cinfo->mem->alloc_small) ((j_common_ptr) cinfo, JPOOL_PERMANENT,
                                  INPUT_BUF_SIZE * SIZEOF(JOCTET));
  }

  src = (my_src_ptr) cinfo->src;
  src->pub.init_source = init_source;
  src->pub.fill_input_buffer = jpg_fill_input_buffer;
  src->pub.skip_input_data = skip_input_data;
  src->pub.resync_to_restart = jpeg_resync_to_restart; 
  src->pub.term_source = term_source;
  //src->infile = infile;
  src->jpg.img_buffer = buffer;
  src->jpg.buffer_size = size;
  src->jpg.pos = 0;
  src->pub.bytes_in_buffer = 0; 
  src->pub.next_input_byte = NULL; 
}

通过该函数会发现:我们用户输入的buffer初始化了my_source_mgr,并用我们实现的回调函数jpg_fill_input_buffer初始化了jpeg_source_mgr数据结构中的fill_input_buffer。这样每次libjpeg读取数据就将会调用jpg_fill_input_buffer来读取内存jpeg数据了。

     最后把jpeg_stdio_buffer_src接口暴露给最终用户。在jpeglib.h中增加如下定义:

EXTERN(void) jpeg_stdio_buffer_src JPP((j_decompress_ptr cinfo, UINT8 * buffer, UINT32 size));

    至此libjpeg已经可以支持内存jpeg数据的解码了。只需要在调用jpeg_stdio_src接口的地方改调用jpeg_stdio_buffer_src就可以了。

    以上代码仅供参考,以上代码初步测试没有问题,但是未经过严格测试。如果你经过测试发现代码中的错误或者有改进的方法,请你和我联系。我的联系方式请看《关于云中漫步》。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值