Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 16767] ファイルの概要

このコミットは、Go言語の標準ライブラリ image/gif パッケージにGIF画像の書き込み(エンコード)機能を追加するものです。これまではGIF画像の読み込み(デコード)のみがサポートされていましたが、この変更により、Goプログラム内でGIF画像を生成し、ファイルやストリームに書き出すことが可能になります。

コミット

commit 9ebc5be39c5bad22ca0f97849d1ad475bacdc950
Author: Andrew Bonventre <andybons@chromium.org>
Date:   Mon Jul 15 10:57:01 2013 +1000

    image/gif: add writer implementation
    
    R=r, nigeltao
    CC=golang-dev
    https://golang.org/cl/10896043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/9ebc5be39c5bad22ca0f97849d1ad475bacdc950

元コミット内容

image/gif: add writer implementation

このコミットは、Goの image/gif パッケージにGIF画像の書き込み(エンコード)機能を追加します。

変更の背景

Go言語の image パッケージは、様々な画像フォーマットの読み書きをサポートすることを目的としています。このコミット以前は、image/gif パッケージはGIF画像のデコード(読み込み)機能のみを提供していました。しかし、画像処理アプリケーションやツールにおいて、GIF画像を生成したり、既存の画像をGIF形式で保存したりするニーズは高く、エンコード機能の追加は自然な拡張でした。

特に、アニメーションGIFの生成は、Webアプリケーションやグラフィックツールにおいて重要な機能であり、このエンコーダの実装は、Goエコシステムにおける画像処理能力を大きく向上させるものでした。

前提知識の解説

GIF (Graphics Interchange Format)

GIFは、CompuServeによって開発されたビットマップ画像フォーマットです。特徴としては以下の点が挙げられます。

  • インデックスカラー: 最大256色(2^8色)のパレットを使用します。各ピクセルはパレット内の色のインデックスで表現されます。
  • 可逆圧縮: LZW (Lempel-Ziv-Welch) アルゴリズムを用いた可逆圧縮により、画質を損なわずにファイルサイズを削減します。
  • アニメーション: 複数の画像を連続して表示することでアニメーションを表現できます。各フレームには表示遅延時間や透過色などの設定が可能です。
  • 透過: 1色を透過色として指定できます。
  • インターレース: 画像を段階的に表示するインターレース方式をサポートします。

LZW (Lempel-Ziv-Welch) 圧縮

LZWは、データ圧縮アルゴリズムの一種で、GIF、TIFF、PDFなどのフォーマットで広く利用されています。基本的な原理は、入力データストリームから繰り返し出現するパターン(シーケンス)を辞書に登録し、そのパターンをより短いコードに置き換えることで圧縮を実現します。

GIFにおけるLZW圧縮は、ピクセルデータを効率的に圧縮するために使用されます。エンコーダは、ピクセルデータのシーケンスを読み込み、辞書に基づいてコードを生成し、出力ストリームに書き込みます。デコーダは、これらのコードを読み込み、辞書を使って元のピクセルデータを再構築します。

Go言語の image パッケージ

Go言語の標準ライブラリ image パッケージは、画像データの抽象的な表現(image.Image インターフェース)と、様々な画像フォーマット(JPEG, PNG, GIFなど)のエンコード/デコード機能を提供します。

  • image.Image: 画像のピクセルデータと境界を表現するインターフェース。
  • image.Paletted: パレットカラー画像を表す構造体。GIF画像は通常この形式で扱われます。
  • image/color.Palette: 色の配列を表す型。インデックスカラー画像のパレットとして使用されます。
  • image/draw: 画像の描画操作(リサイズ、合成、色変換など)を行うためのパッケージ。特に、減色処理(クオンタイゼーション)やディザリング(誤差拡散)に利用されます。

技術的詳細

