[インデックス 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
フィールドがありましたが、これがどのカラーテーブルに属する透明度なのか、あるいはその透明度をいつ適用すべきかについて、曖昧さや不整合があったと考えられます。特に、ローカルカラーテーブルが使用される場合に、透明度インデックスがグローバルカラーテーブルに誤って適用されたり、全く適用されなかったりする可能性がありました。
この修正では、以下の点が改善されています。
hasTransparentIndex
フラグの導入:decoder
構造体にhasTransparentIndex bool
フィールドが追加されました。これにより、現在のフレームに対して透明度インデックスが指定されているかどうかを明示的に追跡できるようになります。これは、単にtransparentIndex
の値が存在するかどうかだけでなく、GCEで透明度フラグがセットされているかどうかに基づいて判断されます。- 透明度適用ロジックの遅延と正確な適用:
readGraphicControl
メソッドでは、GCEから透明度情報が読み取られた際に、以前のようにすぐにsetTransparency
を呼び出してグローバルカラーマップに透明度を適用するのではなく、d.hasTransparentIndex = true
を設定するだけに変更されました。- 実際の透明度の適用は、
decode
メソッド内で、そのフレームのimage.Paletted
構造体のPalette
が確定した後に行われるようになりました。具体的には、ローカルカラーテーブルが使用された場合でも、そのローカルカラーテーブルがm.Palette
に設定された後に、d.hasTransparentIndex
がtrue
であり、かつd.transparentIndex
がパレットの範囲内であれば、m.Palette[d.transparentIndex] = color.RGBA{}
を実行して、該当する色を透明な黒(RGBA値が全て0)に設定します。
- GCEフィールドのリセット: GIF89a仕様のセクション23(グラフィックコントロール拡張)には、「この拡張のスコープは、それに続く最初のグラフィックレンダリングブロックである」と明記されています。このコミットでは、各画像ブロックのデコードが完了した後、
d.delayTime
とd.hasTransparentIndex
をリセットするように変更されました。これにより、あるフレームのGCE設定が、意図せず次のフレームに引き継がれてしまうことを防ぎ、仕様に厳密に準拠したデコードが可能になります。 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)
コアとなるコードの解説
-
decoder
構造体へのフィールド追加:transparentIndex byte
の下にhasTransparentIndex bool
が追加されました。これは、グラフィックコントロール拡張 (GCE) で透明度インデックスが実際に指定されているかどうかを示すフラグです。
-
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
) が確定した後(ローカルカラーテーブルが使用された場合でも)、hasTransparentIndex
がtrue
であり、かつtransparentIndex
がパレットの有効な範囲内にある場合に、そのインデックスの色をcolor.RGBA{}
(透明な黒) に設定します。これにより、ローカルカラーテーブルの透明度が正しく適用されます。 - フレーム処理の最後に、GCE関連のフィールドがリセットされるようになりました:
これは、GIF89a仕様に従い、GCEの設定が次の画像ブロックに誤って影響を与えないようにするためです。d.delayTime = 0 d.hasTransparentIndex = false
- 以前の
-
readGraphicControl
メソッド内の変更:- 以前の
d.setTransparency(d.globalColorMap)
の呼び出しが削除されました。これは、GCEを読み込んだ直後にグローバルカラーマップに透明度を適用するのではなく、透明度インデックスが存在するという事実だけを記録するように変更されたことを意味します。 - 代わりに
d.hasTransparentIndex = true
が設定されます。これにより、透明度の実際の適用は、そのフレームの正しいパレットが利用可能になった時点まで遅延されます。
- 以前の
-
setTransparency
関数の削除:setTransparency
ヘルパー関数は、その機能がdecode
メソッド内にインライン化され、より正確なタイミングで実行されるようになったため、完全に削除されました。
これらの変更により、GIFデコーダは、ローカルカラーテーブルの有無にかかわらず、GIFの透明度仕様に厳密に準拠し、正確な画像レンダリングを実現します。
関連リンク
- Go Issue #6441: https://github.com/golang/go/issues/6441
- Go CL 13829043: https://golang.org/cl/13829043
参考にした情報源リンク
- GIF89a Specification: https://www.w3.org/Graphics/GIF/spec-gif89a.txt (特に Section 23. Graphic Control Extension)
- Go
image/gif
パッケージドキュメント: https://pkg.go.dev/image/gif - Go
image
パッケージドキュメント: https://pkg.go.dev/image - Go
color
パッケージドキュメント: https://pkg.go.dev/image/color