前言
前文已經講解過如何解析一張png圖片,然而對於掃描演算法裡只是說明了逐行掃描的方式。其實png還支援一種隔行掃描技術,即Adam7隔行掃描演算法。
優劣
使用隔行掃描有什麼好處呢?如果大家有去仔細觀察的話,會發現網路上有一些png圖在載入時可以做到先顯示出比較模糊的圖片,然後逐漸越來越清晰,最後顯示出完整的圖片,類似如下效果:
這就是隔行掃描能帶來的效果。隔行掃描一共會進行1到7次掃描,每一次都是跳著部分畫素點進行掃描的,先掃描到畫素點可以先渲染,每多一次掃描,圖片就會更清晰,到最後一次掃描時就會掃描完所有畫素點,進而渲染出完整的圖片。
當然,也因為要進行跳畫素掃描,整張圖片會儲存更多額外資料而導致圖片大小會稍微變大,具體增加了什麼額外資料下文會進行講解。
生成
要匯出一張基於Adam7隔行掃描的png圖片是非常簡單,我們可以藉助Adobe的神器——PhotoShop(以下簡稱ps)。我們把一張普通的圖片拖入到ps中,然後依次點選【檔案】-【儲存為Web所用的格式】,在彈出的框裡選擇儲存為PNG-24
,然後勾選交錯,最後點選儲存即可。
這裡的交錯就是隻將掃描演算法設為Adam7隔行掃描,如果不勾選交錯,則是普通逐行掃描的png圖片。
原理
Adam7隔行掃描演算法的原理並不難,本質上是將一張png圖片拆分成多張png小圖,然後對這幾張png小圖進行普通的逐行掃描解析,最後將解析出來的畫素資料按照一定的規則進行歸位即可。
分析
在解壓縮完影象資料後就要馬上進行拆圖。拆圖並不難,就是將原本儲存影象資料的Buffer陣列拆分成多個Buffer陣列而已。關鍵的問題是怎麼拆,這時我們先祭上wiki上這張圖:
上面這張圖就說明了每次掃描需要掃描到的畫素,正常來說一張基於Adam7隔行掃描的png圖片是要經歷7次掃描的,不過有些比較小的圖片的實際掃描次數不到7次,這是因為有些掃描因為沒有實際畫素點而落空的原因,所以下面的講解還是以標準的7次掃描來講解,本質上此演算法的程式碼寫出來後,是能相容任何大小的png圖片的,因為演算法本身和圖片大小無關。
7次掃描,其實就回答了上面拆圖的問題:要拆成7張小圖。每張小圖就包含了每次掃描時要歸位的畫素點。
以第一次掃描為例:第一次掃描的規則是從左上角(我們設定此座標為(0,0))開始,那麼它掃描到的下一個點是同一行上一個點往右偏移8個畫素,即(8,0)。以此類推,再下一個點就是(16,0)、(24,0)等。噹噹前行所有符合規則的點都掃描完時則跳到下一個掃描行的起點,即(8,0),也就是說第一次掃描的掃描行也是以8個畫素為偏移單位的。直到所有掃描行都已經掃描完成,我們就可以認為這次掃描已經結束,可以考慮進入第二次掃描。
我們以一張10*10大小的png圖片來舉例,下面每個數字代表一個畫素點,數字的值代表這個點在第幾次掃描時被掃描到:
1 2 3 4 5 6 7 8 9 10 |
1 6 4 6 2 6 4 6 1 6 7 7 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7 7 7 3 6 4 6 3 6 4 6 3 6 7 7 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7 7 7 1 6 4 6 2 6 4 6 1 6 7 7 7 7 7 7 7 7 7 7 |
按照規則,在第一次掃描時我們會掃描到4個畫素點,我們把這4個畫素點單獨抽離出來合在一起,就是我們要拆的第一張小圖:
1 2 3 4 5 6 7 8 9 10 |
(1) 6 4 6 2 6 4 6 (1) 6 7 7 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7 7 7 1 1 3 6 4 6 3 6 4 6 3 6 ==> 1 1 7 7 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7 7 7 (1) 6 4 6 2 6 4 6 (1) 6 7 7 7 7 7 7 7 7 7 7 |
也就是說,我們的第一張小圖就是2*2大小的png圖片。後面的小圖大小以此類推,這樣我們就能得知拆圖的依據了。
拆圖
上面有提到,拆圖本質上就是把存放圖片資料的Buffer陣列進行切分,在nodejs裡的Buffer物件有個很好用的方法——slice,它的用法和陣列的同名方法一樣。
直接用上面的例子,我們的第一張小圖是2*2點png圖片,在假設我們一個畫素點所佔的位元組數是3個,那麼我們要切出來的第一個Buffer子陣列的長度就是2*(2*3+1)
。也許就有人好奇了,為什麼是乘以2*3+1
而不是直接乘以2*3
呢?之前我們提到過,拆成小圖後要對小圖進行普通的逐行掃描解析,這樣解析的話每一行的第一個位元組實際存放的不是影象資料,而是過濾型別,因此每一行所佔用的位元組需要在2*3
的基礎上加1。
畫素歸位
其他的小圖拆分的方法是一樣,在最後一次掃描完畢後,我們就會拿到7張小圖。然後我們按照上面的規則對這些小圖的畫素進行歸位,也就是填回去的意思。下面簡單演示下歸位的流程:
1 2 3 4 5 6 7 8 9 10 |
(1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) (1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) 1 1 ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) 1 1 ==> ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) (1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) (1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) |
待到7張小圖的畫素全部都歸位後,最後我們就能拿到一張完整的png圖片了。
程式碼
整個流程的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
let width; // 完整影象寬度,解析IHDR資料塊可得 let height; // 完整影象高度,解析IHDR資料塊可得 let colors; // 通道數,解析IHDR資料塊可得 let bitDepth; // 影象深度,解析IHDR資料塊可得 let data; // 完整影象資料 let bytesPerPixel = Math.max(1, colors * bitDepth / 8); // 每畫素位元組數 let pixelsBuffer = Buffer.alloc(bytesPerPixel * width * height, 0xFF); // 用來存放最後解析出來的影象資料 // 7次掃描的規則 let startX = [0, 0, 4, 0, 2, 0, 1]; let incX = [8, 8, 8, 4, 4, 2, 2]; let startY = [0, 4, 0, 2, 0, 1, 0]; let incY = [8, 8, 4, 4, 2, 2, 1]; let offset = 0; // 記錄小圖開始位置 // 7次掃描 for(let i=0; i<7; i++) { // 子影象資訊 let subWidth = Math.ceil((width - startY[i]) / incY[i], 10); // 小圖寬度 let subHeight = Math.ceil((height - startX[i]) / incX[i], 10); // 小圖高度 let subBytesPerRow = bytesPerPixel * subWidth; // 小圖每行位元組數 let offsetEnd = offset + (subBytesPerRow + 1) * subHeight; // 小圖結束位置 let subData = data.slice(offset, offsetEnd); // 小影象素資料 // 對小圖進行普通的逐行掃描 let subPixelsBuffer = this.interlaceNone(subData, subWidth, subHeight, bytesPerPixel, subBytesPerRow); let subOffset = 0; // 畫素歸位 for(let x=startX[i]; x<height; x+=incX[i]) { for(let y=startY[i]; y<width; y+=incY[i]) { // 逐個畫素拷貝回原本所在的位置 for(let z=0; z<bytesPerPixel; z++) { pixelsBuffer[(x * width + y) * bytesPerPixel + z] = subPixelsBuffer[subOffset++] & 0xFF; } } } offset = offsetEnd; // 置為下一張小圖的開始位置 } return pixelsBuffer; |
尾聲
整個Adam7隔行掃描的流程大概就是這樣: