1. 甚么是 BMP
BMP 格式是最简单,最直观的位图数据格式.它的思想非常朴素:
用若干个位来保存1个像素的信息,由若干个像素组成1个像素流来表达1张图片.
通常我们会用1位(可以保存2种色彩,0 表示1种色彩,1表示另外一种,不1定是黑白,也能够是蓝绿,总之是2种),4位 - 16种色彩,8位 - 256种色彩,16位 - 65535种色彩,24位 - 2^24种色彩,32位 - 前24位和24位位图1样可以保存2^24种色彩,最后8位用来保存这个像素的灰度(也就是这个像素的明暗程度),可以表示256种灰度.注意,目前的显示器在硬件上通常只能支持24位色,而且 libjpeg 也只能处理最多24位色的像素流.
对24位以下的BMP,可以引入1个"调色板"来增强图片的表达能力,以8位,256色位图作为例子:
在8位位图中,每一个像素的信息用1个字节存储,那末 DIB 数据就是1个 BYTE dibBuffer[] 数组, dibBuffer[0] 表示第1个像素的色彩,以此类推. 我们知道色彩是由RGB 3个份量组合而成的,编程中用 RGBQUAD 结构表示,那末8位除以3,每一个份量只能用2位来存储(不使用编码紧缩RLE的条件下),实际能够表现的色彩非常有限.现在我们引入1个长度为256的 RGBQUAD 类型的数组: RGBQUAD colorTable[256],我们在 DBI 数组 dibBuffer[] 中不再直接寄存的每一个像素的色彩值,而是寄存该色彩值在
colorTable 中的索引,这样就能够充分利用 dibBuffer 中的每位的存储空间.这个 "colorTable" 就是 Windows 中调色板的概念. 知道了这些,就能够理解为何24位及以上色深的位图不需要调色板了.
2.1. BMP 文件格式和DIB
DIB就是"装备无关位图"的意思,我们可以理解为1个像素数组,这是编程时我们需要处理的数据,非常简单,就是1个定长数组,如果是1个24位的DIB数据,那末在编程时就能够认为是1个 BYTE dibBuffer[], dibBuffer[0],dibBuffer[2],dibBuffer[2]表示第1个像素的 RGB 值(实际上是 BGR), dibBuffer[3],[4],[5] 表示第2个像素的 RGB 值,以此类推.固然我们不能直接把这个 dibBuffer 数组写到磁盘作为 BMP 文件,缺少图片的调色板,宽,高等信息,所以我们需要1个特定的格式来存储
DIB 像素流.
BMP文件格式就是把DIB像素流存储到磁盘是需要遵守的相干约定. 关于BMP文件格式的详细说明在网上可以找到很多,比如这篇说的就很清楚: http://blog.csdn.net/lanbing510/article/details/8176231
从编程的角度来看,1个BMP文件是可以表述为以下结构:
typedef struct tagBITMAP_FILE
{
BITMAPFILEHEADER bitmapheader;
BITMAPINFOHEADER bitmapinfoheader;
PALETTEENTRY palette[n]; // 调色板数据(可选,由BITMAPFILEHEADER::bOffBits计算 n 的值)
UCHAR *dibBuffer; // DIB 数据数组
} BITMAP_FILE;
BITMAPFILEHEADER, BITMAPINFOHEADER, PALETTEENTRY 结构的详细信息可以在 MSDN 中查到.
用自然语言简单描写1下:
文件头 - 固定长度,表示这个文件是1个 BMP 文件,版本号,文件长度等,最重要的时文件头结构中的 bfOffBits 字段,它表示 DIB 像素流数据在文件中的偏移位置,编程时,我们打开1个 BMP 文件,先读取固定长度的文件头,在根据这个字段就能够构造前面说的调色板数组 RGBQUAD colorTable[] 和 DIB 数组 BYTE dibBuffer[] 了.
BMP信息头 - 固定长度,存储位图的宽高等信息,需要注意的字段 biHeight, 如果它是正数则表示像素流的信息是倒序存储的,即位图的底下1行的像素存储在前;如果它是负数则表示像素流的信息是正序存储的,位图的第1行像素存储在 dibBuffer 开头.
调色板数组 - 可选,用文件头中的偏移地址减去文件头和信息头的长度就是调色板数组的长度.
DIB像素流 - 就是 dibBuffer[] 数组.
特别要注意的1点是,在实际编程中, 24位 DIB 数据的寄存顺序是 BGR 即 dibBuffer[0] 寄存的是最后1行的第1个像素的 B 份量, dibBuffer[1] 是 G 份量, dibBuffer[2] 是 R 份量, 而 JPG 紧缩时要求输入顺序是 RGB, 所以把 dibBuffer 提供给 JPEG 紧缩器前需要处理1下, dibBuffer[i] 和 dibBuffer[i + 2] 交换,否则得到的 JPG 图象色彩是不对的.
2.2. DDB
DDB 是"装备相干位图"的意思,把 DIB 数据写入装备以后,装备在内部会把 DIB 数据处理为内部数据格式, Windows GDI 中用 HBITMAP 表述1个 DDB,我们只需要调用相干的 API 就能够了,具体细节不用理睬.
3. 甚么是 JPEG
JPEG是 DIB 数据的1种编码规则,前面我们提到 BMP 文件,直接把 DIB 数组 dibBuffer[] 直接写到文件中,所以BMP文件是原始的,无损失的保存了内存中的图象数据.如果用某种算法把 dibBuffer 数组编码紧缩,那末我们或许就没必要把全部 dibBuffer (通常是1个很大的数组) 直接写入文件中,从而大大节省磁盘空间. JPEG 就是这样1种算法.
4. libjpeg
C语言实现的 JPEG 库,官网地址: http://www.ijg.org/
4.1 编译
我写这篇文章的时候 JPEG 库的版本是 jpeg⑼b,从官网上下载源码 jpegsr9b.zip 解压后,启动Visual Studio,进入命令行模式,切换到 jpeg 源码目录,输入: nmake /f makefile.vc 就会看到 jpeg.sln - VS工程文件出现了,用Visual Studio 打开编译便可.
如果履行 nmake 命令时提示找不到 win32.mak,就编辑1下 makefile.vc 把第12行 !include 注释掉就能够,其实 nmake /f makefile.vc 其实不是真正编译,这是重命名了几个文件而已.
编译完成后得到: jpeg.lib 这就是你需要的库文件了,再把 jconfig.h, jerror.h, jinclude.h, jmorecfg.h, jpeglib.h 复制到你的工程中就算配置完成了.
4.2 example.c
libjpeg 的使用实例在源码包中的 example.c 文件里, 我们只要把 write_JPEG_file / read_JPEG_file 两个函数看明白就能够应付大多数利用了.
4.3 内存 JPG 紧缩解紧缩及其它
example.c 中的实例是使用文件io的, 用 jpeg_mem_src / jpeg_mem_dest 函数代替 jpeg_stdio_src / jpeg_stdio_dest 就能够实现内存io了.
JPEG库是不知道调色板之类的东西的,它只是很单纯的把输入的 DIB 像素流紧缩输出为1个更短的输出数据流.所以对包括了调色板的 BMP 文件,由于 DIB 数组内保存的是调色板的索引号而其实不是色彩值,在提交给 JPEG 库之前需要根据调色板查表构造1个真实的包括色彩信息的 DIB 像素流,这样 JPEG 库才能正常工作.
====================================================================================================
附录: 截取windows桌面,并保存为 .jpg 文件
int save_screen_to_jpeg(const char* filename, int quality)
{
/*
* 把屏幕内容保存为1个 HBITMAP DDB
*/
HDC hScrnDC = CreateDC(_T("DISPLAY"), NULL, NULL, NULL);
HDC hMemDC = CreateCompatibleDC(hScrnDC);
// 获得屏幕分辨率
int xScrn = GetDeviceCaps(hScrnDC, HORZRES);
int yScrn = GetDeviceCaps(hScrnDC, VERTRES);
// 创建位图,并选中
HBITMAP hScrnBmp = CreateCompatibleBitmap(hScrnDC, xScrn, yScrn);
SelectObject(hMemDC, hScrnBmp);
// 复制屏幕内容
BitBlt(hMemDC, 0, 0, xScrn, yScrn, hScrnDC, 0, 0, SRCCOPY);
// 现在得到了1个 HBITMAP DDB - hScrnBmp
/*
* 通过 hScrnBmp DDB 获得 DIB 数据
*/
// 获得色深 JPG 只能处理 24 位色,所以不管当前系统设置的色深是多少,我们都要求 GetDIBits 函数返回 24 位的 DIB 数据,同时也不需要调色板
//int colorDeepBits = GetDeviceCaps(hScrnBmp, BITSPIXEL);
//if(colorDeepBits > 24) colorDeepBits = 24;
int colorDeepBits = 24;
// 每行像素占用的字节数,每行要对齐4字节.
int imageRowSize = (xScrn * colorDeepBits + 31) / 32 * 4;
// 分配 DIB 数组
unsigned char* dibBuffer = new unsigned char[imageRowSize * yScrn];
assert(dibBuffer);
memset(dibBuffer, 0, imageRowSize * yScrn); // 清零是个好习惯
// 填充 BMP 信息头
BITMAPINFO bmi = {0};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = xScrn;
bmi.bmiHeader.biHeight = yScrn * ⑴; // JPG 紧缩需要正序的 DIB 像素流,所以要负数.
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = colorDeepBits;
bmi.bmiHeader.biCompression = BI_RGB;
// 获得 DIB 像素数组(DIB_RGB_COLORS 表示获得 RGB 值而不是调色板索引,固然24位位图也没有调色板)
int gdiRet = GetDIBits(hMemDC, hScrnBmp, 0, yScrn, dibBuffer, &bmi, DIB_RGB_COLORS);
assert(gdiRet == yScrn);
assert(bmi.bmiHeader.biSizeImage == imageRowSize * yScrn);
// DIB 数据已获得,所有的 GDI 对象可以释放了.
DeleteDC(hScrnDC);
DeleteDC(hMemDC);
DeleteObject(hScrnBmp);
/*
* 把 DIB 数据紧缩为 JPG 数据,用 example.c 中的代码
*/
// DIB 中色彩的寄存顺序是 BGR, 而 JPG 要求的顺序是 RGB, 所以要交换 R 和 B.
// 由于有行对齐因素,所以逐行处理
for(int row = 0; row < yScrn; ++row) { unsigned char* rowData = dibBuffer + imageRowSize * row; for(int col = 0; col < xScrn * 3; col += 3) { unsigned char swap = rowData[col]; rowData[col] = rowData[col + 2]; rowData[col + 2] = swap; } } //把位图数据紧缩为 jpeg struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; FILE * outfile; /* target file */ JSAMPROW row_pointer[1]; /* pointer to JSAMPLE row[s] */ int row_stride; /* physical row width in image buffer */ int image_width = xScrn; int image_height = yScrn; JSAMPLE* image_buffer = dibBuffer; // DIB buffer int image_buffer_len = imageRowSize * image_height; // DIB buffer 长度 if(fopen_s(&outfile, filename, "wb")) //if ((outfile = fopen_s(filename, "wb")) == NULL) { fprintf(stderr, "can't open %s\n", filename); assert(0); } else { /* Step 1: allocate and initialize JPEG compression object */ cinfo.err = jpeg_std_error(&jerr); /* Now we can initialize the JPEG compression object. */ jpeg_create_compress(&cinfo); /* Step 2: specify data destination (eg, a file) */ /* Note: steps 2 and 3 can be done in either order. */ jpeg_stdio_dest(&cinfo, outfile); /* Step 3: set parameters for compression */ /* First we supply a description of the input image. * Four fields of the cinfo struct must be filled in: */ cinfo.image_width = image_width; /* image width and height, in pixels */ cinfo.image_height = image_height; cinfo.input_components = 3; /* # of color components per pixel */ // 由于DIB数据是24位的,所以每一个像素占用3个字节 cinfo.in_color_space = JCS_RGB; /* colorspace of input image */ /* Now use the library's routine to set default compression parameters. * (You must set at least cinfo.in_color_space before calling this, * since the defaults depend on the source color space.) */ jpeg_set_defaults(&cinfo); /* Now you can set any non-default parameters you wish to. * Here we just illustrate the use of quality (quantization table) scaling: */ jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */); /* Step 4: Start compressor */ /* TRUE ensures that we will write a complete interchange-JPEG file. * Pass TRUE unless you are very sure of what you're doing. */ jpeg_start_compress(&cinfo, TRUE); /* Step 5: while (scan lines remain to be written) */ /* jpeg_write_scanlines(...); */ /* Here we use the library's state variable cinfo.next_scanline as the * loop counter, so that we don't have to keep track ourselves. * To keep things simple, we pass one scanline per call; you can pass * more if you wish, though. */ row_stride = imageRowSize; while (cinfo.next_scanline < cinfo.image_height) { /* jpeg_write_scanlines expects an array of pointers to scanlines. * Here the array is only one element long, but you could pass * more than one scanline at a time if that's more convenient. */ row_pointer[0] = &image_buffer[cinfo.next_scanline * row_stride]; //row_pointer[0] = &image_buffer[image_buffer_len - (cinfo.next_scanline + 1) * row_stride]; (void)jpeg_write_scanlines(&cinfo, row_pointer, 1); } /* Step 6: Finish compression */ jpeg_finish_compress(&cinfo); /* After finish_compress, we can close the output file. */ fclose(outfile); /* Step 7: release JPEG compression object */ /* This is an important step since it will release a good deal of memory. */ jpeg_destroy_compress(&cinfo); } // 释放 DIB 数组 delete []dibBuffer; return 0; }
PS: 最近1年半在忙1个项目,1直没时间更新博客,也没有回答网友们的发问,非常抱歉.大多数问题都是百度1下就能够处理的,只能说抱歉了.
截屏保存为JPG其实也是我在项目中用到的1个小功能,由于不触及甚么商业上的东西,就贴出来分享1下.
以后有机会我会把项目中的1些功能分解成可以复用的代码,大家交换1下.