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

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

このコミットは、Go言語の標準ライブラリ image/gif パッケージにおけるGIF画像の透明度処理に関するバグ修正です。具体的には、ローカルカラーテーブルを持つGIF画像において、透明度が正しく適用されない問題を解決します。

コミット

commit ff6b9223616f673aaeddb791f9e0303591b128bc
Author: Nigel Tao <nigeltao@golang.org>
Date:   Wed Dec 18 15:10:40 2013 -0500

    image/gif: respect local color table transparency.
    
    Fixes #6441.
    
    R=r
    CC=andybons, golang-dev
    https://golang.org/cl/13829043

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

https://github.com/golang/go/commit/ff6b9223616f673aaeddb791f9e0303591b128bc

元コミット内容

image/gif: respect local color table transparency. Fixes #6441.

変更の背景

GIF画像フォーマットには、画像全体に適用される「グローバルカラーテーブル (Global Color Table: GCT)」と、各フレーム(画像ブロック)に個別に適用される「ローカルカラーテーブル (Local Color Table: LCT)」の2種類のカラーテーブルが存在します。また、GIFは透明度をサポートしており、カラーテーブル内の特定の色を透明として指定することができます。

このコミットが修正する問題は、Goの image/gif パッケージが、ローカルカラーテーブルを持つGIF画像において、そのローカルカラーテーブルで指定された透明度情報を正しく処理していなかったことにあります。以前の実装では、透明度インデックスがグローバルカラーテーブルにのみ適用されるか、あるいはローカルカラーテーブルが使用されている場合でも透明度情報が適切に引き継がれていなかった可能性があります。

