使用IJG读写JPEG格式文件
烤鱼片(@eii.dlmu)
IJG全称为Independent JPEG Group,是一个用于处理JPEG文件格式的开源库。用于遥感和地理信息系统数据处理的GDAL就是用这个库进行JPEG格式解析的。
IJG的主页是http://www.ijg.org/,源代码和一些相关文档可以在http://www.ijg.org/files/下找到。
本文中将依据文档中的libjpeg.doc介绍一些基本的JPEG文件解压缩和压缩处理方式,细节及其他高级操作可以参考libjpeg.doc中的具体介绍。
编译
编译需要的源代码包下载链接是http://www.ijg.org/files/jpegsrc.v6b.tar.gz,下载到硬盘上解压,比如我们将其解压到C:/jpeg-6b,在文件夹中有几个*.doc的文件,是IJG的安装使用文档,比如install.doc就是安装的说明书。虽然这几个文件是以doc为扩展名,其实只是几个文本文件,用记事本之类的程序也可以打开。Libjpeg.doc则是在你的程序中使用IJG库进行开发的使用说明。另外还有个example.c文件是一个示例代码文件,里面有使用IJG进行JPEG读写的样本代码,以及关于每段代码的详细解释,基本上来说,在实际应用时直接按照example.c的代码框架进行修改就基本能满足我们应用程序的需要。
IJG支持多平台下的应用,不同平台下的编译安装细节都可以在install.doc中找到。在此,我以MS VC 2005为例进行说明。
在windows下使用的时候需要对源代码做几处修改。
在头文件jmorecfg.h中找到如下代码:
#ifdef NEED_FAR_POINTERS
#define FAR far
#else
#define FAR
#endif
在这段代码下面添加一句
#define FAR /*ysm fixed*/
还是在这个文件中将
#ifndef XMD_H /* X11/xmd.h correctly defines INT32 */
typedef long INT32;
#endif
修改为
#ifndef XMD_H /* ysm fixed*/
#ifndef _BASETSD_H_
typedef long INT32;
#endif
#endif
开始编译前需要首先将jconfig.vc更名为jconfig.h,然后在命令行下进入到VC2005的安装目录下,比如在我的机器上是C:/Program Files/Microsoft Visual Studio 8/VC,在bin目录下有一个名为vcvars32.bat的批处理文件,在命令行下运行这个批处理,这个批处理的作用是注册VC2005相关的一些环境变量,然后不要关闭当前命令窗口,进入到IJG的安装目录,在此即C:/jpeg-6b下,执行命令nmake /f makefile.vc,也就是开始编译IJG库,在屏幕迅速闪过一些编译信息后,如果没有错误出现,IJG也就编译完成。编译过程中会可能会出现一些警告信息,大概就是说代码中使用的标准C函数在VC2005下被认为是不安全而过时的,不过没什么大关系,忽略就行,这个警告在用VC6编译的时候就不会出现。
编译完成后,IJG的安装目录下会出现若干文件,我们所需要的是一个叫做libjpeg.lib的静态库文件,在编译我们程序的时候链接使用。除此一般还要用到4个头文件,jconfig.h、jerror.h、jmorecfg.h、jpeglib.h。当然,目录下还会有一些可执行文件,是IJG提供的工具程序,在usage.doc中有使用方法的介绍。但对程序员来说,最重要的还是4个头文件加一个库文件,以后用IJG开发的时候用这几个文件就足够了。
开发环境配置
在VC2005下使用IJG之前,首先要让VC能够找到那四个头文件,比如说将它们拷到使用IJG的项目当前目录下,系统的头文件目录下,或者在VC环境下做一下配置,也就是在菜单工具-选项-项目和解决方案-VC++目录下,将头文件和lib文件所在的目录分别添加到包含文件和库文件的列表中。
在程序中需要引入的头文件就是jpeglib.h,由于IJG是用C语言写的,所以在C++引入这个头文件就需要使用extern “C”,如下所示:
extern "C"
{
#include "jpeglib.h"
}
此外,在jmorecfg.h中有段关于INT32
而lib文件的引入则需要在项目的属性页下的配置属性-链接器-输入-附加依赖项中添加libjpeg.lib。
至此,配置步骤完成,于是就可以开始用IJG写程序了。
JPEG图像的解压缩操作
解压缩操作过程
1. 为JPEG对象分配空间并初始化
2. 指定解压缩数据源
3. 获取文件信息
4. 为解压缩设定参数,包括图像大小,颜色空间
5. 开始解压缩
6. 取出数据
7. 解压缩完毕
8. 释放资源
为JPEG对象分配空间并初始化
解压缩过程中使用的JPEG对象是一个jpeg_decompress_struct的结构体。同时还需要定义一个用于错误处理的结构体对象,IJG中标准的错误结构体是jpeg_error_mgr。
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
然后是将错误处理结构对象绑定在JPEG对象上。
cinfo.err = jpeg_std_error(&jerr);
这个标准的错误处理结构将使程序在出现错误时调用exit()退出程序,如果不希望使用标准的错误处理方式,则可以通过自定义退出函数的方法自定义错误处理结构,详情见文章后面的专门章节。
初始化cinfo结构。
jpeg_create_decompress(&cinfo);
指定解压缩数据源
利用标准C中的文件指针传递要打开的jpg文件。
FILE * infile;
if ((infile = fopen("sample.jpg", "rb")) == NULL)
{
return 0;
}
jpeg_stdio_src(&cinfo, infile);
获取文件信息
IJG将图像的缺省信息填充到cinfo结构中以便程序使用。
(void) jpeg_read_header(&cinfo, TRUE);
此时,常见的可用信息包括图像的宽cinfo.image_width,高cinfo.image_height,色彩空间cinfo.jpeg_color_space,颜色通道数cinfo.num_components等。
为解压缩设定参数
在完成jpeg_read_header调用后,开始解压缩之前就可以进行解压缩参数的设定,也就是为cinfo结构的成员赋值。
比如可以设定解出来的图像的大小,也就是与原图的比例。使用scale_num和scale_denom两个参数,解出来的图像大小就是scale_num/scale_denom,但是IJG当前仅支持1/1, 1/2, 1/4, 和1/8这几种缩小比例。
比如要取得1/2原图的图像,需要如下设定:
cinfo.scale_num=1;
cinfo.scale_denom=2;
也可以设定输出图像的色彩空间,即cinfo.out_color_space,可以把一个原本彩色的图像由真彩色JCS_RGB变为灰度JCS_GRAYSCALE。如:
cinfo.out_color_space=JCS_GRAYSCALE;
开始解压缩
根据设定的解压缩参数进行图像解压缩操作。
(void) jpeg_start_decompress(&cinfo);
在完成解压缩操作后,IJG就会将解压后的图像信息填充至cinfo结构中。比如,输出图像宽度cinfo.output_width,输出图像高度cinfo.output_height,每个像素中的颜色通道数cinfo.output_components(比如灰度为1,全彩色为3)等。
一般情况下,这些参数是在jpeg_start_decompress后才被填充到cinfo中的,如果希望在调用jpeg_start_decompress之前就获得这些参数,可以通过调用jpeg_calc_output_dimensions()的方法来实现。
取出数据
解开的数据是按照行取出的,数据像素按照scanline来存储,scanline是从左到右,从上到下的顺序,每个像素对应的各颜色或灰度通道数据是依次存储,比如一个24-bitRGB真彩色的图像中,一个scanline中的数据存储模式是R,G,B,R,G,B,R,G,B,...,每条scanline是一个JSAMPLE类型的数组,一般来说就是unsigned char,定义于jmorecfg.h中。
除了JSAMPLE,IJG还定义了JSAMPROW和JSAMPARRAY,分别表示一行JSAMPLE和一个2D的JSAMPLE数组。
在此,我们定义一个JSAMPARRAY类型的缓冲区变量来存放图像数据。
JSAMPARRAY buffer;
然后是计算每行需要的空间大小,比如RGB图像就是宽度×3,灰度图就是宽度×1
row_stride = cinfo.output_width * cinfo.output_components;
为缓冲区分配空间,这里使用了IJG的内存管理器来完成分配。
JPOOL_IMAGE表示分配的内存空间将在调用jpeg_finish_compress,jpeg_finish_decompress,jpeg_abort后被释放,而如果此参数改为JPOOL_PERMANENT则表示内存将一直到JPEG对象被销毁时才被释放。
row_stride如上所说,是每行数据的实际大小。
最后一个参数是要分配多少行数据。此处只分配了一行。
buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
output_scanline表示当前已经读取的行数,如此即可依次读出图像的所有数据,并填充到缓冲区中,参数1表示的是每次读取的行数。
while (cinfo.output_scanline < cinfo.output_height)
{
(void) jpeg_read_scanlines(&cinfo, buffer, 1);
//do something
}
解压缩完毕
(void) jpeg_finish_decompress(&cinfo);
释放资源
jpeg_destroy_decompress(&cinfo);
fclose(infile);
退出程序
如果不再需要JPEG对象,则使用
jpeg_destroy_decompress(&cinfo);
或
jpeg_destroy(&cinfo);
而如果还希望继续使用JPEG对象,则可使用
jpeg_abort_decompress(&cinfo);
或
jpeg_abort(&cinfo);
完整例程
//变量定义
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
FILE * infile;
JSAMPARRAY buffer;
int row_stride;
//绑定标准错误处理结构
cinfo.err = jpeg_std_error(&jerr);
//初始化JPEG对象
jpeg_create_decompress(&cinfo);
//指定图像文件
if ((infile = fopen("sample.jpg", "rb")) == NULL)
{
return;
}
jpeg_stdio_src(&cinfo, infile);
//读取图像信息
(void) jpeg_read_header(&cinfo, TRUE);
//设定解压缩参数,此处我们将图像长宽缩小为原图的1/2
cinfo.scale_num=1;
cinfo.scale_denom=2;
//开始解压缩图像
(void) jpeg_start_decompress(&cinfo);
//本程序功能是应用GDI+在客户区绘制图像
CClientDC dc(this);
Bitmap bm( cinfo.output_width , cinfo.output_height);
Graphics graphics(dc.GetSafeHdc());
Graphics gdc(&bm);
//分配缓冲区空间
row_stride = cinfo.output_width * cinfo.output_components;
buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
//读取数据
while (cinfo.output_scanline < cinfo.output_height)
{
(void) jpeg_read_scanlines(&cinfo, buffer, 1);
//output_scanline是从1开始,所以需要减1
int line=cinfo.output_scanline-1;
for(int i=0;i
{
//绘制位图,本例中假设读取的sample.jpg图像为RGB真彩色图像
//因此,实际上cinfo.output_components就等于3,灰度图则需另作处理
bm.SetPixel(i,line,Color(255,(BYTE)buffer[0][i*3],(BYTE)buffer[0][i*3+1],(BYTE)buffer[0][i*3+2]));
}
}
//结束解压缩操作
(void) jpeg_finish_decompress(&cinfo);
//释放资源
jpeg_destroy_decompress(&cinfo);
fclose(infile);
//在客户区绘制位图
graphics.DrawImage(&bm,0,0);
JPEG图像的压缩操作
压缩操作过程
1. 为JPEG对象分配空间并初始化
2. 指定图像输出目标
3. 为压缩设定参数,包括图像大小,颜色空间
4. 开始压缩
5. 写入数据
6. 压缩完毕
7. 释放资源
为JPEG对象分配空间并初始化
压缩过程中使用的JPEG对象是一个jpeg_compress_struct的结构体。同时还需要定义一个用于错误处理的结构体对象,IJG中标准的错误结构体是jpeg_error_mgr。
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
然后是将错误处理结构对象绑定在JPEG对象上。
cinfo.err = jpeg_std_error(&jerr);
这个标准的错误处理结构将使程序在出现错误时调用exit()退出程序,如果不希望使用标准的错误处理方式,则可以通过自定义退出函数的方法自定义错误处理结构,详情见文章后面的专门章节。
初始化cinfo结构。
jpeg_create_compress(&cinfo);
指定图像输出目标
利用标准C中的文件指针传递要输出的jpg文件。
FILE * outfile;
if ((outfile = fopen(filename, "wb")) == NULL)
{
return 0;
}
jpeg_stdio_dest(&cinfo, outfile);
为压缩设定参数
在开始压缩数据之前需要为压缩指定几个参数和缺省参数。
设定缺省参数之前需要指定的几个参数是:图像宽度cinfo.image_width,图像高度cinfo.image_height,图像的颜色通道数cinfo.input_components(比如RGB图像为3,灰度图为1),图像颜色空间cinfo.in_color_space(比如真彩色JCS_RGB,灰度图JCS_GRAYSCALE)。
如:
cinfo.image_width = 800;
cinfo.image_height = 600;
cinfo.input_components = 3;
cinfo.in_color_space = JCS_RGB;
然后是设定缺省设置
jpeg_set_defaults(&cinfo);
注意此处,在set default之前,必须设定in_color_space,因为某些缺省参数的设定需要正确的color space值。
在此之后还可以对其他的一些参数进行设定。具体有哪些参数可以查询libjpeg.doc文档。
比如最常用的一个参数就是压缩比。
jpeg_set_quality(&cinfo, quality, TRUE);
quality是个0~100之间的整数,表示压缩比率。
开始压缩
根据设定的压缩参数进行图像压缩操作。
jpeg_start_compress(&cinfo, TRUE);
开始压缩过程后就不可以修改cinfo对象参数。
写入数据
row_stride = image_width * 3; //假设用到的图示RGB真彩色三通道
同上文介绍的解压缩操作中介绍的,要写入的数据是按照行写入的,数据像素按照scanline来存储,与读取数据的不同是使用jpeg_write_scanlines。
类似于解压缩操作中的cinfo.output_scanline < cinfo.output_height机制,压缩过程使用的cinfo.next_scanline < cinfo.image_height来判断是否完成写入数据。
在此,假设image_buffer是个JSAMPARRAY类型变量,其中保存的是要输出的图像数据,比如可以是用上文中的解压缩操作从某JPEG文件中获得的数据。
JSAMPROW row_pointer;
while (cinfo.next_scanline < cinfo.image_height)
{
//找到图像中的某一行,写入目标文件
row_pointer = image_buffer[cinfo.next_scanline];
(void) jpeg_write_scanlines(&cinfo, &row_pointer, 1);
}
压缩完毕
jpeg_finish_compress(&cinfo);
释放资源
fclose(outfile);
jpeg_destroy_compress(&cinfo);
退出程序
如果不再需要JPEG对象,则使用
jpeg_destroy_compress(&cinfo);
或
jpeg_destroy(&cinfo);
而如果还希望继续使用JPEG对象,则可使用
jpeg_abort_compress(&cinfo);
或
jpeg_abort(&cinfo);
完整例程
//变量定义
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
FILE * outfile;
JSAMPROW row_pointer;
int row_stride;
//绑定标准错误处理结构
cinfo.err = jpeg_std_error(&jerr);
//初始化JPEG对象
jpeg_create_compress(&cinfo);
//指定目标图像文件
if ((outfile = fopen("dest.jpg", "wb")) == NULL)
{
return;
}
jpeg_stdio_dest(&cinfo, outfile);
//设定压缩参数
cinfo.image_width = image_width;
cinfo.image_height = image_height;
cinfo.input_components = 3;
cinfo.in_color_space = JCS_RGB;
jpeg_set_defaults(&cinfo);
//此处设压缩比为90%
jpeg_set_quality(&cinfo, 90, TRUE);
//开始压缩
jpeg_start_compress(&cinfo, TRUE);
//假设使用的是RGB图像
row_stride = image_width * 3;
//写入数据
while (cinfo.next_scanline < cinfo.image_height)
{
row_pointer = image_buffer[cinfo.next_scanline];
(void) jpeg_write_scanlines(&cinfo, &row_pointer, 1);
}
//压缩完毕
jpeg_finish_compress(&cinfo);
//释放资源
fclose(outfile);
jpeg_destroy_compress(&cinfo);
错误处理
在使用默认错误处理结构jpeg_error_mgr的情况下,程序在遇到错误后将调用exit直接退出程序,用户如果不希望使用这种直接退出的方式处理错误的话就需要自定义错误处理结构。
依照example.c中的例子,IJG推荐使用C语言的setjmp和longjmp机制来重写错误处理结构。
首先,需要定义一个包含标准错误处理结构类型变量的自定义结构。
同时,程序将需要引入头文件setjmp.h。
#include
struct my_error_mgr
{
struct jpeg_error_mgr pub;
jmp_buf setjmp_buffer;
};
typedef struct my_error_mgr * my_error_ptr;
以及一个错误处理函数。在出现错误时程序将跳转到本函数中,而本函数将跳转到setjmp设定的程序位置。
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 jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
//此处做了修改
//struct jpeg_error_mgr jerr;
struct my_error_mgr jerr;
////////////////////////////////////////////////////
FILE * infile;
JSAMPARRAY buffer;
int row_stride;
//此处做了修改
//cinfo.err = jpeg_std_error(&jerr);
cinfo.err = jpeg_std_error(&jerr.pub);
jerr.pub.error_exit = my_error_exit;
if (setjmp(jerr.setjmp_buffer))
{
//在正常情况下,setjmp将返回0,而如果程序出现错误,即调用my_error_exit
//然后程序将再次跳转于此,同时setjmp将返回在my_error_exit中由longjmp第二个参数设定的值1
//并执行以下代码
jpeg_destroy_decompress(&cinfo);
fclose(infile);
return;
}
////////////////////////////////////////////////////
jpeg_create_decompress(&cinfo);
if ((infile = fopen("sample.jpg", "rb")) == NULL)
{
return;
}
//以下的代码与上文解压缩操作章节中相同,不再赘述
THE END.