このコミットで追加されたGIFエンコーダは、GIF89a仕様に準拠しています。主な実装は src/pkg/image/gif/writer.go にあり、以下の主要なコンポーネントと機能を含んでいます。

  1. encoder 構造体: GIFエンコード処理のメインロジックをカプセル化します。io.Writer インターフェースを介して出力ストリームにデータを書き込みます。
  2. writeHeader(): GIFファイルのヘッダ(GIF89a シグネチャ、論理スクリーン記述子、グローバルカラーテーブル)を書き込みます。グローバルカラーテーブルは、最初の画像のパレットに基づいて生成されます。アニメーションGIFの場合、NETSCAPE2.0 アプリケーション拡張ブロックも書き込まれ、ループカウントが設定されます。
  3. writeColorTable(p color.Palette, size int): 指定されたパレットをGIFのカラーテーブル形式で書き込みます。パレットサイズに合わせてパディングが行われます。
  4. writeImageBlock(pm *image.Paletted, delay int): 個々の画像フレーム(image.Paletted)をGIFの画像記述子ブロックとして書き込みます。
    • グラフィックコントロール拡張: 遅延時間(delay)や透過色(transparentIndex)が設定されている場合、グラフィックコントロール拡張ブロックが書き込まれます。
    • 画像記述子: 画像の位置とサイズが書き込まれます。
    • ローカルカラーテーブル: 各フレームが独自のパレットを持つ場合、ローカルカラーテーブルが書き込まれます。
    • LZW圧縮: compress/lzw パッケージを使用してピクセルデータがLZW圧縮され、ブロック形式で出力されます。blockWriter というヘルパー型が、LZWエンコーダからの出力をGIFのデータブロック形式(ブロックサイズとデータ)に変換する役割を担います。
  5. Encode(w io.Writer, m image.Image, o *Options) error: 単一の image.Image をGIF形式でエンコードする関数です。
    • 入力画像が image.Paletted でない場合、またはパレットサイズが Options.NumColors を超える場合、image/draw パッケージの QuantizerDrawer を使用して、指定された色数に減色(クオンタイゼーション)およびディザリング処理が行われます。デフォルトでは color.Plan9Palettedraw.FloydSteinberg が使用されます。
  6. EncodeAll(w io.Writer, g *GIF) error: 複数の image.Paletted 画像を含む GIF 構造体(アニメーションGIF用)をエンコードする関数です。各フレームの遅延時間とループカウントが考慮されます。

依存関係の変更

src/pkg/go/build/deps_test.go の変更は、image/gif パッケージが image/draw パッケージに依存するようになったことを示しています。これは、GIFエンコーダが画像の減色やディザリングのために image/draw の機能を利用するためです。

reader.go の変更

src/pkg/image/gif/reader.go のコメントが更新され、パッケージがGIFのデコーダだけでなくエンコーダも実装していることが明記されました。

テストデータの追加

src/pkg/image/testdata/video-005.gray.gif という新しいバイナリテストデータが追加されています。これは、エンコーダのテストに使用される可能性のあるGIF画像です。

コアとなるコードの変更箇所

src/pkg/go/build/deps_test.go

