PNG文件結構分析(上:了解PNG文件存儲格式)
前言
我們都知道,在進行J2ME的手機應用程序開發的時候,在圖片的使用上,我們能夠使用PNG格式的圖片(甚至於在有的手機上,我們僅僅能夠使用PNG格式的圖片),雖然使用圖片能夠為我們的應用程序添加不少亮點,然而,僅僅支持PNG格式的圖片卻又限制了我們進一步發揮的可能性(事實上,應該說是因為手機平台上的處理能力有限)。 在MIDP2中,或者某些廠商(如NOKIA)提供的API中,提供了drawPixels/getPixels的方法,這些方法進一步提高了開發人員處理圖片的靈活性,然而,在MIDP2還未全然普及的今天,我們須要在MIDP1 .0中實現這類方法還屬於異想天開,因此,為了實現更高級的應用,我們必須充分挖掘PNG的潛力。
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數據塊 |
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個數據塊按以上先后順序進行存儲,而且都僅僅出現一次。
數據塊結構
PNG文件里,每一個數據塊由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
以下,我們依次來了解一下各個重要數據塊的結構吧。
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遍隔行掃描方法) |
因為我們研究的是手機上的PNG,因此,首先我們看看MIDP1.0對所使用PNG圖片的要求吧:
- 在MIDP1.0中,我們僅僅能夠使用1.0版本號的PNG圖片。而且,所以的PNG重要數據塊都有特別要求:
IHDR - 文件大小:MIDP支持隨意大小的PNG圖片,然而,實際上,假設一個圖片過大,會因為內存耗盡而無法讀取。
- 顏色類型:全部顏色類型都有被支持,盡管這些顏色的顯示依賴於實際設備的顯示能力。同一時候,MIDP也能支持alpha通道,可是,全部的alpha通道信息都會被忽略而且當作不透明的顏色對待。
- 色深:全部的色深都能被支持。
- 壓縮方法:僅支持壓縮方式0(deflate壓縮方式),這和jar文件的壓縮方式全然同樣,所以,PNG圖片數據的解壓和jar文件的解壓能夠使用同樣的代碼。(事實上這也就是為什么J2ME能非常好的支持PNG圖像的原因:))
- 濾波器方法:雖然在PNG的白皮書中僅定義了方法0,然而全部的5種方法都被支持!
- 隔行掃描:盡管MIDP支持0、1兩種方式,然而,當使用隔行掃描時,MIDP卻不會真正的使用隔行掃描方式來顯示。
- PLTE chunk:支持
- IDAT chunk:圖像信息必須使用5種過濾方式中的方式0 (None, Sub, Up, Average, Paeth)
- IEND chunk:當IEND數據塊被找到時,這個PNG圖像才覺得是合法的PNG圖像。
- 可選數據塊:MIDP能夠支持下列輔助數據塊,然而,這卻不是必須的。
bKGD cHRM gAMA hIST iCCP iTXt pHYs
sBIT sPLT sRGB tEXt tIME tRNS zTXt
關於很多其它的信息,能夠參考http://www.w3.org/TR/REC-png.html
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圖像不合法。
真彩色圖像和帶α通道數據的真彩色圖像也能夠有調色板數據塊,目的是便於非真彩色顯示程序用它來量化圖像數據,從而顯示該圖像。
IDAT
圖像數據塊IDAT(image data chunk):它存儲實際的數據,在數據流中可包括多個連續順序的圖像數據塊。
IDAT存放着圖像真正的數據信息,因此,假設可以了解IDAT的結構,我們就行非常方便的生成PNG圖像。
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
下面是由Fireworks生成的一幅圖像,圖像大小為8*8,
為了方便大家觀看,我們將圖像放大:

使用UltraEdit32打開該文件,例如以下:
00000000~00000007:

能夠看到,選中的頭8個字節即為PNG文件的標識。
接下來的地方就是IHDR數據塊了:
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色的圖像(也有可能顏色數不超過16,當然,假設顏色數不超過8,用03表示更合適)
- 03 顏色類型,索引圖像
- 00 PNG Spec規定此處總為0(非0值為將來使用更好的壓縮方法預留),表示使壓縮方法(LZ77派生算法)
- 00 同上
- 00 非隔行掃描
- 36 21 A3 B8 CRC校驗
00000021~0000002F:

可選數據塊sBIT,顏色採樣率,RGB都是256(2^8=256)
00000030~00000062:

這里是調色板信息
- 00 00 00 27 說明調色板數據長為39字節,既13個顏色數
- 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兩種類型的數據塊共3塊,因為並不太重要,因此也不再具體描寫敘述了。
000000C0~000000F8:

以上選中部分是IDAT數據塊
- 00 00 00 27 數據長為39字節
- 49 44 41 54 IDAT標識
- 78 9C…… 壓縮的數據,LZ77派生壓縮方法
- DA 12 06 A5 CRC校驗
IDAT中壓縮數據部分在后面會有具體的介紹。
000000F9~00000104:

