png的故事:隔行掃描算法

轉載自AlloyTeam:http://www.alloyteam.com/2017/06/the-story-of-png-deinterlacing-algorithm/node

前言

前文已經講解過如何解析一張png圖片,然而對於掃描算法裏只是說明了逐行掃描的方式。其實png還支持一種隔行掃描技術,即Adam7隔行掃描算法。git

優劣

使用隔行掃描有什麼好處呢?若是你們有去仔細觀察的話,會發現網絡上有一些png圖在加載時能夠作到先顯示出比較模糊的圖片,而後逐漸愈來愈清晰,最後顯示出完整的圖片,相似以下效果:github

Adam7效果

這就是隔行掃描能帶來的效果。隔行掃描一共會進行1到7次掃描,每一次都是跳着部分像素點進行掃描的,先掃描到像素點能夠先渲染,每多一次掃描,圖片就會更清晰,到最後一次掃描時就會掃描完全部像素點,進而渲染出完整的圖片。算法

固然,也由於要進行跳像素掃描,整張圖片會存儲更多額外數據而致使圖片大小會稍微變大,具體增長了什麼額外數據下文會進行講解。數組

生成

要導出一張基於Adam7隔行掃描的png圖片是很是簡單,咱們能夠藉助Adobe的神器——PhotoShop(如下簡稱ps)。咱們把一張普通的圖片拖入到ps中,而後依次點選【文件】-【存儲爲Web所用的格式】,在彈出的框裏選擇存儲爲PNG-24,而後勾選交錯,最後點擊存儲便可。網絡

這裏的交錯就是隻將掃描算法設爲Adam7隔行掃描,若是不勾選交錯,則是普通逐行掃描的png圖片。this

原理

Adam7隔行掃描算法的原理並不難,本質上是將一張png圖片拆分紅多張png小圖,而後對這幾張png小圖進行普通的逐行掃描解析,最後將解析出來的像素數據按照必定的規則進行歸位便可。spa

  • 分析

在解壓縮完圖像數據後就要立刻進行拆圖。拆圖並不難,就是將本來存儲圖像數據的Buffer數組拆分紅多個Buffer數組而已。關鍵的問題是怎麼拆,這時咱們先祭上wiki上這張圖:3d

原理

上面這張圖就說明了每次掃描須要掃描到的像素,正常來講一張基於Adam7隔行掃描的png圖片是要經歷7次掃描的,不過有些比較小的圖片的實際掃描次數不到7次,這是由於有些掃描由於沒有實際像素點而落空的緣由,因此下面的講解仍是以標準的7次掃描來說解,本質上此算法的代碼寫出來後,是能兼容任何大小的png圖片的,由於算法自己和圖片大小無關。code

7次掃描,其實就回答了上面拆圖的問題:要拆成7張小圖。每張小圖就包含了每次掃描時要歸位的像素點。

以第一次掃描爲例:第一次掃描的規則是從左上角(咱們設定此座標爲(0,0))開始,那麼它掃描到的下一個點是同一行上一個點往右偏移8個像素,即(8,0)。以此類推,再下一個點就是(16,0)、(24,0)等。噹噹前行全部符合規則的點都掃描完時則跳到下一個掃描行的起點,即(8,0),也就是說第一次掃描的掃描行也是以8個像素爲偏移單位的。直到全部掃描行都已經掃描完成,咱們就能夠認爲此次掃描已經結束,能夠考慮進入第二次掃描。

咱們以一張10*10大小的png圖片來舉例,下面每一個數字表明一個像素點,數字的值表明這個點在第幾回掃描時被掃描到:

image

按照規則,在第一次掃描時咱們會掃描到4個像素點,咱們把這4個像素點單獨抽離出來合在一塊兒,就是咱們要拆的第一張小圖:

image

也就是說,咱們的第一張小圖就是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張小圖。而後咱們按照上面的規則對這些小圖的像素進行歸位,也就是填回去的意思。下面簡單演示下歸位的流程:

image

待到7張小圖的像素所有都歸位後,最後咱們就能拿到一張完整的png圖片了。

  • 代碼

整個流程的代碼以下:

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隔行掃描的流程大概就是這樣:

流程

前面提到基於此種掃描方式的png圖片每每會更大些,這是由於圖片存儲了一些額外數據致使的。這裏的額外數據就是指過濾類型。本來的png大圖拆成小圖後,掃描行的數目就會蹭蹭蹭往上漲,每一個掃描行的第一個字節都是用來存儲過濾類型的,因此行數增長的越多,額外數據就會越多。至於在用png圖片等時候要選用哪一種掃描方式等圖片,就要視具體場景而定了。若是對完整代碼有興趣的同窗能夠戳這裏


參考資料:



相關文章
相關標籤/搜索