详解PNG文件结构
前言
PNG,JPEG,GIF,BMP作為數(shù)據(jù)壓縮文件,有許多重要的信息我們需要區(qū)深度解析。
一.PNG的文件結(jié)構(gòu)
1.1、數(shù)據(jù)塊構(gòu)成結(jié)構(gòu)
PNG文件結(jié)構(gòu)很簡單,主要有數(shù)據(jù)塊(Chunk Block)組成,最少包含4個數(shù)據(jù)塊。
| PNG標識符 | PNG數(shù)據(jù)塊(IHDR) | PNG數(shù)據(jù)塊(其他類型數(shù)據(jù)塊) | ... | PNG結(jié)尾數(shù)據(jù)塊(IEND) |
1.2、所有PNG數(shù)據(jù)塊(Chunk)
PNG定義了兩種類型的數(shù)據(jù)塊,一種是稱為關(guān)鍵數(shù)據(jù)塊(critical chunk),這是標準的數(shù)據(jù)塊,另一種叫做輔助數(shù)據(jù)塊(ancillary chunks),這是可選的數(shù)據(jù)塊。關(guān)鍵數(shù)據(jù)塊定義了4個標準數(shù)據(jù)塊,每個PNG文件都必須包含它們,PNG讀寫軟件也都必須要支持這些數(shù)據(jù)塊。雖然PNG文件規(guī)范沒有要求PNG編譯碼器對可選數(shù)據(jù)塊進行編碼和譯碼,但規(guī)范提倡支持可選數(shù)據(jù)塊。
下表就是PNG中數(shù)據(jù)塊的類別,其中,關(guān)鍵數(shù)據(jù)塊部分我們使用深色背景加以區(qū)分。
|
PNG文件格式中的數(shù)據(jù)塊 |
||||
|
數(shù)據(jù)塊符號 |
數(shù)據(jù)塊名稱 |
多數(shù)據(jù)塊 |
可選否 |
位置限制 |
| IHDR | 文件頭數(shù)據(jù)塊 | 否 | 否 | 第一塊 |
| cHRM | 基色和白色點數(shù)據(jù)塊 | 否 | 是 | 在PLTE和IDAT之前 |
| gAMA | 圖像γ數(shù)據(jù)塊 | 否 | 是 | 在PLTE和IDAT之前 |
| sBIT | 樣本有效位數(shù)據(jù)塊 | 否 | 是 | 在PLTE和IDAT之前 |
| PLTE | 調(diào)色板數(shù)據(jù)塊 | 否 | 是 | 在IDAT之前 |
| bKGD | 背景顏色數(shù)據(jù)塊 | 否 | 是 | 在PLTE之后IDAT之前 |
| hIST | 圖像直方圖數(shù)據(jù)塊 | 否 | 是 | 在PLTE之后IDAT之前 |
| tRNS | 圖像透明數(shù)據(jù)塊 | 否 | 是 | 在PLTE之后IDAT之前 |
| oFFs | (專用公共數(shù)據(jù)塊) | 否 | 是 | 在IDAT之前 |
| pHYs | 物理像素尺寸數(shù)據(jù)塊 | 否 | 是 | 在IDAT之前 |
| sCAL | (專用公共數(shù)據(jù)塊) | 否 | 是 | 在IDAT之前 |
| IDAT | 圖像數(shù)據(jù)塊 | 是 | 否 | 與其他IDAT連續(xù) |
| tIME | 圖像最后修改時間數(shù)據(jù)塊 | 否 | 是 | 無限制 |
| tEXt | 文本信息數(shù)據(jù)塊 | 是 | 是 | 無限制 |
| zTXt | 壓縮文本數(shù)據(jù)塊 | 是 | 是 | 無限制 |
| fRAc | (專用公共數(shù)據(jù)塊) | 是 | 是 | 無限制 |
| gIFg | (專用公共數(shù)據(jù)塊) | 是 | 是 | 無限制 |
| gIFt | (專用公共數(shù)據(jù)塊) | 是 | 是 | 無限制 |
| gIFx | (專用公共數(shù)據(jù)塊) | 是 | 是 | 無限制 |
| IEND | 圖像結(jié)束數(shù)據(jù) | 否 | 否 | 最后一個數(shù)據(jù)塊 |
1.3、數(shù)據(jù)塊結(jié)構(gòu)
PNG文件中,每個數(shù)據(jù)塊由4個部分組成,如下:
|
名稱 |
字節(jié)數(shù) |
說明 |
|
Length (長度) |
4字節(jié) |
指定數(shù)據(jù)塊中數(shù)據(jù)域的長度,其長度不超過(231-1)字節(jié) |
|
Chunk Type Code (數(shù)據(jù)塊類型碼) |
4字節(jié) |
數(shù)據(jù)塊類型碼由ASCII字母(A-Z和a-z)組成的“數(shù)據(jù)塊符號” |
|
Chunk Data (數(shù)據(jù)塊數(shù)據(jù)) |
可變長度 |
存儲按照Chunk Type Code指定的數(shù)據(jù) |
|
CRC (循環(huán)冗余檢測) |
4字節(jié) |
存儲用來檢測是否有錯誤的循環(huán)冗余碼 |
CRC(cyclic redundancy check)域中的值是對Chunk Type Code域和Chunk Data域中的數(shù)據(jù)進行計算得到的。CRC具體算法定義在ISO 3309和ITU-T V.42中,其值按下面的CRC碼生成多項式進行計算:
x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1
CRC: 一種校驗算法。僅僅用來校驗數(shù)據(jù)的正確性的,這里因為使用了4個字節(jié),說明使用的是CRC32標準算法。
二.PNG圖像標識符
根據(jù)PNG文件的定義來說,其文件頭位置總是由位固定的字節(jié)來描述的:
|
十進制數(shù) |
137 80 78 71 13 10 26 10 |
|
十六進制數(shù) |
89 50 4E 47 0D 0A 1A 0A |
JPEG,PNG,GIF,BMP等圖片都具有不同的圖像標識符號,判讀一個文件的正確mimeType類型,更應(yīng)該通過標識符,而不是通過后綴名判斷,下面這種方法是不可靠的,因為后綴名可以隨便修改。
boolean isPNG = filename.endsWith(".png");
同樣,jdk本身提供api判斷文件 mime type依舊有問題的,他同樣是根據(jù)后綴名判斷,甚至不去檢測文件是否存在。
String contentTypeFor = URLConnection.getFileNameMap().getContentTypeFor("123.gif");
三.IHDR數(shù)據(jù)塊
文件頭數(shù)據(jù)塊IHDR(header chunk):它包含有PNG文件中存儲的圖像數(shù)據(jù)的基本信息,并要作為第一個數(shù)據(jù)塊出現(xiàn)在PNG數(shù)據(jù)流中,而且一個PNG數(shù)據(jù)流中只能有一個文件頭數(shù)據(jù)塊。
文件頭數(shù)據(jù)塊由13字節(jié)組成,它的格式如下表所示。
|
域的名稱 |
字節(jié)數(shù) |
說明 |
|
Width |
4 bytes |
圖像寬度,以像素為單位 |
|
Height |
4 bytes |
圖像高度,以像素為單位 |
|
Bit depth |
1 byte |
圖像深度: |
|
ColorType |
1 byte |
顏色類型: |
|
Compression method |
1 byte |
壓縮方法(LZ77派生算法) |
|
Filter method |
1 byte |
濾波器方法 |
|
Interlace method |
1 byte |
隔行掃描方法: |
由于本文很多設(shè)計到了PNG在手機方面的應(yīng)用,因此在此提出MIDP1.0對所使用PNG圖片的要求:
在MIDP1.0中,只可以使用1.0版本的PNG圖片。
文件大?。篗IDP支持任意大小的PNG圖片,然而實際上,如果一個圖片過大,會由于內(nèi)存耗盡而無法讀取。
顏色類型:所有顏色類型都有被支持,雖然這些顏色的顯示依賴于實際設(shè)備的顯示能力。同時,MIDP也能支持alpha通道,但是,所有的alpha通道信息都會被忽略并且當作不透明的顏色對待。
色深:所有的色深都能被支持。
壓縮方法:僅支持deflate壓縮方式,這和jar文件的壓縮方式完全相同,所以,PNG圖片數(shù)據(jù)的解壓和jar文件的解壓可以使用相同的代碼。
濾波器方法:在PNG中所有的5種方法都被支持。
隔行掃描:雖然MIDP支持0、1兩種方式,然而,當使用隔行掃描時,MIDP卻不會真正的使用隔行掃描方式來顯示。
PLTE chunk:支持
IDAT chunk:圖像信息必須使用5種過濾方式中的方式之一 (None, Sub, Up, Average, Paeth)
IEND chunk:當IEND數(shù)據(jù)塊被找到時,這個PNG圖像才認為是合法的PNG圖像。
可選數(shù)據(jù)塊:MIDP可以支持下列輔助數(shù)據(jù)塊,然而,這卻不是必須的。
bKGD cHRM gAMA hIST iCCP iTXt pHYs
sBIT sPLT sRGB tEXt tIME tRNS zTXt
PLTE
調(diào)色板數(shù)據(jù)塊PLTE(palette chunk)包含有與索引彩色圖像(indexed-color image)相關(guān)的彩色變換數(shù)據(jù),它僅與索引彩色圖像有關(guān),而且要放在圖像數(shù)據(jù)塊(image data chunk)之前。
PLTE數(shù)據(jù)塊是定義圖像的調(diào)色板信息,PLTE可以包含1~256個調(diào)色板信息,每一個調(diào)色板信息由3個字節(jié)組成:
|
顏色 |
字節(jié) |
意義 |
|
Red |
1 byte |
0 = 黑色, 255 = 紅 |
|
Green |
1 byte |
0 = 黑色, 255 = 綠色 |
|
Blue |
1 byte |
0 = 黑色, 255 = 藍色 |
因此,調(diào)色板的長度應(yīng)該是3的倍數(shù),否則,這將是一個非法的調(diào)色板。
對于索引圖像,調(diào)色板信息是必須的,調(diào)色板的顏色索引從0開始編號,然后是1、2……,調(diào)色板的顏色數(shù)不能超過色深中規(guī)定的顏色數(shù)(如圖像色深為4的時候,調(diào)色板中的顏色數(shù)不可以超過2^4=16),否則,這將導(dǎo)致PNG圖像不合法。
真彩色圖像和帶alpha通道數(shù)據(jù)的真彩色圖像也可以有調(diào)色板數(shù)據(jù)塊,目的是便于非真彩色顯示程序用它來量化圖像數(shù)據(jù),從而顯示該圖像。
IDAT
圖像數(shù)據(jù)塊IDAT(image data chunk):它存儲實際的數(shù)據(jù),在數(shù)據(jù)流中可包含多個連續(xù)順序的圖像數(shù)據(jù)塊。
IDAT存放著圖像真正的數(shù)據(jù)信息,因此,如果能夠了解IDAT的結(jié)構(gòu),我們就可以很方便的生成PNG圖像。
IEND
圖像結(jié)束數(shù)據(jù)IEND(image trailer chunk):它用來標記PNG文件或者數(shù)據(jù)流已經(jīng)結(jié)束,并且必須要放在文件的尾部。
如果我們仔細觀察PNG文件,我們會發(fā)現(xiàn),文件的結(jié)尾12個字符看起來總應(yīng)該是這樣的:
00 00 00 00 49 45 4E 44 AE 42 60 82
不難明白,由于數(shù)據(jù)塊結(jié)構(gòu)的定義,IEND數(shù)據(jù)塊的長度總是0(00 00 00 00,除非人為加入信息),數(shù)據(jù)標識總是IEND(49 45 4E 44),因此,CRC碼也總是AE 42 60 82。
實例研究PNG
以下是由Fireworks生成的一幅圖像,圖像大小為8*8,
為了方便觀看,將圖像放大:
使用UltraEdit32或者WinHex打開該文件,如下:
00000000~00000007:
可以看到,選中的頭8個字節(jié)即為PNG文件的標識。
接下來的地方就是IHDR數(shù)據(jù)塊了:
00000008~00000020:
00 00 00 0D 說明IHDR頭塊長為13
49 48 44 52 IHDR標識
00 00 00 08 圖像的寬,8像素
00 00 00 08 圖像的高,8像素
04 色深,2^4=16,即這是一個16色的圖像(也有可能顏色數(shù)不超過16,當然,如果顏色數(shù)不超過8,用03表示更合適)
03 顏色類型,索引圖像
00 PNG Spec規(guī)定此處總為0(非0值為將來使用更好的壓縮方法預(yù)留),表示使壓縮方法(LZ77派生算法)
00 同上
00 非隔行掃描
36 21 A3 B8 CRC校驗
CRC校驗代碼如下:
import java.util.zip.CRC32;
public class CrcTest {
public static void main(String[] args) {
byte[] checkData = new byte[]{0x49,0x48,0x44,0x52,0x00,0x00,0x00, 0x08,0x00,0x00,0x00, 0x08,0x04,0x03,0x00,0x00,0x00};
CRC32 crc32 = new CRC32();
crc32.update(checkData);
long value = crc32.getValue();
byte[] intToBytes = longToBytes(value);
String bytesToHexString = bytesToHexString(intToBytes);
System.out.println(bytesToHexString);
}
public static byte[] longToBytes(long value)
{
byte[] src = new byte[4];
src[0] = (byte) ((value>>24) & 0xFF);
src[1] = (byte) ((value>>16)& 0xFF);
src[2] = (byte) ((value>>8)&0xFF);
src[3] = (byte) (value & 0xFF);
return src;
}
//將字節(jié)數(shù)組按16進制輸出
public static String bytesToHexString(byte[] src){
StringBuilder stringBuilder = new StringBuilder("");
if (src == null || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++)
{
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (stringBuilder.length() != 0) {
stringBuilder.append(",");
}
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
}
00000021~0000002F:
可選數(shù)據(jù)塊sBIT,顏色采樣率,RGB都是256(2^8=256)
00000030~00000062:
這里是調(diào)色板信息
00 00 00 27 說明調(diào)色板數(shù)據(jù)長為39字節(jié),既13個顏色數(shù)
50 4C 54 45 PLTE標識
FF FF 00 顏色0
FF ED 00 顏色1
…… ……
09 00 B2 最后一個顏色,12
5F F5 BB DD CRC校驗
00000063~000000C5:
這部分包含了pHYs、tExt兩種類型的數(shù)據(jù)塊共3塊,由于并不太重要,因此也不再詳細描述了。
000000C0~000000F8:
以上選中部分是IDAT數(shù)據(jù)塊
00 00 00 27 數(shù)據(jù)長為39字節(jié)
49 44 41 54 IDAT標識
78 9C…… 壓縮的數(shù)據(jù),LZ77派生壓縮方法
DA 12 06 A5 CRC校驗
IDAT中壓縮數(shù)據(jù)部分在后面會有詳細的介紹。
000000F9~00000104:
IEND數(shù)據(jù)塊,這部分正如上所說,通常都應(yīng)該是
00 00 00 00 49 45 4E 44 AE 42 60 82
至此,我們已經(jīng)能夠從一個PNG文件中識別出各個數(shù)據(jù)塊了。由于PNG中規(guī)定除關(guān)鍵數(shù)據(jù)塊外,其它的輔助數(shù)據(jù)塊都為可選部分,因此,有了這個標準后,我們可以通過刪除所有的輔助數(shù)據(jù)塊來減少PNG文件的大小。(當然,需要注意的是,PNG格式可以保存圖像中的層、文字等信息,一旦刪除了這些輔助數(shù)據(jù)塊后,圖像將失去原來的可編輯性。)
刪除了輔助數(shù)據(jù)塊后的PNG文件,現(xiàn)在文件大小為147字節(jié),原文件大小為261字節(jié),文件大小減少后,并不影響圖像的內(nèi)容。參考:打造自由換色的png圖片類。
如上說過,IDAT數(shù)據(jù)塊是使用了LZ77壓縮算法生成的,由于受限于手機處理器的能力,因此,如果我們在生成IDAT數(shù)據(jù)塊時仍然使用LZ77壓縮算法,將會使效率大打折扣,因此,為了效率,只能使用無壓縮的LZ77算法,關(guān)于LZ77算法的具體實現(xiàn),此文不打算深究,如果你對LZ77算法的JAVA實現(xiàn)有興趣,可以參考以下兩個站點:
http://jazzlib.sourceforge.net/
http://www.jcraft.com/jzlib/index.html
四.PNG文件結(jié)構(gòu)分析(下:在手機上生成PNG文件)
上面我們已經(jīng)對PNG的存儲格式有了了解,因此,生成PNG圖片只需要按照以上的數(shù)據(jù)塊寫入文件即可。
(由于IHDR、PLTE的結(jié)構(gòu)都非常簡單,因此,這里我們只是重點講一講IDAT的生成方法,IHDR和PLTE的數(shù)據(jù)內(nèi)容都沿用以上的數(shù)據(jù)內(nèi)容)
問題確實是這樣的,我們知道,對于大多數(shù)的圖形文件來說,我們都可以將實際的圖像內(nèi)容映射為一個二維的顏色數(shù)組,對于上面的PNG文件,由于它用的是16色的調(diào)色板(實際是13色),因此,對于圖片的映射可以如下:
| 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 |
| 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 |
| 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 |
| 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 |
| 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 6 | 5 | 4 | 3 | 2 | 1 | 0 | 0 |
| 5 | 4 | 3 | 2 | 1 | 0 | 0 | 0 |
PNG Spec中指出,如果PNG文件不是采用隔行掃描方法存儲的話,那么,數(shù)據(jù)是按照行(ScanLine)來存儲的,為了區(qū)分第一行,PNG規(guī)定在每一行的前面加上0以示區(qū)分,因此,上面的圖像映射應(yīng)該如下:
| 0 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 |
| 0 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 |
| 0 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 |
| 0 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 |
| 0 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
| 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| 0 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 0 |
| 0 | 5 | 4 | 3 | 2 | 1 | 0 | 0 | 0 |
另外,需要注意的是,由于PNG在存儲圖像時為了節(jié)省空間,因此每一行是按照位(Bit)來存儲的,而并不是我們想象的字節(jié)(Byte),如果你沒有忘記的話,我們的IHDR數(shù)據(jù)塊中的色深就指明了這一點,所以,為了湊成PNG所需要的IDAT,我們的數(shù)據(jù)得改成如下:
| 0 | 203 | 169 | 135 | 101 |
| 0 | 186 | 152 | 118 | 84 |
| 0 | 169 | 135 | 101 | 67 |
| 0 | 152 | 118 | 84 | 50 |
| 0 | 135 | 101 | 67 | 33 |
| 0 | 118 | 84 | 50 | 16 |
| 0 | 101 | 67 | 33 | 0 |
| 0 | 84 | 50 | 16 | 0 |
最后,我們對這些數(shù)據(jù)進行LZ77壓縮就可以得到IDAT的正確內(nèi)容了。
然而,事情并不是這么簡單,因為我們研究的是手機上的PNG,如果需要在手機上完成LZ77壓縮工作,消耗的時間是可想而知的,因此,我們得再想辦法加減少壓縮時消耗的時間。
好在LZ77也提供了無壓縮的壓縮方法(奇怪吧?),因此,我們只需要簡單的使用無壓縮的方式寫入數(shù)據(jù)就可以了,這樣雖然浪費了空間,卻換回了時間!
好了,讓我們看一看怎么樣湊成無壓縮的LZ77壓縮塊:
|
字節(jié) |
意義 |
| 0~2 | 壓縮信息,固定為0x78, 0xda, 0x1 |
| 3~6 | 壓縮塊的LEN和NLEN信息 |
|
壓縮的數(shù)據(jù) |
|
| 最后4字節(jié) | Adler32信息 |
其中的LEN是指數(shù)據(jù)的長度,占用兩個字節(jié),對于我們的圖像來說,第一個Scan Line包含了5個字節(jié)(如第一行的0, 203, 169, 135, 101),所以LEN的值為5(字節(jié)/行) * 8(行) = 40(字節(jié)),生成字節(jié)為28 00(低字節(jié)在前),NLEN是LEN的補碼,即NLEN = LEN ^ 0xFFFF,所以NLEN的為 D7 FF,Adler32信息為24 A7 0B A4(具體算法見源程序),因此,按照這樣的順序,我們生成IDAT數(shù)據(jù)塊,最后,我們將IHDR、PLTE、IDAT和IEND數(shù)據(jù)塊寫入文件中,就可以得到PNG文件了,如圖:
(選中的部分為生成的“壓縮”數(shù)據(jù))
至此,我們已經(jīng)能夠采用最快的時間將數(shù)組轉(zhuǎn)換為PNG圖片了。
五.參考文獻
各種圖片編碼格式詳解
java實現(xiàn) 循環(huán)冗余校驗(CRC)算法
JAVA計算文件的crc32校驗碼
byte[]數(shù)組和int之間的轉(zhuǎn)換
總結(jié)
- 上一篇: Mac电脑如何取消用户的管理员权限电脑如
- 下一篇: 复活节的来历(复活节由来以及习俗!)