The Go Blog

GIFデコーダ: Goインターフェースの練習

ロブ・パイク
2011年5月25日

はじめに

2011年5月10日にサンフランシスコで開催されたGoogle I/Oカンファレンスで、Go言語がGoogle App Engineで利用可能になったことを発表しました。Goは、App Engineで利用可能になった最初の言語で、機械語に直接コンパイルされるため、画像操作などのCPU負荷の高いタスクに適しています。

その流れで、このような写真を簡単に改善するMoustachioというプログラムをデモンストレーションしました。

ひげを追加して結果を共有することで

アンチエイリアス処理されたひげのレンダリングを含むすべてのグラフィック処理は、App Engineで実行されているGoプログラムによって行われます。(ソースはappengine-goプロジェクトで入手できます。)

Web上のほとんどの画像(少なくともひげを生やされる可能性のある画像)はJPEGですが、他にも無数のフォーマットが出回っており、Moustachioがいくつかのフォーマットでアップロードされた画像を受け入れるのは妥当だと考えられました。Goの画像ライブラリにはJPEGおよびPNGデコーダがすでに存在しましたが、由緒あるGIF形式は含まれていなかったため、発表までにGIFデコーダを作成することにしました。このデコーダには、Goのインターフェースがどのようにいくつかの問題を解決しやすくするかを示すいくつかの部分が含まれています。このブログ投稿の残りの部分では、いくつかの例を説明します。

GIF形式

まず、GIF形式を簡単に見てみましょう。GIF画像ファイルはパレット化されており、つまり、各ピクセル値はファイルに含まれる固定カラーマップへのインデックスです。GIF形式は、ディスプレイに通常8ビット/ピクセルを超えるものがない時代にさかのぼり、カラーマップは限られた値のセットを画面を照らすのに必要なRGB (赤、緑、青) の3つ組に変換するために使用されていました。(これは、エンコーディングが異なるカラー信号を別々に表すためカラーマップを持たないJPEGとは対照的です。)

GIF画像は、1から8ビット/ピクセルまでを含めることができますが、8ビット/ピクセルが最も一般的です。

やや単純化すると、GIFファイルにはピクセル深度と画像寸法を定義するヘッダー、カラーマップ (8ビット画像の場合は256のRGBトリプル)、そしてピクセルデータが含まれます。ピクセルデータは、一次元のビットストリームとして保存され、LZWアルゴリズムで圧縮されます。これは、コンピュータ生成グラフィックスには非常に効果的ですが、写真画像にはそれほど適していません。圧縮データは、1バイトのカウント (0-255) に続いてそのバイト数を持つ、長さ区切りブロックに分割されます。

ピクセルデータのデブロック

GoでGIFピクセルデータをデコードするには、compress/lzwパッケージのLZWデコンプレッサーを使用できます。これには、ドキュメントに記載されているように、「rから読み取られたデータを解凍することで読み取りを満足させる」オブジェクトを返すNewReader関数があります。

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

ここで、orderはビットパッキング順序を定義し、litWidthはビット単位のワードサイズであり、GIFファイルの場合、ピクセル深度、通常は8に対応します。

しかし、デコンプレッサーはバイトストリームを必要としますが、GIFデータはアンパックする必要があるブロックストリームであるため、NewReaderに入力ファイルを最初の引数として渡すだけではうまくいきません。この問題を解決するには、入力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`から呼び出し元のバッファにデータをコピーします。`ReadFull`ではなく`Read`を実装しているため、要求されたバイト数よりも少ないバイト数を返すことができます。これにより、`b.slice`から呼び出し元のバッファ(`p`)にデータをコピーするだけで済み、`copy`の戻り値は転送されたバイト数になります。次に、`b.slice`を再スライスして最初の`n`バイトを削除し、次の呼び出しに備えます。

Goプログラミングにおいて、スライス (b.slice) を配列 (b.tmp) に結合するのは良いテクニックです。この場合、blockReader型のReadメソッドは一切アロケーションを行いません。また、カウントを保持する必要がなく (スライスの長さに暗黙的に含まれる)、組み込みのcopy関数は必要以上にコピーしないことを保証します。(スライスについては、Goブログのこの記事を参照してください。)

blockReader型があれば、入力リーダー (たとえばファイル) を次のようにラップするだけで、画像データストリームのブロック解除が可能です。

deblockingReader := &blockReader{r: imageFile}

このラッピングにより、ブロック区切りのGIF画像ストリームは、blockReaderReadメソッドへの呼び出しによってアクセス可能な単純なバイトストリームに変換されます。

部品を接続する

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.Readerlzw.LSBはLZWデコンプレッサーのバイト順序を定義し、litWidthはピクセル深度です。

デコンプレッサーが与えられ、2行目はio.ReadFullを呼び出してデータを解凍し、画像m.Pixに格納します。ReadFullが戻ると、画像データは解凍されて画像mに格納され、表示準備が整います。

このコードは初回から動作しました。本当に。

NewReaderへの呼び出しの中にblockReaderを作成したように、NewReader呼び出しをReadFullの引数リストに配置することで一時変数lzwrを避けることができますが、それは一行のコードに詰め込みすぎかもしれません。

まとめ

Goのインターフェースは、このように部品を組み合わせてデータを再構築することで、ソフトウェアを簡単に構築できます。この例では、デブロッカーとデコンプレッサーをio.Readerインターフェースを使用して連鎖させることでGIFデコードを実装しました。これはタイプセーフなUnixパイプラインに似ています。また、デブロッカーを(暗黙的に)Readerインターフェースの実装として記述したため、処理パイプラインに適合させるための追加の宣言や定型文は不要でした。ほとんどの言語では、このようにコンパクトかつクリーンで安全にこのデコーダーを実装するのは難しいですが、インターフェースメカニズムといくつかの慣習により、Goではほとんど自然なことになります。

それは、もう一枚の絵、今回はGIFに値します。

GIF形式はhttp://www.w3.org/Graphics/GIF/spec-gif89a.txtで定義されています。

次の記事: 外部Goライブラリに注目
前の記事: Google I/O 2011でのGo: 動画
ブログインデックス