利用 C++类实现 PNG 图像读写及显示(一)
摘 要 运用 libpng 库函数,设计了一个可读写 PNG 格式图像的 C++类。同时,在 VC++6.0 开
发平台下,设计出一个基于多文档结构的图像浏览器,实现 PNG 格式图像的读写及显
示。
关键词 PNG, 图像格式,C++,类
一、前言
PNG 是一种可携式网络图像格式(Portable Network Graphic Format,PNG),其名称来源于非
官方的“PNG's Not GIF”,是一种位图文件(Bitmap file)存储格式,读成“ping”[1]。在 20 世
纪 90 年代, GIF 已经普遍使用,然而在 1995 年新年前后,Unisys 和 CompuServe 两家公司突然
宣布编程使用 GIF 均需要向其支付版税,理由是他们拥有 LZW 压缩算法(GIF 格式采用该算法)的
版权。付费问题,迫使众多开发人员设计和开发新的用以代替 GIF 的图像格式。PNG 出现的初衷就
是为了替代 GIF 文件格式,避免支付版税。因此,设计 PNG 时,保留了 GIF 的众多特性,如(1)
使用彩色查找表(也称调色板)可支持 256 种颜色的彩色图像;(2)流式读/写性能(Streamability);
(3)逐次逼近显示(Progressive display);(4)透明性(Transparency);(5)使用无损压缩等。
同时,也增加了一些 GIF 文件格式所不具备的特性,如(1)每个像素为 48 位的真彩色图像;(2)
每个像素为 16 位的灰度图像;(3)可为灰度图和真彩色图添加α通道;(4)添加图像的γ信息;
(5)使用循环冗余码(Cyclic redundancy code,CRC)检测损害的文件;(6)加快图像显示的逐
次逼近显示方式;(7)标准的读/写工具包等。目前,PNG 已成为国际互网络联盟(World Wide Web
Consortium ,W3C)推荐的标准[2],是互联网中常用的图像格式之一,常见的绘图软件和浏览器均
支持 PNG 图像浏览(其中 IE 4.0 以上版本均支持 PNG)。本文利用 http://www.libpng.org 提供
的 libpng 库[3],设计一个可读写 PNG 图像的 C++类,同时利用 VC++6.0 开发平台,通过 PNG 图像
浏览器的实例设计,说明 PNG 类的使用方法。
二、PNG 文件结构
PNG 图像由一个 8 字节的 PNG 文件署名(PNG file signature)域和按照特定结构组织的 3 个以
上的数据块(Chunk)组成。PNG 文件署名域是用来识别该文件是不是 PNG 文件,如果用十进制数表
示,该域的值依此是 137, 80, 78, 71, 13, 10, 26, 10,(对应的十六进制数为 89,50,4e,
47,0d, 0a,1a,0a)。PNG 数据块由表 1 所示的 4 个域构成[2]。根据类型的不同,PNG 数据块
又可分为两种:一种是关键数据块(Critical chunk),这是标准的数据块,另一种叫做辅助数据块
(Ancillary chunks),是可选的数据块。其中,关键数据块又定义了 4 个标准数据块,分别为(1)
文件头数据块 IHDR(Header chunk);(2)调色板数据块 PLTE(Palette chunk);(3)图像数据块
IDAT(Image data chunk);(4)图像结束数据 IEND(Image trailer chunk)。由于篇幅限制,这
里只介绍与编程设计最为密切的文件头数据块结构,具体见表 2,其他数据块格式可参考文献[2]。
名称
字节数
说明
表 1 PNG 数据块的结构
Length(长度)
Chunk Type Code(数据块类型
码)
4
4
指定数据块中数据域的长度,其长度不超
过(231-1)字节
数据块类型码由 ASCII 字母(A-Z 和 a-z)组
成
Chunk Data(数据块数据) 可变长
存储按照 Chunk Type Code 指定的数据
CRC(循环冗余检测)
度
4
存储用来检测是否有错误的循环冗余码
表 2 文件头数据块的结构
名称
字节数
说明
Width (宽度)
Height (长度)
Bit depth (图像深度)
4
4
1
Color type (颜色类型)
1
Compression method (压
缩方法)
Filter method (滤波器
方法)
Interlace method (隔行
扫描方法)
1
1
1
图像宽度(单位:像素,0 为无效值)
图像高度(单位:像素,0 为无效值)
索引彩色图像:1,2,4 或 8
灰度图像:1,2,4,8 或 16
真彩色图像:8 或 16
0:灰度图像,
2:真彩色图像
3:索引彩色图像
4:带α通道数据的灰度图像
6:带α通道数据的真彩色图像
显示所用压缩方法,国际标准中只定义了一种方
法(method 0)
滤波器方法
0:非隔行扫描;1: Adam7(由
Adam M. Costello 开发的 7 遍隔行扫描方法
三、PNG 类设计
这一小节我们设计一个名为 MyPNG 的类,主要功能包括(1)将一幅 PNG 图像文件读入
内存,对应函数为 PngLoadImage (const char *pstrFileName);(2) 将内存中的图
像数据保存为 PNG 图像,对应函数为 PngSaveImage (const char *pstrFileName);(3)
显示 PNG 图像,对应函数为 Draw( CDC *pDC, int nX = 0, int nY = 0, int nWidth = -1,
int nHeight = -1 )。通常,Window 应用程序中的其他格式的图像显示可通过显示其对
应位图实现。由于 PNG 图像数据的存放格式与位图格式不同,因此为了显示 PNG 图像,需
要将 PNG 图像数据转换成对应的位图格式数据,类中的成员函数 PngToBitmap()就是实现
这个功能。此外,显示位图像素数据时,需要知道位图文件信息,因此要将 PNG 图像的信
息(如宽度、高度等)转换成相应位图文件信息,实现该功能的类成员函数为
FillBitmapInfo()。于是,可得到 MyPNG 类的头文件定义:
#ifndef _PNG_INC_
#define _PNG_INC_
#include "png.h" //声明 libpng 库函数、png 相关的结构体等信息的头文件
class MyPNG
{
public:
MyPNG();
~MyPNG();
bool PngToBitmap(); //将 Png 数据区转换到 bitmap 数据区
bool FillBitmapInfo();
BOOL Draw( CDC *pDC, int nX = 0, int nY = 0, int nWidth = -1, int nHeight
= -1 ); //显示位图
BOOL PngLoadImage (const char *pstrFileName); //载入 PNG
BOOL PngSaveImage (const char *pstrFileName); //将 pPixelBuffer 的数据保
存 PNG
public:
png_structp png_ptr; //libpng 定义的结构体指针,存放用于读/写 png 图像
的信息
png_infop info_ptr; // libpng 定义的结构体指针,存放 png 文件信息
png_byte *pbImage; //存放次序是 R-G-B
//PNG 图像数据,该数据存放规则与位图相反(第一个像素在左下角,自下而上,从左
到右),即第一个像素在左上角(自上而下,从左到右存放)
int cxImgSize, cyImgSize; //图像宽度、高度
int cImgChannels; //颜色通道
//图像深度,索引彩色图像:1,2,4 或 8;灰度图像:1,2,4,8 或 16;真彩色
图像:8 或 16
int iBitDepth;
int iColorType; //颜色类型,0 --灰度图像,2--真彩色图像,3--索引彩色
图像,4--带α通道数据的灰度图像
//6--带α通道数据的真彩色图像
png_color bkgColor; //背景颜色
BITMAPINFO m_bmi; //位图文件信息
BYTE *pPixelBuffer; //存放 PNG 的位图数据缓存区
WORD wImgRowBytes; //每行的字节数
};
#endif
下面是类成员函数的详细定义。类的构造函数用来初始化成员变量信息,析构函数的
作用是释放内存空间。
bkgColor.red=127; bkgColor.green=127; bkgColor.blue=127;
pPixelBuffer=NULL;
MyPNG::MyPNG()
{ png_ptr=NULL; info_ptr=NULL; pbImage=NULL;
cxImgSize=0; cyImgSize=0; wImgRowBytes=0;
}
MyPNG::~MyPNG()
{ if (pbImage!=NULL) free(pbImage);
if (png_ptr!=NULL) free(png_ptr);
if (info_ptr!=NULL) free(info_ptr); if (pPixelBuffer!=NULL) delete
pPixelBuffer;
}
利用 C++类实现 PNG 图像读写及显示(二)
成员函数 PngLoadImage 从路径 pstrFileName 读入 PNG 图像,并将 PNG 图像数据转换
成位图数据,设置位图信息。如果执行函数成功,返回 TRUE,失败则返回 FALSE。函数中
使用了一个例外处理的宏定义,Try{} Catch(){};其详细定义在另外一个头文件
cexcept.h (libpng 例程 visupng 有该文件定义),这里不再列举。
BOOL MyPNG::PngLoadImage (const char *pstrFileName)
{ FILE *pfFile; png_byte pbSig[8];
double dGamma; int i;
png_color_16 *pBackground; png_uint_32 ulChannels, ulRowBytes;
static png_byte **ppbRowPointers = NULL;
if (!pstrFileName) {pbImage=NULL; return FALSE; } //文件名指针为空则
返回
if (!(pfFile = fopen(pstrFileName, "rb"))) { pbImage=NULL; return
FALSE; } //打开 PNG 文件
fread(pbSig, 1, 8, pfFile); //读取 8 位 PNG 文件署名
if (!png_check_sig(pbSig, 8)) { pbImage=NULL; return FALSE; } //检查
PNG 文件署名
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL,
(png_error_ptr)png_cexcept_error, (png_error_ptr)NULL); //创建结构
体信息
if (!png_ptr) {pbImage=NULL; return FALSE;} //创建失败,返回
info_ptr = png_create_info_struct(png_ptr); //创建结构体
//创建失败,释放已创建信息,返回 false
if (!info_ptr) { png_destroy_read_struct(&png_ptr, NULL, NULL); pbImage=NULL;
return FALSE; }
Try
{
#if !defined(PNG_NO_STDIO)
png_init_io(png_ptr, pfFile); //初始化 png 结构体
#else
png_set_read_fn(png_ptr, (png_voidp)pfFile, png_read_data);
#endif
png_set_sig_bytes(png_ptr, 8); //设置 PNG 文件署名
png_read_info(png_ptr, info_ptr); //读取 PNG 信息
png_get_IHDR(png_ptr, info_ptr, (unsigned long *) &cxImgSize,
(unsigned long *)&cyImgSize, &iBitDepth, &iColorType, NULL, NULL, NULL); //获取
宽度、高度、颜色深度、颜色类型等信息
//将图像数据扩展成每个像素由 3 个 8 比特组成
if (iBitDepth == 16) png_set_strip_16(png_ptr);
if (iColorType == PNG_COLOR_TYPE_PALETTE) png_set_expand(png_ptr);
if (iBitDepth < 8) png_set_expand(png_ptr);
if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
png_set_expand(png_ptr);
if (iColorType == PNG_COLOR_TYPE_GRAY || iColorType ==
PNG_COLOR_TYPE_GRAY_ALPHA)
png_set_gray_to_rgb(png_ptr);
if (png_get_bKGD(png_ptr, info_ptr, &pBackground)) //设置背景透明颜
色,
{ png_set_background(png_ptr, pBackground,
PNG_BACKGROUND_GAMMA_FILE, 1, 1.0);
bkgColor.red = (byte) pBackground->red;
bkgColor.green = (byte) pBackground->green;
bkgColor.blue = (byte) pBackground->blue;
} else {bkgColor.red=0;bkgColor.green=0; bkgColor.blue =0; }
//判断是否需要 gamma 转换
if (png_get_gAMA(png_ptr, info_ptr, &dGamma))
png_set_gamma(png_ptr, (double) 2.2, dGamma);
png_read_update_info(png_ptr, info_ptr); //变换后,需要更新 info_ptr
png_get_IHDR(png_ptr, info_ptr,(unsigned long *) &cxImgSize,
(unsigned long *)&cyImgSize, &iBitDepth, &iColorType, NULL, NULL, NULL); //获
取宽度、高度、颜色类型、颜色深度等信息
ulRowBytes = png_get_rowbytes(png_ptr, info_ptr); //行字节数
ulChannels = png_get_channels(png_ptr, info_ptr); //通道信息
cImgChannels = ulChannels;
if (pbImage!=NULL) { free(pbImage); pbImage=NULL;
}//空间已经
存在,则先释放空间
if ((pbImage = (png_byte *) malloc(ulRowBytes * cyImgSize *
sizeof(png_byte))) == NULL)
{ png_error(png_ptr, "Load PNG: Out of memory"); }
if ((ppbRowPointers = (png_bytepp) malloc(cyImgSize *
sizeof(png_bytep))) == NULL) //分配内存空间
{ png_error(png_ptr, "Load PNG: Out of memory"); }
for (i = 0; i < cyImgSize; i++) ppbRowPointers[i] = pbImage + i *
ulRowBytes; // 设置 PNG 图像的每行地址
png_read_image(png_ptr, ppbRowPointers); //一次读入图像信息
png_read_end(png_ptr, NULL); //读取辅助块(additional chunks)信息
free (ppbRowPointers); //释放内存空间
ppbRowPointers = NULL;
} Catch (msg)
{ png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
if(ppbRowPointers) free (ppbRowPointers);
fclose(pfFile);
return FALSE;
}
fclose (pfFile);
pbImage=NULL;
FillBitmapInfo();
bool ret=PngToBitmap(); //将 Png 图像数据转换成位图数据
//填充 Bitmap 信息
return ret;
}
PngSaveImage 实现将内存图像数据保存为文件名为 pstrFileName 的 PNG 图像,如果
执行成功,函数返回 TRUE,失败则返回 FALSE。
BOOL MyPNG::PngSaveImage (const char * pstrFileName)
{
const int ciBitDepth = 8, ciChannels = 3;
FILE *pfFile; png_uint_32 ulRowBytes; static png_byte
**ppbRowPointers = NULL;
int i, iWidth,iHeight;
iWidth=cxImgSize; iHeight=cyImgSize;
if (!pstrFileName) return FALSE; // 文件指针为空则返回
if (!(pfFile = fopen(pstrFileName, "wb"))) return FALSE;//创建文件
// 创建标准的 PNG 结构信息
png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL,
(png_error_ptr)png_cexcept_error, (png_error_ptr)NULL);
if (!png_ptr) { fclose(pfFile); return FALSE; }
info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) { fclose(pfFile); png_destroy_write_struct(&png_ptr,
(png_infopp) NULL); return FALSE; }
Try
{
#if !defined(PNG_NO_STDIO)
png_init_io(png_ptr, pfFile); //初始化 png 结构体信息
#else
png_set_write_fn(png_ptr, (png_voidp)pfFile, png_write_data,
png_flush);
#endif
png_set_IHDR(png_ptr, info_ptr, iWidth, iHeight, ciBitDepth,
PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_BASE,
PNG_FILTER_TYPE_BASE); //写入每像素 3 比特的 PNG 图像
png_write_info(png_ptr, info_ptr); //写入文件头信息
png_set_bgr(png_ptr); //将 BGR 次序转换为 RGB 次序
ulRowBytes = iWidth * ciChannels; // 每行的字节数
if ((ppbRowPointers = (png_bytepp) malloc(iHeight *
sizeof(png_bytep))) == NULL)
Throw "MyPNG: Out of memory"; //分配内存失败;
//设置每行的指针地址
for (i = 0; i < iHeight; i++) ppbRowPointers[i] = pPixelBuffer +
(iHeight-1-i) * (((ulRowBytes + 3) >> 2) << 2);
png_write_image (png_ptr, ppbRowPointers);// 一次写入整幅图像
png_write_end(png_ptr, info_ptr); // 写辅助块信息
free (ppbRowPointers); //释放空间
ppbRowPointers = NULL;
png_destroy_write_struct(&png_ptr, (png_infopp) NULL); //写操作结
束,释放空间
} Catch (msg)
{ png_destroy_write_struct(&png_ptr, (png_infopp) NULL);
if(ppbRowPointers) free (ppbRowPointers);
fclose(pfFile);
return FALSE;
}
fclose (pfFile);
return TRUE;}
利用 C++类实现 PNG 图像读写及显示(三)
函数 Draw 主要用来显示 PNG 图像对应的位图图像,通过指定宽度和高度,可实现图像
的缩放显示。该函数主要为 VC++应用程序编写。
BOOL MyPNG::Draw(CDC *pDC, int nX, int nY, int nWidth, int nHeight)
{ if( nWidth == -1 )
nWidth = cxImgSize; if( nHeight == -1 ) nHeight=
cyImgSize;
StretchDIBits( pDC->m_hDC, nX, nY,nWidth, nHeight,0, 0,cxImgSize,
cyImgSize,pPixelBuffer,&m_bmi, BI_RGB, SRCCOPY );
return true;
}
成员函数 FillBitmapInfo 的功能是根据 PNG 图像大小设置位图信息,为显示图像做准
备。
m_bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
m_bmi.bmiHeader.biHeight=cyImgSize;
bool MyPNG::FillBitmapInfo() //给变量 m_bmi(位图信息头)赋值
{
m_bmi.bmiHeader.biWidth=cxImgSize;
}
成员函数 PngToBitmap 主要实现将 PNG 图像数据转换成位图图像数据。
bool MyPNG::PngToBitmap()
{
m_bmi.bmiHeader.biPlanes = 1; m_bmi.bmiHeader.biBitCount = 24;
m_bmi.bmiHeader.biCompression = 0;
return true;
BYTE *src, *dst; BYTE r, g, b, a;
const int cDIChannels = 3; WORD wDIRowBytes;
int xImg, yImg;
if (pPixelBuffer!=NULL) { delete pPixelBuffer; pPixelBuffer=NULL; }//
空间已分配则先释放
wImgRowBytes = cImgChannels * cxImgSize;//每行的字节数
wDIRowBytes = (WORD) ((cDIChannels * cxImgSize + 3L) >> 2) << 2;
pPixelBuffer=new BYTE[wDIRowBytes*cyImgSize]; //分配内存空间
if (pPixelBuffer==NULL) { AfxMessageBox("PNG to Bitmap:Out of
memory."); return false;} //分配失败则返回
for (yImg = 0; yImg < cyImgSize; yImg++) // 拷贝图像数据
{
src = pbImage + yImg * wImgRowBytes;
dst = pPixelBuffer + (cyImgSize - yImg - 1) * wDIRowBytes ;//+ cxImgPos
* cDIChannels;
为 RGB
}
for (xImg = 0; xImg < cxImgSize; xImg++)
{ r = *src++; g = *src++; b = *src++;
*dst++ = b; *dst++ = g; *dst++ = r; //位图的次序为 BGR,PNG 的次序
if (cImgChannels == 4) { a = *src++; } //通道信息跳过
}
return true;
}
利用 C++类实现 PNG 图像读写及显示(四)
四、实例设计
在设计实例前,先准备好相关的库。PNG 文件的读写库为 libpng,由于 PNG 用到 LZ77
派生压缩算法,因此,编译读写库时,libpng 需要连接 zlib 库[4],这两个库的源代码可
在文献[3]和文献[4]中获得。下面开始讲解如何应用 VC++6.0 设计 PNG 图像浏览器。
第一步:建立一个名为 PngImage 的多文档框架应用程序,其 View 类选择继承于
CScrollView。
第二步:在应用程序 CPngImageApp 的初始化函数 InitInstance()中,加入以下代码
cmdInfo.m_nShellCommand=CCommandLineInfo::FileNothing;
该代码位于 ParseCommandLine(cmdInfo)后,目的是使程序启动时,不打开新的空白
文档。同时将字符串 IDR_PNGIMATYPE 修改为
“\nPNG\nPNG\nPNG(*.png)\n.png\nPNG.Document\nPNG Document”,目的是打开文件时,
默认的文件对话框只显示后缀名为 PNG 文件。
第三步:从 libpng 库和 zlib 库中,选择以下文件,将他们添加到 PngImage 项目中。
pngconf.h, png.h, libpng.lib, cexcept.h, zlib.lib, zlib.h, zconf.h
同时,将第三节设计的 MyPNG 类也添加到项目中。
第四步:为文档类 CPngImageDoc 添加公有成员变量 MyPNG m_png, 并改写
OnOpenDocument 函数,具体如下:
BOOL CPngImageDoc::OnOpenDocument(LPCTSTR lpszPathName)
{ if (!CDocument::OnOpenDocument(lpszPathName)) return FALSE;
BOOL ret=m_png.PngLoadImage((const char *)lpszPathName);
return ret;
}
重写文档类的虚函数 OnSaveDocument,具体如下:
BOOL CPngImageDoc::OnSaveDocument(LPCTSTR lpszPathName)
{ BOOL ret=m_png.PngSaveImage((const char *)lpszPathName);
if (ret==TRUE)
else AfxMessageBox("保存失败!");
AfxMessageBox("保存成功!");
return ret;
}
同时,为菜单 IDR_PNGIMATYPE 中的“另存为”添加消息映射,具体代码如下:
void CPngImageDoc::OnFileSaveAs()
{
BOOL ret;
static char BASED_CODE szSaveFilter[]="PNG(*.png)|*.png||";
//过滤文
件
//创建文件保存对话框
CFileDialog
FileDlg(FALSE,"*.png",NULL,OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT,szSaveFilter);
if (FileDlg.DoModal()==IDOK) //如果选择确定按钮,则保存
{ ret=m_png.PngSaveImage((const char *)FileDlg.m_ofn.lpstrFile);
}
}
if (ret==TRUE) AfxMessageBox("另存成功!");
else
AfxMessageBox("另存失败!");