その結果、ローカルカラーテーブルで透明色が指定されているGIF画像がGoの image/gif パッケージでデコードされると、透明であるべきピクセルが不透明な色で描画されてしまうというバグ(Issue #6441)が発生していました。このコミットは、この問題を解決し、GIFの仕様に則ってローカルカラーテーブルの透明度を尊重するように修正します。

前提知識の解説

GIF (Graphics Interchange Format)

GIFは、1987年にCompuServeによって導入されたビットマップ画像フォーマットです。最大256色(8ビットカラー)をサポートし、可逆圧縮方式(LZW圧縮)を使用します。アニメーションをサポートする点が特徴で、複数の画像を連続して表示することで動画のように見せることができます。

カラーテーブル (Color Table)

GIF画像は、直接ピクセルごとのRGB値を保持するのではなく、カラーテーブル(パレット)と呼ばれる色のリストを参照してピクセルを表現します。各ピクセルは、このカラーテーブル内の色のインデックスとして格納されます。

  • グローバルカラーテーブル (Global Color Table: GCT): GIFファイル全体に適用されるデフォルトのカラーテーブルです。ファイルヘッダの直後に定義されます。
  • ローカルカラーテーブル (Local Color Table: LCT): 各画像ブロック(フレーム)に個別に適用されるカラーテーブルです。LCTが定義されている場合、その画像ブロックではGCTではなくLCTが優先されます。これにより、各フレームが異なる色のセットを持つことが可能になります。

透明度 (Transparency)

GIF89a仕様では、透明度をサポートしています。これは、カラーテーブル内の特定の色を「透明色」として指定することで実現されます。透明色として指定されたピクセルのインデックスは、レンダリング時に背景が透けて見えるように処理されます。この透明度情報は、グラフィックコントロール拡張 (Graphic Control Extension: GCE) と呼ばれるブロックで指定されます。GCEには、透明度インデックスの他に、ディレイタイム(アニメーションのフレーム間隔)や処分方法(前のフレームをどう扱うか)などの情報も含まれます。

Go言語の image パッケージ

Go言語の標準ライブラリには、画像処理のための image パッケージ群が含まれています。image/gif はその一つで、GIF画像のエンコードとデコードを提供します。image.Paletted 型は、GIFのようにパレット(カラーテーブル)を使用する画像を表現するための構造体です。

技術的詳細

このコミットの技術的な核心は、GIFのデコード処理において、グラフィックコントロール拡張 (GCE) で指定された透明度インデックスを、そのフレームに実際に適用されるカラーテーブル(グローバルまたはローカル)に対して正しくマッピングすることです。

以前の実装では、decoder 構造体には transparentIndex byte フィールドがありましたが、これがどのカラーテーブルに属する透明度なのか、あるいはその透明度をいつ適用すべきかについて、曖昧さや不整合があったと考えられます。特に、ローカルカラーテーブルが使用される場合に、透明度インデックスがグローバルカラーテーブルに誤って適用されたり、全く適用されなかったりする可能性がありました。

この修正では、以下の点が改善されています。

  1. hasTransparentIndex フラグの導入: decoder 構造体に hasTransparentIndex bool フィールドが追加されました。これにより、現在のフレームに対して透明度インデックスが指定されているかどうかを明示的に追跡できるようになります。これは、単に transparentIndex の値が存在するかどうかだけでなく、GCEで透明度フラグがセットされているかどうかに基づいて判断されます。
  2. 透明度適用ロジックの遅延と正確な適用:
    • readGraphicControl メソッドでは、GCEから透明度情報が読み取られた際に、以前のようにすぐに setTransparency を呼び出してグローバルカラーマップに透明度を適用するのではなく、d.hasTransparentIndex = true を設定するだけに変更されました。
    • 実際の透明度の適用は、decode メソッド内で、そのフレームの image.Paletted 構造体の Palette が確定したに行われるようになりました。具体的には、ローカルカラーテーブルが使用された場合でも、そのローカルカラーテーブルが m.Palette に設定された後に、d.hasTransparentIndextrue であり、かつ d.transparentIndex がパレットの範囲内であれば、m.Palette[d.transparentIndex] = color.RGBA{} を実行して、該当する色を透明な黒(RGBA値が全て0)に設定します。
  3. GCEフィールドのリセット: GIF89a仕様のセクション23(グラフィックコントロール拡張)には、「この拡張のスコープは、それに続く最初のグラフィックレンダリングブロックである」と明記されています。このコミットでは、各画像ブロックのデコードが完了した後、d.delayTimed.hasTransparentIndex をリセットするように変更されました。これにより、あるフレームのGCE設定が、意図せず次のフレームに引き継がれてしまうことを防ぎ、仕様に厳密に準拠したデコードが可能になります。
  4. setTransparency 関数の削除: 上記の変更により、setTransparency ヘルパー関数は不要となり、削除されました。そのロジックは decode メソッド内にインライン化され、より適切なタイミングで実行されるようになりました。

これらの変更により、image/gif パッケージは、ローカルカラーテーブルを持つGIF画像であっても、その透明度情報を正確に解釈し、正しくレンダリングできるようになりました。

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

src/pkg/image/gif/reader.go ファイルが変更されています。

--- a/src/pkg/image/gif/reader.go
+++ b/src/pkg/image/gif/reader.go
@@ -79,7 +79,8 @@ type decoder struct {
 	imageFields byte
 
 	// From graphics control.
-	transparentIndex byte
+	transparentIndex    byte
+	hasTransparentIndex bool
 
 	// Computed.
 	pixelSize      uint
@@ -175,11 +176,12 @@ func (d *decoder) decode(r io.Reader, configOnly bool) error {
 			if err != nil {
 				return err
 			}
-			// TODO: do we set transparency in this map too? That would be
-			// d.setTransparency(m.Palette)
 			} else {
 				m.Palette = d.globalColorMap
 			}
+			if d.hasTransparentIndex && int(d.transparentIndex) < len(m.Palette) {
+				m.Palette[d.transparentIndex] = color.RGBA{}
+			}
 			litWidth, err := d.r.ReadByte()
 			if err != nil {
 				return err
@@ -228,7 +230,11 @@ func (d *decoder) decode(r io.Reader, configOnly bool) error {
 
 			d.image = append(d.image, m)
 			d.delay = append(d.delay, d.delayTime)
-			d.delayTime = 0 // TODO: is this correct, or should we hold on to the value?
+			// The GIF89a spec, Section 23 (Graphic Control Extension) says:
+			// "The scope of this extension is the first graphic rendering block
+			// to follow." We therefore reset the GCE fields to zero.
+			d.delayTime = 0
+			d.hasTransparentIndex = false
 
 		case sTrailer:
 			if len(d.image) == 0 {
@@ -339,17 +345,11 @@ func (d *decoder) readGraphicControl() error {
 	d.delayTime = int(d.tmp[2]) | int(d.tmp[3])<<8
 	if d.flags&gcTransparentColorSet != 0 {
 		d.transparentIndex = d.tmp[4]
-		d.setTransparency(d.globalColorMap)
+		d.hasTransparentIndex = true
 	}
 	return nil
 }
 
-func (d *decoder) setTransparency(colorMap color.Palette) {
-	if int(d.transparentIndex) < len(colorMap) {
-		colorMap[d.transparentIndex] = color.RGBA{}
-	}
-}
-
 func (d *decoder) newImageFromDescriptor() (*image.Paletted, error) {
 	if _, err := io.ReadFull(d.r, d.tmp[0:9]); err != nil {
 		return nil, fmt.Errorf("gif: can't read image descriptor: %s", err)

コアとなるコードの解説

  1. decoder 構造体へのフィールド追加:

    • transparentIndex byte の下に hasTransparentIndex bool が追加されました。これは、グラフィックコントロール拡張 (GCE) で透明度インデックスが実際に指定されているかどうかを示すフラグです。
  2. decode メソッド内の変更:

    • 以前の // TODO: do we set transparency in this map too? というコメントが削除されました。これは、このコミットでその疑問に対する解決策が実装されたことを示唆しています。
    • 新しいコードブロックが追加されました:
      if d.hasTransparentIndex && int(d.transparentIndex) < len(m.Palette) {
          m.Palette[d.transparentIndex] = color.RGBA{}
      }
      
      このコードは、現在のフレームのパレット (m.Palette) が確定した後(ローカルカラーテーブルが使用された場合でも)、hasTransparentIndextrue であり、かつ transparentIndex がパレットの有効な範囲内にある場合に、そのインデックスの色を color.RGBA{} (透明な黒) に設定します。これにより、ローカルカラーテーブルの透明度が正しく適用されます。
    • フレーム処理の最後に、GCE関連のフィールドがリセットされるようになりました:
      d.delayTime = 0
      d.hasTransparentIndex = false
      
      これは、GIF89a仕様に従い、GCEの設定が次の画像ブロックに誤って影響を与えないようにするためです。
  3. readGraphicControl メソッド内の変更:

    • 以前の d.setTransparency(d.globalColorMap) の呼び出しが削除されました。これは、GCEを読み込んだ直後にグローバルカラーマップに透明度を適用するのではなく、透明度インデックスが存在するという事実だけを記録するように変更されたことを意味します。
    • 代わりに d.hasTransparentIndex = true が設定されます。これにより、透明度の実際の適用は、そのフレームの正しいパレットが利用可能になった時点まで遅延されます。
  4. setTransparency 関数の削除:

    • setTransparency ヘルパー関数は、その機能が decode メソッド内にインライン化され、より正確なタイミングで実行されるようになったため、完全に削除されました。

これらの変更により、GIFデコーダは、ローカルカラーテーブルの有無にかかわらず、GIFの透明度仕様に厳密に準拠し、正確な画像レンダリングを実現します。

関連リンク

参考にした情報源リンク