文章目录
分析PNG文件格式
简介
Portable Network Graphics (PNG, 官方发音为 /pɪŋ/, 但通常被念作 /ˌpiːɛnˈdʒiː/) ,是一种无损压缩的位图图像格式。它支持索引、灰度、RGB三种颜色方案以及Alpha通道等特性。PNG的开发目标是改善并取代GIF作为适合网络传输的格式而不需专利许可,所以被广泛应用于互联网及其他方面上。
以上内容来自维基百科。
文件如何组成?
PNG文件由基本上由两大部分组成。
- 第一部分是File header,用来标识这是一个PNG格式的文件。
- 其余部分则是由Chunks组成,每一个Chunk包含了关于图像的某些信息。
文件中的数据如何组织?
Flie header
File header中包含8-byte的识别标志。每部分含义如下:
取值(hex) | 含义 |
---|---|
89 | 表示不支持8bit的数据,并减小一个文本文件被错误地认为是PNG文件的可能性。 |
50 4E 47 | 英文字符串“PNG”的ASCII码 |
0D 0A | DOS风格的换行符(CRLF)。用于DOS-Unix数据的换行符转换。 |
1A | 在DOS命令行下,用于阻止文件显示的文件结束符。 |
0A | Unix风格的换行符(LF)。用于Unix-DOS换行符的转换。 |
十进制取值为:137 80 78 71 13 10 26 10
Chunks
PNG中定义了两种数据块。分别是:
- critical chunk:关键数据块,PNG文件必须包含,读写软件也必须要支持的数据块
- ancillary chunks:辅助数据块,PNG允许软件忽略它不认识的数据块。正是由于这种设计,允许了PNG格式在拓展时仍然能够保证与旧版本的兼容。
Critical chunks——关键数据块
Critical chunks共有四种类型:
- IHDR,header chunk:包含有图像基本信息,作为第一个数据块出现并只出现一次。
- PLTE,palette chunk:调色板数据块,必须存放在图像数据块之前。
- IDAT,image data chunk,存储实际的图像数据。PNG数据包允许包含多个连续的图像数据块
- IEND,image trailer chunk:图像结束数据,表示PNG数据流结束。
除了上述的限制之外,chunk可以以任意顺序出现。由于不要求解释器可以识别所有内容,甚至自己也可以在png文件中人为地存储一些信息。
另外文件中的顺序是按照高位在前,低位在后的顺序进行存储的。
ancillary chunks——辅助数据块
PNG文件格式规范制定了10个辅助数据块:
- 背景颜色数据块bKGD(background color)。
- 基色和白色度数据块cHRM(primary chromaticities and white point)。
- 图像γ数据块gAMA(image gamma)。
- 图像直方图数据块hIST(image histogram)。
- 物理像素尺寸数据块pHYs(physical pixel dimensions)。
- 样本有效位数据块sBIT(significant bits)。
- 文本信息数据块tEXt(textual data)。
- 图像最后修改时间数据块tIME (image last-modification time)。
- 图像透明数据块tRNS (transparency)。
- 压缩文本数据块zTXt (compressed textual data)。
如何获取元数据信息?
PNG中数据块的设计如下:
Length | Chunk type | Chunk data | CRC |
---|---|---|---|
4 byte | 4 byte | Length bytes | 4bytes |
-
Length:4字节的无符号整数,值的是data的长度,而不是自己整个chunk的长度。0是一个无效的长度。虽然他是一个无符号整型,但规定一个chunk中数据最多为 2 31 − 1 2^{31}-1 231−1
-
Chunk type:类型被限制为ASCII字符中的英文的大小写,以及数字。但是编码和解码的人必须把这些类型当作固定的二进制值来进行处理。这种方式也为人们读取数据提供了方便。
-
Chunk Data:故名思意,就是存储数据的,数据长度不能为0。
-
CTC:Cyclic Redundancy Check,循环冗余校验。其值按照 x 3 2 + x 2 6 + x 2 3 + x 2 2 + x 1 6 + x 1 2 + x 1 1 + x 1 0 + x 8 + x 7 + x 5 + x 4 + x 2 + x + 1 x^32+x^26+x^23+x^22+x^16+x^12+x^11+x^10+x^8+x^7+x^5+x^4+x^2+x+1 x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1的生成多项式进行计算。
通过这种设计方式,我们每读取到一个chunk,只需要读取前八个字节就可以大体知道这个数据块的大小,以及数据类型。
而要获取更详细的数据,那么就需要对Critical chunk进行进一步的分析。
IHDR
由13个byte构成,格式如下:
名称 | 大小(byte) | 说明 |
---|---|---|
Width | 4 | 图像宽度,像素为单位 |
Height | 4 | 图像高度,像素为单位 |
Bit depth | 1 | 图像深度: 索引彩色图像:1,2,4或8 灰度图像:1,2,4,8或16 真彩色图像:8或16 |
ColorType | 1 | 颜色类型: 0:灰度图像, 1,2,4,8或16 2:真彩色图像,8或16 3:索引彩色图像,1,2,4或8 4:带α通道数据的灰度图像,8或16 6:带α通道数据的真彩色图像,8或16 |
Compression method | 1 | 压缩方法(LZ77派生算法) |
Filter method | 1 | 滤波器方法 |
Interlace method | 1 | 隔行扫描方法: 0::非隔行扫描 1:Adam7隔行扫描方式 |
PLTE
域的名称 | 字节数(byte) | 说明 |
---|---|---|
Red | 1 | 0 = 黑,255 = 红 |
Green | 1 | 0 = 黑,255 = 绿 |
Blue | 1 | 0 = 黑,255 = 蓝 |
IDAT
存储实际的图像数据。
IEND
16进制表示为:00 00 00 00 49 45 4E 44 AE 42 60 82
表示PNG文件或数据流已经结束。
分析实例
下面就以一张PNG图片为例对这些数据进行分析:
有哪些关键数据?
File header
如前文所说,前8个字节为89 50 4E 47 0D 0A 1A 0A。
IHDR
属性 | 实际取值 | 说明 |
---|---|---|
Length | 即块的长度为13 | |
Chunk Type | 该chunk存储的类型为IHDR | |
Width | 图像宽度为 411像素 | |
Height | 图像高度为 403像素 | |
Bit depth | 图像深度为8位 | |
ColorType | 颜色类型:为带α通道数据的真彩色图像 | |
Compression method | 没有进行压缩 | |
Filter method | 滤波方法0 | |
Interlace method | 非隔行扫描 | |
CRC | 冗余校验 |
PLTE
由于该图片采用真彩方式,所以没有调色板。
IDAT
属性 | 实际取值 | 说明 |
---|---|---|
Length | 数据块的长度为8192字节 | |
chunk type | 该块为IDAT | |
data chunk | 较多,不做展示 | 由8192字节的位图信息 |
IEND
如图所示。与标准IEND内容一致。
有哪些辅助数据?
为了方便分析文件中含有那些辅助数据块,编写了以下代码:
#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <set>
using namespace std;
struct fileHeader
{
unsigned char head[8];
void GetHead( ifstream &in ) { in.read((char *)head,8); }
void Print()
{
for(auto i : head ) cout << (int) i<< " ";
cout << endl;
}
}file_header;
struct chunks
{
unsigned int length = 0;
char type[4];
string s_type = "";
unsigned char *data;
unsigned char CRC[4];
void GetChunk( ifstream &in )
{
unsigned char* buffer = new unsigned char[4];
in.read((char*)buffer,4);
for( int i = 0; i < 4; ++ i ) { length = (length << 8) + buffer[i]; }
in.read(type,4);
if( length != 0 ) {
data = new unsigned char[length];
in.read((char *) data, length);
}
in.read((char*)CRC,4);
for( auto i : type ) s_type += (int)i;
return;
}
};
const string path = "0.png";
map< string, vector<int> > mp;
set<string> ancillary_types;
int main()
{
ifstream in(path,ios::binary);
file_header.GetHead(in);
vector<chunks> v;
int pos = -1;
while(1)
{
chunks c_tmp;
c_tmp.GetChunk(in);
v.push_back(c_tmp); pos ++;
mp[c_tmp.s_type].push_back(pos);
if(c_tmp.s_type=="IEND") break;
if(c_tmp.s_type != "IDAT"&& c_tmp.s_type != "IHDR" && c_tmp.s_type != "PLTE")
{
ancillary_types.insert(c_tmp.s_type);
}
}
cout << "All Chunks information:" << endl;
for( auto &i : mp )
{
cout << "Chunk type: " << i.first << ", Chunk position: ";
auto &v = i.second;
for( auto &j : v ) cout << j << " ";
cout << endl;
}
cout << "ancillary chunks:" << endl;
for( auto i : ancillary_types )
cout << i << endl;
}
由于上面的PNG文件中没有辅助信息,所以使用另一个PNG文件进行分析。
图片如下:
运行代码可得到如下结果:
发现这个文件只有一个tIME辅助块。
我们可以查阅官方文档来对辅助数据块进行具体分析。
tIME给出这个图像最后一次被编辑的时间。格式如下:
内容 | 大小(byte) |
---|---|
Year | 2 |
Month | 1 |
Day | 1 |
Hour | 1 |
Minute | 1 |
Second | 1 |
使用的是UTC或者是GMT而不是地方时。
为了查看该文件的tIME信息,可以增添代码:
定义:
struct tIME
{
int year, month, day, hour, minute, second;
void GetTime(unsigned char *buffer)
{
year = ((buffer[0]) << 8)+buffer[1];
int pos = 2;
for( auto i : {&month,&day,&hour,&minute,&second} ) *i = buffer[pos++];
}
void Print()
{
cout << year << "-" << month << "-" << day << " " << hour << ":" << minute << ":" << second << endl;
}
}time;
主函数中:
auto position = mp["tIME"][0];
time.GetTime(v[position].data);
time.Print();
可得到结果:
即图片最后一次被修改的时间为2016-8-31 17:45:57。
对于其他关键数据块或者辅助数据块的分析可以按照相同方式进行。
总结
为了方便数据的交换、管理、编辑和呈现,PNG文件使用了数据块这种形式。
数据块的结构简单,并且可以很方便的对每块信息进行修改、存储、编辑;同时为了方便显示,只要求解码器可以识别关键数据块,这对兼容性的实现有莫大帮助,即以前的版本的解码器依旧可以打开新版本的图片,只是损失部分新的特征,而核心内容的显示是完全没问题的;同时可以非常方便的对文件进行拓展,甚至可以自己定义一些信息,一些对图像的操作、存储一些其他数据——只要自己再编写出相应的解码器即可。
例如,多年前很火的游戏孢子(spore),里边所有人物的模型都是通过png格式进行存储的,也就是自己定义了一些chunk类型对数据进行存储。
参考资料: Portable Network Graphics (PNG) Specification (Second Edition)