Go 部落格
一個 GIF 解碼器:一個 Go 介面的練習
簡介
於 2011 年 5 月 10 日在舊金山舉辦的 Google I/O 會議中,我們宣布 Go 語言現已在 Google App Engine 上提供服務。Go 是第一種直接編譯為機器碼後,才能在 App Engine 上提供的語言,這讓它成為像是影像處理這類需要大量使用 CPU 任務的理想選擇。
在這種情況下,我們示範了一個名叫 Moustachio 的程式,它能讓使用者輕而易舉地改善像這樣的圖像

加入小鬍子並分享結果

所有的圖形處理,包含呈現漸層抗鋸齒小鬍子,全都透過在 App Engine 上執行的 Go 程式執行。(原始程式碼可在 appengine-go 專案 取得。)
儘管網路上大部分的影像-至少那些可能蓄鬍的-都是 JPEG,還有無數的其他的格式流傳著,而讓 Moustachio 接受幾個中上傳的影像,似乎是很合理的。 JPEG 和 PNG 解碼器已經存在於 Go 影像程式庫中,但是頗負盛名的 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
。換句話說,我們將解封程式碼放入新類型的 Read
方法中,我們稱之為 blockReader
。
這是 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
),複製的傳回值即為傳輸的位元組數。接著,我們重新切片 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 的介面讓軟體建構變得簡單。在此範例中,我們鏈接解阻隔器及解壓縮器來實作 GIF 解碼,並使用 io.Reader
介面,這類似於經過類型安全處理的 Unix 管線。此外,我們以(隱含)讀取器介面的實作方式撰寫解阻隔器,之後無需其他宣告或樣板碼,即可將其整合到處理管線中。在多數語言中,很難如此緊湊、乾淨且安全地實作這個解碼器,但介面機制加上數個慣例讓 Go 幾近自然。
這值得再附上另一張圖片,這次是 GIF

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