Go 部落格

GIF 解碼器:Go 介面練習

Rob Pike
2011 年 5 月 25 日

引言

在 2011 年 5 月 10 日於舊金山舉行的 Google I/O 大會上,我們宣佈 Go 語言現已在 Google App Engine 上可用。Go 是第一門在 App Engine 上提供的、可直接編譯為機器碼的語言,這使其成為 CPU 密集型任務(如影像處理)的理想選擇。

有鑑於此,我們演示了一個名為 Moustachio 的程式,它可以輕鬆地改善類似下圖這樣的圖片

透過新增鬍子並分享結果

包括抗鋸齒鬍子渲染在內的所有圖形處理,都由執行在 App Engine 上的 Go 程式完成。(原始碼可在 appengine-go 專案中找到。)

儘管網路上的大多數影像(至少是可能需要新增鬍子的影像)是 JPEG 格式,但還有無數其他格式在流傳,Moustachio 接受幾種上傳格式似乎是合理的。Go 影像庫中已存在 JPEG 和 PNG 解碼器,但標誌性的 GIF 格式卻未被代表,因此我們決定及時編寫一個 GIF 解碼器以供釋出。該解碼器包含一些展示 Go 介面如何簡化某些問題解決過程的片段。本文的其餘部分將介紹其中幾個例項。

GIF 格式

首先,快速瀏覽一下 GIF 格式。GIF 影像檔案是**調色盤化**的,也就是說,每個畫素值都是檔案中包含的固定顏色對映的索引。GIF 格式的出現是在顯示器通常每個畫素不超過 8 位數的時代,並且使用顏色對映將有限的數值集轉換為照亮螢幕所需的 RGB(紅、綠、藍)三元組。(例如,這與 JPEG 不同,JPEG 沒有顏色對映,因為編碼單獨表示不同的顏色訊號。)

GIF 影像可以包含 1 到 8 位畫素,包含邊界,但 8 位畫素是最常見的。

為簡化起見,GIF 檔案包含一個定義畫素深度和影像尺寸的頭部、一個顏色對映(8 點陣圖像為 256 個 RGB 三元組),然後是畫素資料。畫素資料儲存為一維位元流,使用 LZW 演算法進行壓縮,該演算法對於計算機生成的圖形非常有效,但對於攝影影像效果不佳。壓縮後的資料然後被分解為長度分隔的塊,每個塊有一個位元組計數(0-255),後跟該位元組數的位元組。

解塊畫素資料

要在 Go 中解碼 GIF 畫素資料,我們可以使用 `compress/lzw` 包中的 LZW 解壓縮器。它有一個 `NewReader` 函式,該函式返回一個物件,該物件根據文件的說法,“透過解壓縮從 r 讀取的資料來滿足讀取請求”。

func NewReader(r io.Reader, order Order, litWidth int) io.ReadCloser

此處 `order` 定義了位打包順序,`litWidth` 是以位為單位的字大小,對於 GIF 檔案,它對應於畫素深度,通常為 8。

但我們不能直接將輸入檔案作為第一個引數傳遞給 `NewReader`,因為解壓縮器需要位元組流,而 GIF 資料是塊流,必須進行解包。為了解決這個問題,我們可以用一些解塊程式碼包裝輸入 `io.Reader`,並使該程式碼再次實現 `Reader`。換句話說,我們將解塊程式碼放入一個新型別(我們稱之為 `blockReader`)的 `Read` 方法中。

這是 `blockReader` 的資料結構。

type blockReader struct {
   r     reader    // Input source; implements io.Reader and io.ByteReader.
   slice []byte    // Buffer of unread data.
   tmp   [256]byte // Storage for slice.
}

讀取器 `r` 將是影像資料的源,可能是檔案或 HTTP 連線。`slice` 和 `tmp` 欄位將用於管理解塊。這是 `Read` 方法的完整程式碼。它是 Go 中切片和陣列使用的一個很好的例子。

1  func (b *blockReader) Read(p []byte) (int, os.Error) {
2      if len(p) == 0 {
3          return 0, nil
4      }
5      if len(b.slice) == 0 {
6          blockLen, err := b.r.ReadByte()
7          if err != nil {
8              return 0, err
9          }
10          if blockLen == 0 {
11              return 0, os.EOF
12          }
13          b.slice = b.tmp[0:blockLen]
14          if _, err = io.ReadFull(b.r, b.slice); err != nil {
15              return 0, err
16          }
17      }
18      n := copy(p, b.slice)
19      b.slice = b.slice[n:]
20      return n, nil
21  }

