PNG圖片是當前移動終端最主流的圖片格式之一,由於android中大部分圖片顏色數比較小而且尺寸也不大,所以在各類app軟件中PNG圖片幾乎是首選的圖片格式。在手Q中PNG圖片大概有四五千張。如此眾多的PNG圖片是android安裝包資源以及內存占用的大頭消費者。大家都知道,在android中,有一種特殊的PNG圖片形式 .9.png,俗稱點九圖,也叫九宮圖。在android平台下使用點九PNG技術,可以將圖片橫向和縱向同時進行拉伸,以實現在多分辨率下的完美顯示效果。但是對這種點九圖,是否還存在優化的空間呢?是否可以進一步剪裁呢?因為本身就可以拉伸擴展,我們的圖片是否可以縮小到極致,另外對顏色區域比較規范的PNG圖片是否也可以轉換為點九圖?一方面可以減少安裝包的大小,同時在繪制展示圖片的時候,也可以節省系統內存。基於這種思路,本文嘗試使用一種工具,來實現png圖片的優化處理。
要進行剪裁處理,首先我們需要了解下PNG圖片的文件格式。
PNG格式分類
PNG格式主要有8位、24位、32位三種形式,其中8位PNG支持兩種不同的透明形式(索引透明和alpha透明,索引透明僅支持全透明,alpha透明支持半透明),24位PNG不支持透明,32位PNG在24位基礎上增加了8位透明通道,因此可展現256級透明程度。
在android中,我們經常用到的是png8和png32兩種格式。
索引透明的png8只有256色,通過一個值表明某個像素點是否透明。
Alpha透明的png8也有256色,但它可以指定某個像素點的透明度,支持半透明。但這種alpha信息在PS中讀取不了。
PNG8(Alpha)只有256色,它通過8位索引值在調色板中索引一個顏色表示,因此在顏色數方面比PNG32少很多,所以占用的空間會比PNG32小很多,而對於大多數的圖標、按鈕和背景等常使用PNG格式的圖片來說,256色基本都可以勝任,因此在我們制作圖片資源的時候,如果圖片質量可以接受盡量使用PNG8格式圖片。一些有損壓縮工具大多也是將小於256色的PNG32圖片壓縮為PNG8(alpha透明)。
PNG文件解析
PNG文件主要由一個文件標示或文件頭域和至少3個數據塊組成。其中文件頭固定為以下格式用來識別PNG:89 50 4E 47 0D 0A 1A 0A(十六進制)
第一個字節0x89超出了ASCII字符表示的范圍,可以避免某些軟件將PNG文件當做文本文件來處理。4E 47 0D三個字節是“PNG”三個字母對應的ASCII碼,后面的四個字節表示了不同格式的換行符等固定的標識。
除了特定的文件頭外,后續是由一個個按照順序排列的PNG的數據塊(Chunk)。
PNG定義了兩種類型的數據塊:一種是PNG文件必須包含、讀寫軟件也都必須要支持的關鍵數據塊(critical chunk),關鍵數據塊定義了4個標准數據塊:IHDR, PLTE, IDAT, IEND;另一種叫做輔助數據塊(ancillary chunks),PNG允許軟件忽略它不認識的附加塊。這種基於數據塊的設計,允許PNG格式在擴展時仍能保持與舊版本兼容。
PNG中數據塊的類別如下表,其中關鍵數據塊使用橙色背景區分:
PNG文件格式中的數據塊 |
||||
數據塊符號 |
數據塊名稱 |
多數據塊 |
可選否 |
位置限制 |
IHDR |
文件頭數據塊 |
否 |
否 |
第一塊 |
cHRM |
基色和白色點數據塊 |
否 |
是 |
在PLTE和IDAT之前 |
gAMA |
圖像Y的數據塊 |
否 |
是 |
在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 |
壓縮文本數據塊 |
是 |
是 |
無限制 |
dRAx |
(專用公共數據塊) |
是 |
是 |
無限制 |
gIFg |
(專用公共數據塊) |
是 |
是 |
無限制 |
gIFt |
(專用公共數據塊) |
是 |
是 |
無限制 |
gIFx |
(專用公共數據塊) |
是 |
是 |
無限制 |
IEND |
圖像結束數據塊 |
否 |
否 |
最后一個數據塊 |
針對於每個數據塊,又主要由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域中的數據進行計算得到的,具體算法可以參照ISO相關文檔。
由上面的表我們發現很多數據塊其實是我們不需要的,所以針對PNG圖片的很多壓縮工具,無損壓縮主要是處理這些非必須的數據塊來減少PNG的體積。下面我們了解下PNG的關鍵數據塊:
IHDR(文件頭數據塊)
該數據塊是第一個數據塊,而且必須是第一個,並且僅能有一個,它包含了PNG圖片的主要信息,主要包括了圖片的寬度和高度(4bytes表示),以及圖片的位深度,顏色類型,壓縮以及掃描方法等(1byte),該數據塊固定為13bytes長度,因此我們可以通過00 00 00 0D 49 48 44 52來定位該數據塊(前四位表示13個字節長度,后四位表示“IHDR”的ASCII碼)。
如上圖所示,圖片寬度為 00 00 00 36即54像素,圖像高度為:00 00 00 20 即32像素。08表示色深為8,28=256色位真彩色,06代表帶alpha通道 8位alpha,即圖片格式為PNG32,壓縮類型0000表示使用Deflate壓縮編碼壓縮圖像數據;接着的00表示為將來使用更好的壓縮方法預留;然后是表示非隔行掃描00后面的59 ca 8b 5b為循環冗余檢測。
PLTE(調色板數據塊)50 4C 54 45
調色板數據塊包含有與索引彩色圖像相關的彩色變換數據,它僅與索引彩色圖像有關(PNG8),該數據塊定義了圖像的調色板信息,可以包含1-256個調色板信息,每個調色板信息包含RGB顏色值。因此調色板長度必須是3的倍數,否則就是非法的調色板。比如,我們有一個png8格式的圖片,在圖像數據塊中,顏色值都是一個個的索引值,當解析和渲染圖片時,我們可以通過這個索引值到PLTE調色板數據塊中查找到相應的RGB顏色值。對應關系如下圖所示:
對於索引圖像,必須要有調色板信息,調色板的顏色索引從0開始編號,顏色數不能超過色深中規定的顏色數(如色深為4,調色板顏色數不能查過24=16),否則導致圖片不合法。
如上圖所示,表明PLTE的長度為15, 50 4C 54 45 為PLTE標示位,后面就是索引數據。一直到 00 00 00,后面的08 88 39 af為循環冗余檢測。
真彩色圖像和帶α通道數據的真彩色圖像也可以有調色板數據塊,目的是便於非真彩色顯示程序用它來量化圖像數據,從而顯示該圖像。
在這里順便介紹下tRNS塊,這個塊可有可無,主要是看是否使用了透明色,該區塊可以理解為透明度的索引板,循環長度為調色盤的顏色數,相當於調色盤顏色表的一個對應表,標識該顏色是否透明,0xFF不透明,0x00透明。
IDAT(圖像數據塊)
圖像數據塊IDAT(Image Data Chunk):它存儲了實際的數據,在PNG文件數據中可包含多個連續順序的圖像數據塊。
如上圖所示: 表明該圖片數據長度為130,其中49 44 41 54 為IDAT數據塊的標示。后面跟着的就是IDAT存放着的圖像真正的數據信息,因此,如果能夠了解IDAT的結構,就可以很方便的生產PNG圖像。IDAT 區塊是經過壓縮的,所以數據不可讀。
IEND(圖像結束數據塊)
圖像結束數據塊(Image Trailer Chunk):用來標識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圖片相比,它只是在四周各增加了一行像素點,增加的像素點只有黑色和透明兩種,黑色像素點標明拉伸區域:最上面的一行像素點通過黑色區域標示水平可拉伸區域,左邊的一列像素點表明垂直拉伸區域,兩者重合部分可以全拉伸。下邊和右邊的像素點標示改圖片用於資源時,其上的內容的填充區域。
因此,既然存在拉伸區域,拉伸前像素點完全相同的若干行拉伸后的效果和單一行拉伸后的效果是一樣的,因此就可以裁剪為一行,這樣標注起這一行可以拉伸,就可以將我們裁剪掉的像素點通過拉伸的方式填充進來,這也是我們裁剪點9圖圖片的主要思路。
同樣,對於PNG圖片,我們可以通過相同的思路,逐行掃描像素點完全相同的行和列,然后只保留一行(列),然后,對PNG圖片四周各添加一行像素值,對保留的行和列通過描點標示可以拉伸,填充區域由於之前是PNG,所以我們設置為全部行和列都可以填充,最后修改圖片的后綴為.9.png,這樣就可以將PNG圖片轉換為點9圖。
Png剪裁實現
在處理PNG圖片時,我們需要逐行掃描出該圖片中哪些行像素點是相同的,秉承不重復造輪子的原則,我們選用了一個第三方的庫。針對png處理的庫有很多,這里我們選擇了完全使用JAVA實現的pngj庫來實現PNG圖片的編解碼。該庫只是個很小的處理單元,不涉及到相關的壓縮處理,當然也沒有庫依賴。因為功能單一,所以處理速度也比較理想。最主要的是它能夠實現逐行讀取圖片的原生數據信息。所以可以允許我們針對每個像素點做處理工作。下面是該庫的一些介紹:
Very efficient in memory usage and speed.
Pure Java (requires 5 or greater).
Small and self contained. No dependencies on third party libraries,
Allows to read/write progressively, row by row. This means that you can process huge images without needing to load them fully in memory.
Reads and writes all PNG color models (true color, gray, palette, all bit depths: 1-2-4-8-16, with-without alpha). Interlaced PNG is supported (though not welcomed) for reading.
Full support for metadata ("chunks") handling.
The format of the pixel data (read and write) is extensible (since 2.0) and very efficient (no double copies).
Supports (since 2.0) asyncronous reading and low level tweaking and extension in the reader.
Note that this is a relatively low-level library. It does not provide any high-level image processing (eg, resizing, colour conversions), nor tries to abstract the concrete image format (as BufferedImage does, for example). In particular, the default format of the scanlines (as wrapped inImageLineInt or ImageLineByte) is not abstract, the meaning of the values depends on the image color model and bitdepth.
實現png剪裁,首先,我們需要設定一個掃描目錄,和一個輸出目錄,掃描目錄可以選擇android工程的drawable文件目錄。
1,首先需要先掃描待裁剪的png圖片。看下是否存在剪裁空間。優化比例是否值得我們做剪裁操作:先初始化PngReader:
PngReader pngr = null;
try{
pngr = new PngReader(file);
}catch(PngjInputException e){
System.err.println(filename + " read error");
return null;
}
pngReader會解析圖片文件的相關結構,獲取到圖片的格式,channel,位深度,alpha,寬高等圖片基本信息存儲到ImgInfo中。獲取這些數據可以計算出原始圖片面積,原始圖片的基本信息等屬性。通過函數
line = (ImageLineInt) pngr.readRow()).getScanline()
逐行掃描圖片的每一行數據,然后將數據存儲到一個int[]數組中:
int[] lineI = Arrays.copyOf(line, pngr.imgInfo.cols * channels);
這個lineI數組,存儲着每個像素點的數據,如果為png8,那么每一個int保存一個像素點數據,如果為png32,每四個int存儲一個像素點,分別代表RGBA數據。這樣可以再接着掃描第二行..第n行,獲取所有像素數據。因此現在的問題就變成了從一個類二維數組中查詢出所有連續相同的列和連續相同的行的問題,這是個純數學的算法問題,這里不再詳述。需要注意的是:因為.9.png四個邊各有一個像素點的擴展區域標示, 表示可拉伸區域和可填充區域。所以在掃描時比對像素點是否相同需要去除這個區域,每行比較時去除第一和最后一個像素點,掃描開始於第二行,結束於n-1行。
通過一輪掃描,我們可以獲取到圖片可剪裁的優化區域:連續相同的列可以合並成一列,連續相同的行可以合並成一行。然后針對合並后保留的那一行(列),標示為可拉伸的區域。因為原始的.9.png也存在拉伸區域,我們這次掃描出的拉伸區域要和原來.9.png的圖片拉伸區域進行一次對比,如果兩者沒有重合·,就忽略處理(防止出現多條間斷的拉伸區間)。如果重合就使用兩者並集。
2,掃描完數據后,為了便於觀察結果,會將每一張圖片的可優化區域通過一個excel表的形式進行輸出,包含:圖片原始大小,圖片可裁剪行列的起止位置。圖片可優化面積,可裁剪的比例等信息。
3,裁剪:需要將連續相同的行(列)去除,只保留一行(列)。去除之后,還需要標示拉伸區域,如果為png還需要轉換為.9.png。
針對邊緣描邊:可拉伸區域為黑色。不可拉伸區域為透明。但是針對png8格式的png圖片,圖片中只保存了各個像素點的索引值,而不是具體的RGB數值。所以針對這種格式的png圖片我們沒法確認黑色值和透明值具體的索引是多少,所以這種格式的圖片只是提供裁剪優化指引,而沒有進行裁剪處理。
針對.9.png。我們在第一輪掃描第一行的時候,可以利用這個時機記錄下兩個值:第一行第一個像素點肯定是透明的,后續我們填充不可拉伸區域就用這個值。第一行里和第一個像素點不同的像素點肯定是記錄了黑色的RGBA或者索引值,我們可以根據這個值來標示我們剪裁后圖像的可拉伸區域。針對png32的png圖片,透明值為:255.255.255.0,黑色為:0.0.0.255.
4,輸出新的圖片
確定了剪裁區域,我們可以把我們新圖的一些屬性信息通過新建一個ImageInfo存儲下來。然后新建pngWriter:
PngWriter pngw = new PngWriter(outFile, newInfo, true);
//如果是索引圖片,需要拷貝全部的Chunk信息,主要是需要包含索引板
if (info.channels == 1 && info.indexed) {
isPalettedImage = true;
pngw.copyChunksFrom(pngr.getChunksList(), ChunkCopyBehaviour.COPY_ALL);
} else {
isPalettedImage = false;
pngw.copyChunksFrom(pngr.getChunksList(), ChunkCopyBehaviour.COPY_ALL_SAFE);
}
讀取原始圖片每一行的數據
ImageLineInt line= (ImageLineInt)pngr.readRow();
int[] oldline = line.getScanline();
按前面的規則修改:(邊界的話,重新確立可拉伸區域,內容,刪除重復的列所對應的像素點,只保留一列)
ImageLineInt lineInt = new ImageLineInt(newInfo, firstPixs);
pngw.writeRow(lineInt);
這樣完成圖片所有數據的更改,生成新圖,輸出到目錄中。針對png32格式的png圖片,我們需要新增四個邊界處的像素點,並修改為.9.png格式為圖片文件。
目前該小工具已經接入手Q中的自動化工具,可以針對手Q中的圖片資源進行掃描,發現可以優化的圖片時,會自動提單給相應的開發。由於這種方式的修改圖片粒度比較低,所以,在應用到手Q中時,我們針對處理進行了一次過濾:設定掃描圖片面積閾值為50*50像素點,單行可優化像素點最低為50,優化比例為大於45%。剔除比較小的優化點。
如下圖展示了輸出的可裁剪圖片的匯總表格:
針對單張圖片優化前后的對比如下:
優化前:優化后:
該png剪裁工具可以高效率實現掃描png圖片是否存在剪裁壓縮空間的目的,並且可以根據掃描出的優化比例自動修正圖片,輸出更節省空間和內存的新圖片。並支持轉換png圖片為.9.png的功能。但同時也存在一定的不足:比如一些特殊的png圖片在特定環境下不需要剪裁; 剪裁的選擇區域有時候不夠智能, png圖片同時存在行和列的優化區域存才可轉換成點9; png8格式的png圖片,使用索引值表征像素點數據所以只能給出裁剪建議,而沒有生成裁剪后的新圖等問題。
轉:http://mp.weixin.qq.com/s?__biz=MzA4MDc5OTg5MA==&mid=401181863&idx=2&sn=e3562efa052c9622d5140d52c4fc25d5&scene=23&srcid=1217QFsx12bDGwGN6dj6kZw2#rd