PNG文件格式:
PNG文件格式分為PNG-24和PNG-8,其最大的區別是PNG-24是用24位來保存一個像素值,是真彩色,而PNG-8是用8位索引值來在調色盤中索引一個顏色,因為一個索引值的最大上限為2的8次方既128,故調色盤中顏色數最多為128種,所以該文件格式又被叫做PNG-8 128仿色。
PNG-24因為其圖片容量過大,而且在Nokia和Moto等某些機型上創建圖片失敗和顯示不正確等異常時有發生,有時還會嚴重拖慢顯示速度,故並不常用,CoCoMo認為這些異常和平台底層的圖像解壓不無關系。不過該格式最大的優點是可以保存Alpha通道,同事也曾有過利用該圖片格式實現Alpha混合的先例,想來隨著技術的發展,手機硬件平台的提升,Alpha混合一定會被廣泛的應用,到那時該格式的最大優勢才會真正發揮。
PNG-8文件是目前廣泛應用的PNG圖像格式,其主要有六大塊組成:
1.文件頭
2.IHDR塊
3.PLTE塊
4.tRNS塊
5.IDAT塊
6.文件尾
這六大塊按順序排列,也就是說IDAT塊永遠是在PLTE塊之後,期間也會有許多其他的區塊用來描述信息,例如圖像的最後修改時間是多少,圖像的創建者是誰等,不過這些區塊的信息對我們來說都是可有可無的描述信息,故壓縮時一般先向這些區塊開刀。
數據塊:
除了文件頭,其中四大數據塊和文件尾都是由統一的數據塊文件結構描述的:
Chunk Length: 4byte
Chunk Type: 4byte
Chunk Data: Chunk Length的長度
Chunk CRC: 4byte
例如IHDR塊的數據長度為13,既
Chunk Length = 13
Chunk Type = "IHDR"
文件頭:
用來標示PNG文件,為固定的64個字節:0x89504e47 0x0d0a1a0a
IHDR塊:
用來描述圖像的基本信息,其格式為:
圖像寬: 4byte
圖像高: 4byte
圖像色深: 4byte
顏色類型: 1byte
壓縮方法: 1byte
濾波方法: 1byte
掃描方法: 1byte
曾經有人問過我,撒叫濾波方法和掃描方法,汗,說實話我也不知道,不過我們是在做手機游戲,不是在搞圖形學不是嘛。
PLTE塊:
這個就是傳說中放置調色盤數據的地方啦,其格式為:
循環
RED: 1byte
GREEN:1byte
BLUE: 1byte
END
循環長度嘛,不就是Chunk Length / 3的長度嘛,而且Chunk Length一定為3的倍數。
tRNS塊:
這個塊時有時無,主要是看你是否使用了透明色。該區塊的格式為:
循環
if(對應調色盤顏色非透明)
0xFF: 1byte
else
0x00: 1byte
END
循環長度為調色盤的顏色數,相當於調色盤顏色表的一個對應表,標識該顏色是否透明,0xFF不透明,0x00透明。故如果用UltraEdit查看PNG文件的二進制編碼,如果看到一大片FF,一般就是tRNS區塊啦,因為一個PNG文件一般只有一個透明色。
IDAT塊:
這個就是存放圖像數據的地方啦,這裡要注意的是一個PNG文件可能有多個IDAT區塊,而其他三大區塊只可能有一個。
IDAT區塊是經過壓縮的,所以數據不可讀,壓縮算法一般為LZ77滑動窗口算法,如果硬要看裡面的數據的話,用zlib庫也是可以的,CoCoMo當年就見過Windows Mobile上的帝國時代巨變態的用zlib庫壓縮和解壓該區塊來進一步減少PNG文件大小,真是寸K寸金啊。
IEND塊:
該區塊雖然也按照數據塊的結構,但Chunk Data是沒有的,所以是固定的96個字節:0x00000000 0x49454e44 0xae426082
PNG圖像壓縮:
了解了PNG的文件結構,壓縮就有的放矢了。壓縮有6個級別,可以根據需要選擇。
Level1:讀取PNG文件,將除六大塊之外的所有區塊都過濾掉
Level2:文件頭是固定的0x89504e47 0x0d0a1a0a,文件尾是固定的0x00000000 0x49454e44 0xae426082,去掉!
Level3:每個區塊的Chunk Type我們是否需要呢?很明顯,我們自己寫的壓縮格式自己應該清楚是按照什麼樣的順序,去掉!
Level4:每個區塊的Chunk Length我們是否需要呢?
IHDR塊:定長13個字節,明顯不需要,去掉。
PLTE塊:最多128個顏色,為撒要用4byte來記錄區塊長度而不是用1byte來記錄顏色數呢?
tRNS塊:既然有顏色數,tRNS又是調色盤顏色表的對應表,既數量與顏色數相同,為撒還需要呢?
IDAT塊:我想這個是唯一需要4byte來記錄長度的區塊。
Level5:每個區塊的Chunk CRC是否需要呢?
因為計算CRC需要一些時間,但對於字節較少的區塊一般可以忽略不計,所以對於這個問題還是由程序員自己決定吧。對於CRC的計算可以參看CoCoMo的另一篇Blog“PNG文件的CRC碼計算”
Level6:每個區塊我們是否要原封不動的保存期數據呢?
IHDR塊:除了寬、高、色深是需要的,後面那4byte的信息是固定的0x03000000
PLTE塊:為撒要用3byte來表示RGB而不是2byte的565格式?壓縮方法可以參看CoCoMo的另一篇Blog“關於PNG圖像壓縮的一點感悟”
tRNS塊:我想tRNS塊是冗余最多的區塊了吧,大段大段的0xFF明顯沒有必要,一般的PNG文件只有一個透明色,為撒要用對應表的方法而不是一個索引來記錄到底哪個是透明色呢?由於顏色數最多128,所以只需1byte就可以代替tRNS那麼多0xFF啦。
IDAT塊:麼想法,如果你夠變態,把zlib加進來吧!
PNG圖像解壓:
創建了自定義的文件,J2ME端讀取後,就面臨解壓的問題了。我們可以利用此函數來創建Image:static Image
createImage(byte[] imageData, int imageOffset, int imageLength)
前提是傳入的imageData與PNG未被壓縮前的一致。因為PNG文件格式是固定的,所以讀取自定義的壓縮文件後,開始將那些默認的數據再添加進去,實現解壓的目的。下面就開始解壓之旅吧!
首先要創建一個ByteArrayOutputStream out,
1.寫入文件頭:
out.writeInt(0x89504e47);
out.writeInt(0x0d0a1a0a);
2.寫入IHDR塊
out.writeInt(13);
out.writeInt(0x49484452); //0x49484452為Chunk Type "IHDR"
out.writeInt(width);
out.writeInt(height);
out.writeByte(depth);
out.writeInt(0x03000000); //壓縮時捨掉的4byte,默認0x03000000
out.writeInt(crc);
其他區塊方法一致,故略過。。。
3.寫入文件尾
out.writeInt(0x00000000);
out.writeInt(0x49454e44);
out.writeInt(0xae426082);
4.轉換成數組,創建Image
byte[] pngBuffer = out.toByteArray();
Image image = Image.createImage(pngBuffer, 0, pngBuffer.length);
哈哈,大功告成。這裡注意如果中途數據寫入有錯誤,經常會出現創建Image失敗的異常,而且非常不好調試,不過只要自定的壓縮格式定下來後,對應的創建Image的函數只要寫一次,以後基本不會出問題哈。
PNG圖像加解密:
很多人都擔心自己辛苦創作的漂亮的美術圖片很easy就被別人拿到了,究其原因是由於PNG文件格式是固定的,稍微了解的人用UltraEdit很容易就能找到IHDR,PLTE等標識了。CoCoMo就經常看GameLoft的圖像文件,哈哈。一般是2byte的Length,然後緊接著圖片數據,都放在一個文件裡,直接拷貝2進制然後粘貼到一個新文件裡就是一幅圖。後來的加密技術會把PNG分塊,例如前100個字節一塊,緊接著1K一塊,最後剩余字節一塊,然後把塊順序打亂,用2byte來記錄總長度,1byte記錄順序,但是這並沒有從根本上消除IHDR,IEND這些顯眼的定位標識,好像在對破解者說:嘿,看,我就在這裡!
現在了解了之前的壓縮和解壓技術,這個問題也就迎刃而解了,因為Chunk Length,Chunk Type和Chunk CRC這些東西都消失了,甚至連數據塊本身的數據都修改了,我可以按照ImageWidth、ImageHeight、ImageDepth的順序寫數據,也可以倒過來寫。我想再牛的PNG分析器也是無能為力的吧,唯一可以定位的就只有IDAT區塊了,不過就算得到該區塊的數據,也應該是一張黑白圖。
不過在加解密的領域真是道高一尺,魔高一丈,CoCoMo很希望和各位共同探討,共同提高!