很久以前遇到的一个问题,当时使用GDI+绘图,打开超大jpeg的时候,会卡很久直到图片完全解码完毕才能一次性显示出来。
当时设想的完美解决方案是逐步解码,逐步显示,但是技术水平所限,一直找不到解决方案,最后是使用了等待进度条使用户体验稍好一点,并没有真正解决。
现在GDI+已经可以在MFC里直接使用CImage了,但是依然没有区域解码的接口。网络上也搜索了,可能是搜索方式不对,没有找到有价值的东西,于是想通过学习libjpeg开源库来找出解决方案。
稍微学习了libJpeg之后发现其实很简单。下面直接上结果。jpeg图像好像分两种,一种线性的一种渐进的,并没有深入了解,这个方法应该是适用于线性的。
1.新建一个线程,用于载入和解码大图。因为大图解码需要不少时间,如果在UI线程的话,会导致界面卡死一段时间,所以要放到新线程中。
DWORD WINAPI MyThreadFunction(LPVOID lpParam)
{
//文件路径通过线程参数传入,可自行修改使用自己习惯的方式实现。
LPCTSTR lpszImage = (LPCTSTR)lpParam;
//以下基本都是libJpeg的例子里的,自己的实现都加了注释
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&cinfo);
FILE *f;
errno_t err = _tfopen_s(&f, lpszImage, _T("rb"));
if (f == NULL)
{
MessageBox(0, _T("Open file error!"), _T("error!") , MB_OK);
return 0;
}
jpeg_stdio_src(&cinfo, f);
jpeg_read_header(&cinfo, TRUE);
//上面读取了文件头,获取了图像信息,填充到下面的BITMAPINFO bi结构体中
//bi是全局变量,因为线程里要填充,后面绘制的地方要用到。可自行使用习惯的方式实现
bi.bmiHeader.biSize = sizeof(BITMAPINFO);
bi.bmiHeader.biWidth = cinfo.image_width;
bi.bmiHeader.biHeight = cinfo.image_height;
bi.bmiHeader.biPlanes = 1;
bi.bmiHeader.biBitCount = 24;
bi.bmiHeader.biCompression = BI_RGB;
//由于要内存绘制所以要四字节对齐
int nStride = ALIGN(cinfo.image_width*cinfo.num_components , 4);
//申请解码空间,同时也是绘制的时候的图像数据
pBuff = new BYTE[nStride * cinfo.image_height];
jpeg_start_decompress(&cinfo);
JSAMPROW row_pointer[1];
DWORD dwTick = GetTickCount();
while (cinfo.output_scanline < cinfo.output_height)
{
//按行获取解码数据要存放的地址,传递给jpeg_read_scanlines第二个参数
row_pointer[0] = &pBuff[(cinfo.output_height - cinfo.output_scanline - 1) * nStride];
jpeg_read_scanlines(&cinfo, row_pointer,1);
DWORD dwCurrentTick = GetTickCount();
if (dwCurrentTick - dwTick > 500)
{
//请求界面刷新。加入dwTick是怕刷新太快闪屏
//后来使用了双缓冲绘制,不清楚去掉会不会闪
theApp.GetMainWnd()->Invalidate(FALSE);
dwTick = dwCurrentTick;
}
}
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
fclose(f);
return 0;
}
线程代码涉及到两个全局变量:
BYTE *pBuff = 0;
BITMAPINFO bi = { 0 };
2.我使用的是MFC对话框,绘制区域是对话框中的一个Picture Control,定义了一个CStatic控件m_ctrl_Frame,绘制代码在OnPaint中。
if (pBuff)
{
CRect rect;
m_ctrl_Frame.GetWindowRect(&rect);
this->ScreenToClient(&rect);
rect.OffsetRect(-rect.left, -rect.top);
HDC hDC = m_ctrl_Frame.GetDC()->GetSafeHdc();
HDC hDCMem;
HBITMAP hBmpMem, hPreBmp;
hDCMem = CreateCompatibleDC(hDC);
// 创建一块指定大小的位图
hBmpMem = CreateCompatibleBitmap(hDC, rect.right, rect.bottom);
// 将该位图选入到内存DC中,默认是全黑色的
hPreBmp = (HBITMAP)SelectObject(hDCMem, hBmpMem);
/* 在双缓冲中绘图 */
// 加载背景位图
SetStretchBltMode(hDCMem, STRETCH_HALFTONE);
int nRet = StretchDIBits(hDCMem, 0, 0, rect.Width(), rect.Height(),
0, 0, bi.bmiHeader.biWidth, bi.bmiHeader.biHeight ,
pBuff, &bi, DIB_RGB_COLORS, SRCCOPY);
/* 将双缓冲区图像复制到显示缓冲区 */
BitBlt(hDC, 0, 0, rect.right, rect.bottom, hDCMem, 0, 0, SRCCOPY);
/* 释放资源 */
SelectObject(hDCMem, hPreBmp);
DeleteObject(hBmpMem);
DeleteDC(hDCMem);
}
测试了一下速度很快,了结一个心愿。感谢libJpeg作者。
***补充:按照这样的方法解码之后RGB颜色是BGR排列的,需要修改一下libJpeg库的“jmorecfg.h”文件,重新编译libJpeg库再进行链接。
jmorecfg.h的400行左右有定义:
#define RGB_RED 0 /* Offset of Red in an RGB scanline element */
#define RGB_GREEN 1 /* Offset of Green */
#define RGB_BLUE 2 /* Offset of Blue */
改成:
#define RGB_RED 2 /* Offset of Red in an RGB scanline element */
#define RGB_GREEN 1 /* Offset of Green */
#define RGB_BLUE 0 /* Offset of Blue */