IEND數據塊,這部分正如上所說,通常都應該是
00 00 00 00 49 45 4E 44 AE 42 60 82
至此,我們已經能夠從一個PNG文件里識別出各個數據塊了。因為PNG中規定除重要數據塊外,其他的輔助數據塊都為可選部分,因此,有了這個標准后,我們能夠通過刪除全部的輔助數據塊來降低PNG文件的大小。(當然,須要注意的是,PNG格式能夠保存圖像中的層、文字等信息,一旦刪除了這些輔助數據塊后,圖像將失去原來的可編輯性。)

刪除了輔助數據塊后的PNG文件,如今文件大小為147字節,原文件大小為261字節,文件大小降低后,並不影響圖像的內容。
事實上,我們能夠通過改變調色板的色值來完畢一些又趣的事情,比方說實現雲彩/水波的流動效果,實現圖像的淡入淡出效果等等,在此,給出一個鏈接給大家看或許更直接:http://blog.csdn.net/flyingghost/archive/2005/01/13/251110.aspx,我寫此文也就是受此文的啟示的。
如上說過,IDAT數據塊是使用了LZ77壓縮算法生成的,因為受限於手機處理器的能力,因此,假設我們在生成IDAT數據塊時仍然使用LZ77壓縮算法,將會使效率大打折扣,因此,為了效率,僅僅能使用無壓縮的LZ77算法,關於LZ77算法的詳細實現,此文不打算深究,假設你對LZ77算法的JAVA實現有興趣,能夠參考下面兩個網站:
PNG文件結構分析(下:在手機上生成PNG文件)
(已閱讀 次)
上面我們已經對PNG的存儲格式有了了解,因此,生成PNG圖片僅僅須要依照以上的數據塊寫入文件就可以。
(因為IHDR、PLTE的結構都很easy,因此,這里我們僅僅是重點講一講IDAT的生成方法,IHDR和PLTE的數據內容都沿用以上的數據內容)
問題確實是這種,我們知道,對於大多數的圖形文件來說,我們都能夠將實際的圖像內容映射為一個二維的顏色數組,對於上面的PNG文件,因為它用的是16色的調色板(實際是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文件不是採用隔行掃描方法存儲的話,那么,數據是依照行(ScanLine)來存儲的,為了區分第一行,PNG規定在每一行的前面加上0以示區分,因此,上面的圖像映射應該例如以下:
| 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在存儲圖像時為了節省空間,因此每一行是依照位(Bit)來存儲的,而並非我們想象的字節(Byte),假設你沒有忘記的話,我們的IHDR數據塊中的色深就指明了這一點,所以,為了湊成PNG所須要的IDAT,我們的數據得改成例如以下:
| 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 |
最后,我們對這些數據進行LZ77壓縮就能夠得到IDAT的正確內容了。
然而,事情並非這么簡單,由於我們研究的是手機上的PNG,假設須要在手機上完畢LZ77壓縮工作,消耗的時間是可想而知的,因此,我們得再想辦法加降低壓縮時消耗的時間。
好在LZ77也提供了無壓縮的壓縮方法(奇怪吧?),因此,我們僅僅須要簡單的使用無壓縮的方式寫入數據就能夠了,這樣盡管浪費了空間,卻換回了時間!
好了,讓我們看一看怎么樣湊成無壓縮的LZ77壓縮塊:
|
字節
|
意義
|
| 0~2 | 壓縮信息,固定為0x78, 0xda, 0x1 |
| 3~6 | 壓縮塊的LEN和NLEN信息 |
|
壓縮的數據
|
|
| 最后4字節 | Adler32信息 |
當中的LEN是指數據的長度,占用兩個字節,對於我們的圖像來說,第一個Scan Line包括了5個字節(如第一行的0, 203, 169, 135, 101),所以LEN的值為5(字節/行) * 8(行) = 40(字節),生成字節為28 00(低字節在前),NLEN是LEN的補碼,即NLEN = LEN ^ 0xFFFF,所以NLEN的為 D7 FF,Adler32信息為24 A7 0B A4(詳細算法見源程序),因此,依照這種順序,我們生成IDAT數據塊,最后,我們將IHDR、PLTE、IDAT和IEND數據塊寫入文件里,就能夠得到PNG文件了,如圖:

(選中的部分為生成的“壓縮”數據)
至此,我們已經可以採用最快的時間將數組轉換為PNG圖片了。
參考資料:
PNG文件格式白皮書:http://www.w3.org/TR/REC-png.html
為數不多的中文PNG格式說明:http://dev.gameres.com/Program/Visual/Other/PNGFormat.htm
RFC-1950(ZLIB Compressed Data Format Specification):ftp://ds.internic.net/rfc/rfc1950.txt
RFC-1950(DEFLATE Compressed Data Format Specification):ftp://ds.internic.net/rfc/rfc1951.txt
LZ77算法的JAVA實現:http://jazzlib.sourceforge.net/
LZ77算法的JAVA實現,包含J2ME版本號:http://www.jcraft.com/jzlib/index.html
