整理自文档:
http://blog.csdn.net/yongzhewuwei_2008/article/details/1007130
http://blog.csdn.net/flyingghost/article/details/251110
http://www.cnblogs.com/xiaoxiaoboke/archive/2012/02/13/2349765.html
PNG文件是以大端模式存放的,所以读取PNG文件时候超过一个字节类型的需要注意转换为小端模式才能读取正确。
1.PNG文件结构概述:
对于一个PNG文件来说,其文件头总是由位固定的字节来描述的:
十进制数 | 137 80 78 71 13 10 26 10 |
十六进制数 | 89 50 4E 47 0D 0A 1A 0A |
其中第一个字节0x89超出了ASCII字符的范围,这是为了避免某些软件将PNG文件当做文本文件来处理。文件中剩余的部分由3个以上的PNG的数据块(Chunk)按照特定的顺序组成,因此,一个标准的PNG文件结构应该如下:
PNG文件标志 | PNG数据块 | …… | PNG数据块 |
2.PNG数据块(Chunk)
PNG定义了两种类型的数据块,一种是称为关键数据块(critical chunk),这是标准的数据块,另一种叫做辅助数据块(ancillary chunks),这是可选的数据块。关键数据块定义了4个标准数据块,每个PNG文件都必须包含它们,PNG读写软件也都必须要支持这些数据块。虽然 PNG文件规范没有要求PNG编译码器对可选数据块进行编码和译码,但规范提倡支持可选数据块。
下表就是PNG中数据块的类别,其中,关键数据块部分我们使用深色背景加以区分。
PNG文件格式中的数据块 | ||||
数据块符号 | 数据块名称 | 多数据块 | 可选否 | 位置限制 |
IHDR | 文件头数据块 | 否 | 否 | 第一块 |
cHRM | 基色和白色点数据块 | 否 | 是 | 在PLTE和IDAT之前 |
gAMA | 图像γ数据块 | 否 | 是 | 在PLTE和IDAT之前 |
sBIT | 样本有效位数据块 | 否 | 是 | 在PLTE和IDAT之前 |
PLTE | 调色板数据块 | 否 | 是 | 在IDAT之前 |
bKGD | 背景颜色数据块 | 否 | 是 | 在PLTE之后IDAT之前 |
hIST | 图像直方图数据块 | 否 | 是 | 在PLTE之后IDAT之前 |
tRNS | 图像透明数据块 | 否 | 是 | 在PLTE之后IDAT之前 |
oFFs | (专用公共数据块) | 否 | 是 | 在IDAT之前 |
pHYs | 物理像素尺寸数据块 | 否 | 是 | 在IDAT之前 |
sCAL | (专用公共数据块) | 否 | 是 | 在IDAT之前 |
IDAT | 图像数据块 | 是 | 否 | 与其他IDAT连续 |
tIME | 图像最后修改时间数据块 | 否 | 是 | 无限制 |
tEXt | 文本信息数据块 | 是 | 是 | 无限制 |
zTXt | 压缩文本数据块 | 是 | 是 | 无限制 |
fRAc | (专用公共数据块) | 是 | 是 | 无限制 |
gIFg | (专用公共数据块) | 是 | 是 | 无限制 |
gIFt | (专用公共数据块) | 是 | 是 | 无限制 |
gIFx | (专用公共数据块) | 是 | 是 | 无限制 |
IEND | 图像结束数据 | 否 | 否 | 最后一个数据块 |
为了简单起见,我们假设在我们使用的PNG文件中,这4个数据块按以上先后顺序进行存储,并且都只出现一次。
3.数据块(Chunk)结构
PNG文件中,每个数据块(Chunk)由4个部分组成,如下:
名称 | 字节数 | 说明 |
Length (长度) | 4字节 | 指定数据块中数据域的长度,其长度不超过(231-1)字节 |
Chunk Type Code (数据块类型码) | 4字节 | 数据块类型码由ASCII字母(A-Z和a-z)组成 |
Chunk Data (数据块数据) | 可变长度 | 存储按照Chunk Type Code指定的数据 |
CRC (循环冗余检测) | 4字节 | 存储用来检测是否有错误的循环冗余码 |
CRC(cyclic redundancy check)域中的值是对Chunk Type Code域和Chunk Data域中的数据进行计算得到的。CRC具体算法定义在ISO 3309和ITU-T V.42中,其值按下面的CRC码生成多项式进行计算:
x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1
下面,我们依次来了解一下各个关键数据块的结构吧。
4.各个关键数据块(critical chunk)的结构
4.1 IHDR
文件头数据块IHDR(header chunk):它包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流中只能有一个文件头数据块。
文件头数据块由13字节组成,它的格式如下表所示。
域的名称 | 字节数 | 说明 |
Width | 4 bytes | 图像宽度,以像素为单位 |
Height | 4 bytes | 图像高度,以像素为单位 |
Bit depth | 1 byte | 图像深度: 索引彩色图像:1,2,4或8 灰度图像:1,2,4,8或16 真彩色图像:8或16 |
ColorType | 1 byte | 颜色类型: 0:灰度图像, 1,2,4,8或16 2:真彩色图像,8或16 3:索引彩色图像,1,2,4或8 4:带α通道数据的灰度图像,8或16 6:带α通道数据的真彩色图像,8或16 |
Compression method | 1 byte | 压缩方法(LZ77派生算法) |
Filter method | 1 byte | 滤波器方法 |
Interlace method | 1 byte | 隔行扫描方法: 0:非隔行扫描 1: Adam7(由Adam M. Costello开发的7遍隔行扫描方法) |
Type Name0 None1 Sub2 Up3 Average4 Paeth
The encoder can choose which of these filter algorithms to apply on a scanline-by-scanline basis. In the image data sent to the compression step, each scanline is preceded by a filter type byte that specifies the filter algorithm used for that scanline.
Filtering algorithms are applied to bytes, not to pixels, regardless of the bit depth or color type of the image. The filtering algorithms work on the byte sequence formed by a scanline that has been represented as described in Image layout. If the image includes an alpha channel, the alpha data is filtered in the same way as the image data.
4.2 PLTE
调色板数据块PLTE(palette chunk)包含有与索引彩色图像(indexed-color image)相关的彩色变换数据,它仅与索引彩色图像有关,而且要放在图像数据块(image data chunk)之前。
PLTE数据块是定义图像的调色板信息,PLTE可以包含1~256个调色板信息,每一个调色板信息由3个字节组成:
颜色 | 字节 | 意义 |
Red | 1 byte | 0 = 黑色, 255 = 红 |
Green | 1 byte | 0 = 黑色, 255 = 绿色 |
Blue | 1 byte | 0 = 黑色, 255 = 蓝色 |
因此,调色板的长度应该是3的倍数,否则,这将是一个非法的调色板。
对于索引图像,调色板信息是必须的,调色板的颜色索引从0开始编号,然后是1、2……,调色板的颜色数不能超过色深中规定的颜色数(如图像色深为4的时候,调色板中的颜色数不可以超过2^4=16),否则,这将导致PNG图像不合法。
真彩色图像和带α通道数据的真彩色图像也可以有调色板数据块,目的是便于非真彩色显示程序用它来量化图像数据,从而显示该图像。
4.3 IDAT
图像数据块IDAT(image data chunk):它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。
IDAT存放着图像真正的数据信息,因此,如果能够了解IDAT的结构(索引数据,图像数据;调色板可以是不使用的,索引调色板图像数据,掩码调色板掩码数据),我们就可以很方便的生成PNG图像。
PNG Spec中指出,如果PNG文件不是采用隔行扫描方法存储的话,那么,数据是按照行(ScanLine)来存储的,为了区分第一行,PNG规定在每一行的前面加上0以示区分。
另外,需要注意的是,由于PNG在存储图像时为了节省空间,因此每一行是按照位(Bit)来存储的,而并不是我们想象的字节(Byte),如果你没有忘记的话,我们的IHDR数据块中的色深就指明了这一点。
因为png存在复杂的deflate压缩算法所以用pnglib库解析png数据是大多数处理png图像的方法。
4.4 IEND
图像结束数据IEND(image trailer chunk):它用来标记PNG文件或者数据流已经结束,并且必须要放在文件的尾部。
如果我们仔细观察PNG文件,我们会发现,文件的结尾12个字符看起来总应该是这样的:
00 00 00 00 49 45 4E 44 AE 42 60 82
不难明白,由于数据块结构的定义,IEND数据块的长度总是0(00 00 00 00,除非人为加入信息),数据标识总是IEND(49 45 4E 44),因此,CRC码也总是AE 42 60 82。
PNG文件的优化和调整效果
由于PNG中规定除关键数据块外,其它的辅助数据块都为可选部分,因此,有了这个标准后,我们可以通过删除所有的辅助数据块来减少PNG文件的大 小。(当然,需要注意的是,PNG格式可以保存图像中的层、文字等信息,一旦删除了这些辅助数据块后,图像将失去原来的可编辑性。)删除了辅助数据块后的PNG文件,现在文件大小为147字节,原文件大小为261字节,文件大小减少后,并不影响图像的内容。
其实,我们可以通过改变调色板的色值来完成一些又趣的事情,每次改变调色板都要重新生成该Chunk的CRC,比如说实现云彩/水波的流动效果,实现图像的淡入淡出效果等等,在此,给出一个链接给大家看也许更直接:http://blog.csdn.net/flyingghost/archive/2005/01/13/251110.aspx
IDAT数据块是使用了deflate压缩算法生成的,由于受限于手机处理器的能力,因此,如果我们在生成IDAT数据块时仍然使用LZ77压缩算法,将会使效 率大打折扣,因此,为了效率,只能使用无压缩的LZ77算法,关于LZ77算法的具体实现,此文不打算深究,如果你对LZ77算法的JAVA实现有兴趣.
利用libpng读取png数据代码:
libpng下载地址:http://sourceforge.net/projects/libpng/files/ 本项目下载的是lpng1617版本。
下载好后,还需要下载http://zlib.net/库先编译zlib库(本项目是采用zlib-1.2.8,zlib编译到zlib-1.2.8\contrib\vstudio\vc10下打开工程编译)。
引入zlib.lib到libpng工程中,打开lpng1617\projects\vstudio编译libpng,引入libpng.h,libpng.lib库目录和库名称即可。
测试代码:
#ifndef PNGFILE_H_
#define PNGFILE_H_
#include "png.h"typedef struct
{unsigned char* data;int size;int offset;
}tImageData;typedef struct tagPngImageInfo
{unsigned char *m_pImageData;bool m_bHasAlpha;bool m_bPreMulti;unsigned int m_nSize;int m_nBitsComponent;int m_nWidth;int m_nHeight;tagPngImageInfo(){Clear();}void Clear(){m_pImageData = NULL;m_bHasAlpha = false;m_bPreMulti = false;m_nSize = 0;m_nBitsComponent = 0;m_nWidth = 0;m_nHeight = 0;}
}PngImageInfo;#define CC_RGB_PREMULTIPLY_ALPHA(vr, vg, vb, va) \(unsigned)(((unsigned)((unsigned char)(vr) * ((unsigned char)(va) + 1)) >> 8) | \((unsigned)((unsigned char)(vg) * ((unsigned char)(va) + 1) >> 8) << 8) | \((unsigned)((unsigned char)(vb) * ((unsigned char)(va) + 1) >> 8) << 16) | \((unsigned)(unsigned char)(va) << 24))class CPNGFile
{
public:CPNGFile(const char *pFilePath);~CPNGFile();PngImageInfo* GetImageInfo();
protected:unsigned char* ReadFile(const char *pFilePath, unsigned int &nLen);void AnalyzeData(unsigned char* pData, unsigned int nLen);void Destory();static void PngReadCallback(png_structp png_ptr, png_bytep data, png_size_t length);void FormatImageInfo();
private:CPNGFile(){};
private:PngImageInfo m_nImageInfo;
};#endif
#include "PNGFile.h"
#include <stdio.h>
#include <string.h>
static const int PNG_MARK_LEN = 8;CPNGFile::CPNGFile(const char *pFilePath)
{m_nImageInfo.Clear();unsigned int nLen = 0;unsigned char *data = ReadFile(pFilePath, nLen);AnalyzeData(data, nLen);FormatImageInfo();
}CPNGFile::~CPNGFile()
{if(m_nImageInfo.m_pImageData != NULL){Destory();}
}unsigned char* CPNGFile::ReadFile(const char *pFilePath, unsigned int &nLen)
{FILE *pFile = fopen(pFilePath, "rb");if(pFile == NULL){printf("ReadFileData: %s error\n", pFilePath);}int nFileLen = 0;fseek(pFile, 0, SEEK_END);nFileLen = ftell(pFile);fseek(pFile, 0, SEEK_SET);unsigned char *pTotalData = new unsigned char[nFileLen];fread(pTotalData, 1, nFileLen, pFile);fclose(pFile);nLen = nFileLen;return pTotalData;
}void CPNGFile::PngReadCallback(png_structp png_ptr, png_bytep data, png_size_t length)
{tImageData* pSourceData = (tImageData*)png_get_io_ptr(png_ptr);// 该png_ptr参数是输入结构体if((int)(pSourceData->offset + length) <= pSourceData->size){memcpy(data, pSourceData->data + pSourceData->offset, length);pSourceData->offset += length;}else{png_error(png_ptr, "PngReaderCallback failed.\n");}
}void CPNGFile::AnalyzeData(unsigned char* pData, unsigned int nLen)
{if( pData == NULL || nLen < PNG_MARK_LEN){return;}unsigned char pngMarkByte[PNG_MARK_LEN] = {0};png_structp png_ptr = 0;png_infop info_ptr = 0;// 检查是否是Png文件memcpy(pngMarkByte, pData, PNG_MARK_LEN);if( png_sig_cmp(pngMarkByte, 0, PNG_MARK_LEN) != 0 ){printf("png_sig_cmp fail.\n");return;}// 初始化libpng, 异常处理函数使用默认的png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0);if( png_ptr == NULL ){printf("png_create_read_struct fail.\n");return;}// 创建图像信息,解码后的图像数据将放置在其中info_ptr = png_create_info_struct(png_ptr);if( info_ptr == NULL ){printf("png_create_info_struct fail.\n");return;}// 设置异常错误返回点;初始化libpng的时候未指定用户自定义的错误处理函数情况下,才需要设置if (setjmp(png_jmpbuf(png_ptr))){/* Free all of the memory associated with the png_ptr and info_ptr */png_destroy_read_struct(&png_ptr, (info_ptr) ? &info_ptr : 0, 0);printf("AnalyzeData exception.\n");return;}// 自定义回调函数设置libpng数据源,读取的时候调用该回调实现//png_set_read_fn(png_ptr, (void *)user_io_ptr, user_read_fn);tImageData imageData;imageData.data = pData;imageData.offset = 0;imageData.size = nLen;png_set_read_fn( png_ptr, &imageData, PngReadCallback );// 已经用png_sig_cmp读取过png标志位的字节了,后面不读了;不能有这么一句不然会png_read_info异常//png_set_sig_bytes( png_ptr, PNG_MARK_LEN);// 用高层函数读取数据/*当用户的内存足够大,可以一次性读入所有的png数据,并且输出数据格式为如下libpng预定义数据类型时,可以用高层函数png_read_png(png_ptr, info_ptr, png_transforms, png_voidp_NULL);具体libpng下预定义数据类型见http://www.libpng.org/pub/png/libpng-manual.txt该函数相当于调用底层函数(下文将会介绍)如下调用顺序:a)调用png_read_info函数获得图片信息。b)根据png_transforms所指示的,调用png_set_transform设置输出格式转换的函数。c)调用png_read_image来解码整个图片的数据到内存。d)调用png_read_end结束图片解码。*/// 读取PNG文件信息,调用回到函数放置数据到info_ptr中png_read_info(png_ptr, info_ptr);m_nImageInfo.m_nWidth = png_get_image_width(png_ptr, info_ptr);m_nImageInfo.m_nHeight = png_get_image_height(png_ptr, info_ptr);m_nImageInfo.m_nBitsComponent = png_get_bit_depth(png_ptr, info_ptr);// 大小端转换/*PNG files store 16-bit pixels in network byte order (big-endian,ie. most significant bits first). This code would be used if they aresupplied the other way (little-endian, i.e. least significant bitsfirst, the way PCs store them):*/if (m_nImageInfo.m_nBitsComponent > 8){ png_set_swap(png_ptr);}png_uint_32 color_type = png_get_color_type(png_ptr, info_ptr);// force palette images to be expanded to 24-bit RGB// it may include alpha channelif (color_type == PNG_COLOR_TYPE_PALETTE){png_set_palette_to_rgb(png_ptr);}// low-bit-depth grayscale images are to be expanded to 8 bitsif (color_type == PNG_COLOR_TYPE_GRAY && m_nImageInfo.m_nBitsComponent < 8){png_set_expand_gray_1_2_4_to_8(png_ptr);}// expand any tRNS chunk data into a full alpha channelif (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)){png_set_tRNS_to_alpha(png_ptr);} // reduce images with 16-bit samples to 8 bitsif (m_nImageInfo.m_nBitsComponent == 16){png_set_strip_16(png_ptr); } // expand grayscale images to RGBif (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA){png_set_gray_to_rgb(png_ptr);}// read png data// m_nBitsPerComponent will always be 8m_nImageInfo.m_nBitsComponent = 8;png_uint_32 rowbytes;png_bytep* row_pointers = new png_bytep[m_nImageInfo.m_nHeight];png_read_update_info(png_ptr, info_ptr);rowbytes = png_get_rowbytes(png_ptr, info_ptr);m_nImageInfo.m_nSize = rowbytes * m_nImageInfo.m_nHeight;m_nImageInfo.m_pImageData = new unsigned char[m_nImageInfo.m_nSize];if( m_nImageInfo.m_pImageData == NULL){printf("new unsigned char[rowbytes * m_nHeight] fail.\n");}for (unsigned short i = 0; i < m_nImageInfo.m_nHeight; ++i){row_pointers[i] = m_nImageInfo.m_pImageData + i*rowbytes;}// 解码整个图片的数据到内存m_nImageInfo.m_pImageData中png_read_image(png_ptr, row_pointers);// 结束图片解码png_read_end(png_ptr, NULL);png_uint_32 channel = rowbytes / m_nImageInfo.m_nWidth;if (channel == 4){m_nImageInfo.m_bHasAlpha = true;unsigned int *tmp = (unsigned int *)m_nImageInfo.m_pImageData;for(unsigned short i = 0; i < m_nImageInfo.m_nHeight; i++){for(unsigned int j = 0; j < rowbytes; j += 4){*tmp++ = CC_RGB_PREMULTIPLY_ALPHA( row_pointers[i][j], row_pointers[i][j + 1], row_pointers[i][j + 2], row_pointers[i][j + 3] );}}m_nImageInfo.m_bPreMulti = true;}delete []row_pointers;if (png_ptr){png_destroy_read_struct(&png_ptr, (info_ptr) ? &info_ptr : 0, 0);}
}void CPNGFile::Destory()
{if(m_nImageInfo.m_pImageData != NULL){delete []m_nImageInfo.m_pImageData;m_nImageInfo.m_pImageData = NULL;}
}PngImageInfo* CPNGFile::GetImageInfo()
{return &m_nImageInfo;
}void CPNGFile::FormatImageInfo()
{// Repack the pixel data into the right formatif (m_nImageInfo.m_bHasAlpha && m_nImageInfo.m_nBitsComponent >= 8){// Convert "R8G8B8A8" to "R8G8B8"unsigned int length = m_nImageInfo.m_nWidth * m_nImageInfo.m_nHeight;int nImageSize = 3 * length;unsigned char *pTempData = new unsigned char[nImageSize];unsigned int* inPixel32 = (unsigned int*)m_nImageInfo.m_pImageData;unsigned char *outPixel8 = pTempData;for(unsigned int i = 0; i < length; ++i, ++inPixel32){*outPixel8++ = (*inPixel32 >> 0) & 0xFF; // R*outPixel8++ = (*inPixel32 >> 8) & 0xFF; // G*outPixel8++ = (*inPixel32 >> 16) & 0xFF; // B}Destory();m_nImageInfo.m_pImageData = pTempData;m_nImageInfo.m_nSize = nImageSize;m_nImageInfo.m_bHasAlpha = false;}
}
DX中测试代码:
//DirectX method like glTexImage2D http://www.gamedev.net/topic/399930-direct3d-equivalent-functions/
PngImageInfo *pImageInfo = cfile.GetImageInfo();D3DFORMAT colorFormat = GetColorFormat(pImageInfo->m_bHasAlpha, pImageInfo->m_nBitsComponent);HRESULT hr = D3DXCreateTexture(Device, pImageInfo->m_nWidth,pImageInfo->m_nHeight,0, 0, colorFormat, // 4 bytes for a pixel D3DPOOL_MANAGED, &Tex);if(FAILED(hr)){printf("Device->CreateTexture fail.\n");return false;}D3DLOCKED_RECT pRect;Tex->LockRect( 0, &pRect, NULL, 0 );memcpy(pRect.pBits, pImageInfo->m_pImageData, pImageInfo->m_nSize);Tex->UnlockRect( 0 );
结果可以得到部分纹理,但是效果比较黑(如下图),不知道为什么,而且换用BMP非压缩解析出来的imageDataz创建的纹理也是黑乎乎的一片,可能是DX状态设置有问题或者获取的数据还要进行一些大小端内存对齐处理等,有时间再研究下(2015.4.26, Jesse)。 其实一般情况下不需要读取png数据部分,用:
D3DXCreateTextureFromFile(
Device,
"crate.png",
&Tex);
或者:
FILE *pFile = fopen("crate.png","rb");
fseek(pFile, 0, SEEK_END);
int nLen = ftell(pFile);
fseek(pFile, 0, SEEK_SET);
unsigned char *pData = new unsigned char[nLen];
fread(pData, 1, nLen, pFile);
fclose(pFile);
D3DXCreateTextureFromFileInMemory(Device, pData/*cfile.GetImageData()*/, nLen/*cfile.GetImageSize()*/, &Tex);
就可以创建好绝大部分格式文件的纹理。