--- a/src/pkg/go/build/deps_test.go
+++ b/src/pkg/go/build/deps_test.go
@@ -202,7 +202,7 @@ var pkgDeps = map[string][]string{
 	"go/build":            {"L4", "OS", "GOPARSER"},
 	"html":                {"L4"},
 	"image/draw":          {"L4"},
-"	image/gif":           {"L4", "compress/lzw"},
+"	image/gif":           {"L4", "compress/lzw", "image/draw"},
 	"image/jpeg":          {"L4"},
 	"image/png":           {"L4", "compress/zlib"},
 	"index/suffixarray":   {"L4", "regexp"},

image/gif パッケージの依存関係に image/draw が追加されました。

src/pkg/image/gif/reader.go

--- a/src/pkg/image/gif/reader.go
+++ b/src/pkg/image/gif/reader.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// Package gif implements a GIF image decoder.
+// Package gif implements a GIF image decoder and encoder.
 //
 // The GIF specification is at http://www.w3.org/Graphics/GIF/spec-gif89a.txt.
 package gif

パッケージのコメントが更新され、エンコーダ機能が追加されたことが示されています。

src/pkg/image/gif/writer.go (新規ファイル)

このファイルはGIFエンコーダの主要な実装を含みます。

  • log2 ヘルパー関数: パレットサイズから必要なビット数を計算します。
  • writeUint16 ヘルパー関数: 16ビット整数をリトルエンディアンで書き込みます。
  • encoder 構造体: エンコード状態を保持します。
  • blockWriter 構造体: LZWエンコーダからの出力をGIFのデータブロック形式に変換します。
  • writeHeader(): GIFヘッダ、論理スクリーン記述子、グローバルカラーテーブル、およびアニメーション用のNETSCAPE2.0拡張を書き込みます。
  • writeColorTable(): カラーパレットをGIF形式で書き込みます。
  • writeImageBlock(): 個々の画像フレームをGIFの画像ブロックとして書き込みます。グラフィックコントロール拡張(遅延、透過)とLZW圧縮されたピクセルデータを含みます。
  • Options 構造体: エンコードオプション(色数、クオンタイザ、ドロワー)を定義します。
  • EncodeAll(): 複数の画像(アニメーションGIF)をエンコードします。
  • Encode(): 単一の画像をエンコードします。必要に応じて減色処理を行います。

src/pkg/image/gif/writer_test.go (新規ファイル)

このファイルはGIFエンコーダのテストを含みます。

  • readImgreadGIF ヘルパー関数: テスト用の画像を読み込みます。
  • deltaaverageDelta ヘルパー関数: 画像間の色の差を計算し、エンコード後の画質を評価します。
  • TestWriter: 単一画像のエンコードとデコードを行い、元の画像との差を許容範囲内で検証します。
  • TestEncodeAll: 複数の画像(アニメーション)のエンコードとデコードを行い、ループカウントや遅延時間などのメタデータが正しく保持されているかを検証します。
  • BenchmarkEncodeBenchmarkQuantizedEncode: エンコード性能を測定するためのベンチマークテストです。

src/pkg/image/testdata/video-005.gray.gif (新規バイナリファイル)

エンコーダのテストに使用される新しいGIFテストデータです。

コアとなるコードの解説

writer.goencoderblockWriter

encoder 構造体は、GIFエンコード処理の中心です。io.Writer をラップし、GIFの各セクション(ヘッダ、画像ブロックなど)を順次書き込むメソッドを提供します。

特に注目すべきは blockWriter です。GIFの画像データは、LZW圧縮されたバイト列が255バイト以下のブロックに分割され、各ブロックの先頭にそのブロックのサイズを示す1バイトが付加される形式で格納されます。compress/lzw パッケージの lzw.NewWriter は、このブロック形式を意識せずに連続したバイト列を期待します。そこで blockWriterio.Writer インターフェースを実装し、lzw.NewWriter からの出力を受け取り、それをGIFのブロック形式に変換して実際の出力ストリーム(e.w)に書き込む役割を担っています。これにより、LZW圧縮ロジックとGIFのブロック構造の間の結合が疎になり、コードの再利用性と保守性が向上しています。

// blockWriter writes the block structure of GIF image data, which
// comprises (n, (n bytes)) blocks, with 1 <= n <= 255. It is the
// writer given to the LZW encoder, which is thus immune to the
// blocking.
type blockWriter struct {
	e *encoder
}

func (b blockWriter) Write(data []byte) (int, error) {
	// ... (エラーハンドリング) ...
	total := 0
	for total < len(data) {
		n := copy(b.e.buf[1:256], data[total:]) // 最大255バイトをバッファにコピー
		total += n
		b.e.buf[0] = uint8(n) // ブロックサイズを先頭に書き込む
		_, b.e.err = b.e.w.Write(b.e.buf[:n+1]) // ブロックサイズとデータを書き出す
		// ... (エラーハンドリング) ...
	}
	return total, b.e.err
}

Encode 関数の減色処理

Encode 関数は、入力された image.Imageimage.Paletted でない場合、またはパレットの色数が多すぎる場合に、自動的に減色処理を行います。

func Encode(w io.Writer, m image.Image, o *Options) error {
	// ... (境界チェック、オプション設定) ...

	pm, ok := m.(*image.Paletted)
	if !ok || len(pm.Palette) > opts.NumColors {
		// 入力画像がPalettedでない、またはパレットの色数が多すぎる場合
		// 新しいPaletted画像を生成し、減色・ディザリングを行う
		pm = image.NewPaletted(b, color.Plan9Palette[:opts.NumColors]) // デフォルトパレット
		if opts.Quantizer != nil {
			pm.Palette = opts.Quantizer.Quantize(make(color.Palette, 0, opts.NumColors), m)
		}
		opts.Drawer.Draw(pm, b, m, image.ZP) // ディザリング描画
	}

	return EncodeAll(w, &GIF{
		Image: []*image.Paletted{pm},
		Delay: []int{0},
	})
}

この部分は、GIFがインデックスカラー形式であるという制約に対応するための重要なロジックです。image/draw パッケージの QuantizerDrawer インターフェースを利用することで、様々な減色アルゴリズムやディザリング手法を柔軟に適用できるよう設計されています。これにより、元の画像の色数が256色を超える場合でも、品質を保ちつつGIF形式に変換することが可能になります。

関連リンク

参考にした情報源リンク