第 2-4 行只是一個健全性檢查:如果沒有地方存放資料,則返回零。這永遠不應該發生,但最好保持安全。

第 5 行透過檢查 `b.slice` 的長度來詢問上一次呼叫是否還有剩餘資料。如果沒有,切片長度將為零,我們需要從 `r` 讀取下一個塊。

GIF 塊以位元組計數開頭,在第 6 行讀取。如果計數為零,GIF 定義這是一個終止塊,因此我們在第 11 行返回 `EOF`。

現在我們知道應該讀取 `blockLen` 位元組,因此我們將 `b.slice` 指向 `b.tmp` 的前 `blockLen` 位元組,然後使用輔助函式 `io.ReadFull` 讀取那麼多位元組。如果無法讀取那麼多位元組,該函式將返回錯誤,這應該永遠不會發生。否則,我們將準備好 `blockLen` 位元組供讀取。

第 18-19 行將資料從 `b.slice` 複製到呼叫者的緩衝區。我們正在實現 `Read`,而不是 `ReadFull`,因此我們允許返回少於請求的位元組數。這很容易:我們只需將資料從 `b.slice` 複製到呼叫者的緩衝區(`p`),`copy` 函式的返回值就是傳輸的位元組數。然後,我們重新切片 `b.slice` 以刪除前 `n` 個位元組,為下一次呼叫做好準備。

在 Go 程式設計中,將切片(`b.slice`)與陣列(`b.tmp`)耦合是一個很好的技術。在這種情況下,這意味著 `blockReader` 型別的 `Read` 方法永遠不會進行任何分配。這也意味著我們不需要保留一個計數器(它隱含在切片長度中),並且內建的 `copy` 函式確保我們永遠不會複製超過我們應該複製的量。(有關切片的更多資訊,請參閱Go 部落格的這篇帖子。)

給定 `blockReader` 型別,我們只需像這樣包裝輸入讀取器(例如檔案)即可解塊影像資料流

deblockingReader := &blockReader{r: imageFile}

此包裝將塊分隔的 GIF 影像流轉換為一個簡單的位元組流,該流可透過對 `blockReader` 的 `Read` 方法進行呼叫來訪問。

連線各個部分

有了 `blockReader` 的實現以及庫中可用的 LZW 壓縮器,我們就擁有了解碼影像資料流所需的所有部件。我們用這段來自程式碼的雷鳴般的宣告將它們縫合在一起:

lzwr := lzw.NewReader(&blockReader{r: d.r}, lzw.LSB, int(litWidth))
if _, err = io.ReadFull(lzwr, m.Pix); err != nil {
   break
}

就這樣。

第一行建立一個 `blockReader` 並將其傳遞給 `lzw.NewReader` 來建立一個解壓縮器。這裡 `d.r` 是包含影像資料的 `io.Reader`,`lzw.LSB` 定義了 LZW 解壓縮器中的位元組順序,`litWidth` 是畫素深度。

給定解壓縮器,第二行呼叫 `io.ReadFull` 來解壓縮資料並將其儲存在影像 `m.Pix` 中。當 `ReadFull` 返回時,影像資料已被解壓縮並存儲在影像 `m` 中,隨時可以顯示。

這段程式碼第一次就成功了。真的。

我們可以透過將 `NewReader` 呼叫放在 `ReadFull` 的引數列表中來避免臨時變數 `lzwr`,就像我們在 `NewReader` 呼叫中構建 `blockReader` 一樣,但這可能將過多的內容塞進了一行程式碼。

結論

Go 的介面使得透過像這樣組合零件來構建軟體變得容易,以重構資料。在此示例中,我們透過使用 `io.Reader` 介面將解塊器和解壓縮器連結在一起實現了 GIF 解碼,這類似於型別安全的 Unix 管道。此外,我們將解塊器作為 `Reader` 介面的一個(隱式)實現來編寫,然後它不需要額外的宣告或樣板程式碼即可將其納入處理管道。在大多數語言中,要如此簡潔、乾淨且安全地實現此解碼器都很困難,但在 Go 中,介面機制加上一些約定使其變得近乎自然。

這值得再來一張圖,這次是一張 GIF 圖

GIF 格式在 http://www.w3.org/Graphics/GIF/spec-gif89a.txt 上定義。

下一篇文章:聚焦 Go 外部庫
上一篇文章:Google I/O 2011 Go 內容:影片
部落格索引