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

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

このコミットは、Go言語の標準ライブラリである image/png パッケージ内の reader.go ファイルに対する変更です。image/png パッケージは、PNG (Portable Network Graphics) 画像フォーマットのエンコードおよびデコード機能を提供します。reader.go は、PNGファイルのデコード処理、特にチャンクの解析やピクセルデータの読み込みを担当する部分です。

コミット

image/png: degrade gracefully for palette index values that aren't
defined by the PLTE chunk. Such pixels decode to opaque black,
which matches what libpng does.

Fixes #4319.

On my reading, the PNG spec isn't clear whether palette index values
outside of those defined by the PLTE chunk is an error, and if not,
what to do.

Libpng 1.5.3 falls back to opaque black. png_set_PLTE says:

/* Changed in libpng-1.2.1 to allocate PNG_MAX_PALETTE_LENGTH instead
 * of num_palette entries, in case of an invalid PNG file that has
 * too-large sample values.
 */
png_ptr->palette = (png_colorp)png_calloc(png_ptr,
        PNG_MAX_PALETTE_LENGTH * png_sizeof(png_color));

ImageMagick 6.5.7 returns an error:

$ convert -version
Version: ImageMagick 6.5.7-8 2012-08-17 Q16 http://www.imagemagick.org
Copyright: Copyright (C) 1999-2009 ImageMagick Studio LLC
Features: OpenMP
$ convert packetloss.png x.bmp
convert: Invalid colormap index `packetloss.png' @ image.c/SyncImage/3849.

R=r
CC=golang-dev
https://golang.org/cl/6822065

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

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

元コミット内容

このコミットは、PNG画像のデコードにおいて、PLTE (パレット) チャンクで定義されていないパレットインデックス値がピクセルデータに出現した場合の挙動を改善するものです。以前はこのような不正なインデックス値に対してエラーを返していましたが、この変更により、libpng と同様に、該当するピクセルを不透明な黒としてデコードするように「優雅に劣化 (degrade gracefully)」するようになりました。

この変更は、Issue #4319 を修正するものです。PNGの仕様では、PLTE チャンクで定義された範囲外のパレットインデックス値がエラーであるか、またエラーでない場合にどう処理すべきかについて明確な規定がないことが背景にあります。既存のPNGライブラリの挙動は異なっており、libpng 1.5.3 は不透明な黒にフォールバックする一方、ImageMagick 6.5.7 はエラーを返していました。このコミットは、libpng の挙動に合わせることで、より堅牢なデコーダを目指しています。

変更の背景

PNG画像フォーマットでは、インデックスカラー画像の場合、PLTE (Palette) チャンクで最大256色のカラーパレットを定義します。ピクセルデータは、このパレット内の色を参照するためのインデックス値として格納されます。

このコミットの背景にある問題は、PNGファイル内のピクセルデータが、PLTE チャンクで実際に定義されている色の数を超えるパレットインデックス値を含んでいる場合に発生します。例えば、PLTE チャンクが100色しか定義していないにもかかわらず、ピクセルデータがインデックス150を参照しているようなケースです。

PNGの仕様 (PNG (Portable Network Graphics) Specification, Version 1.2) は、このような「範囲外のパレットインデックス」の扱いについて明確な指示を与えていません。この曖昧さのため、異なるPNGデコーダライブラリ間で挙動が分かれていました。

  • libpng: 広く使われているPNGライブラリである libpng のバージョン 1.5.3 は、このような不正なインデックス値に対してエラーを返すのではなく、不透明な黒 (RGBA: 0x00, 0x00, 0x00, 0xff) としてデコードしていました。これは、png_set_PLTE 関数が、無効なPNGファイルで大きすぎるサンプル値がある場合に備えて、num_palette エントリではなく PNG_MAX_PALETTE_LENGTH (通常256) のパレットを割り当てるように変更されたことからも伺えます。
  • ImageMagick: 画像処理ツールキットである ImageMagick のバージョン 6.5.7 は、このようなケースで「Invalid colormap index」というエラーを返していました。

Go言語の image/png パッケージは、以前は FormatError("palette index out of range") を返していましたが、これは一部のPNGファイルでデコードに失敗する原因となっていました。このコミットは、libpng の挙動に合わせることで、より多くのPNGファイルをエラーなくデコードできるようにし、堅牢性を高めることを目的としています。これにより、多少のデータ破損や非標準的なPNGファイルに対しても、完全にデコードを諦めるのではなく、視覚的に許容できる結果(不透明な黒)を提供できるようになります。

前提知識の解説

PNG (Portable Network Graphics) フォーマット

PNGは、可逆圧縮を特徴とするビットマップ画像フォーマットです。ウェブ上で広く利用されており、透明度(アルファチャンネル)をサポートします。PNGファイルは、複数の「チャンク」と呼ばれるデータブロックで構成されており、それぞれが画像に関する特定の情報(ヘッダ、パレット、ピクセルデータなど)を格納しています。

インデックスカラー画像と PLTE チャンク

PNGにはいくつかのカラータイプがありますが、このコミットに関連するのは「インデックスカラー」です。

  • インデックスカラー (Indexed-color): 画像の各ピクセルが直接色情報を持つのではなく、カラーパレット(色のリスト)へのインデックス(参照番号)を持つ形式です。これにより、ファイルサイズを削減できます。
  • PLTE (Palette) チャンク: インデックスカラー画像で使用される必須のチャンクで、画像が使用するカラーパレットを定義します。各エントリはRGB(赤、緑、青)の3バイトで構成され、最大256エントリ(0から255までのインデックス)を持つことができます。PLTE チャンクの長さは、パレット内のエントリ数に3を掛けた値になります。

tRNS (Transparency) チャンク

tRNS (Transparency) チャンクは、インデックスカラー画像において、パレット内の特定の色を透明にするためのアルファ値を提供します。PLTE チャンクで定義された各パレットエントリに対応するアルファ値(透明度)を格納します。tRNS チャンクの長さは、PLTE チャンクで定義されたエントリ数以下である必要があります。

Go言語の image パッケージと image/png

Go言語の標準ライブラリには、画像処理のための image パッケージとそのサブパッケージ群があります。

  • image パッケージ: image.Image インターフェースを定義し、様々な画像フォーマットに共通の抽象化を提供します。image.Paletted はインデックスカラー画像を表現するための具体的な型です。
  • image/png パッケージ: PNGフォーマットのエンコードとデコードを実装しています。デコード時には、PNGファイルから読み込んだデータを image.Image インターフェースを実装する型(例: image.Paletted, image.NRGBA など)に変換します。

color.Palette

Goの image パッケージでは、color.Palette 型がカラーパレットを表します。これは []color.Color のエイリアスであり、color.Color インターフェースを実装する色のスライスです。color.RGBA は、赤、緑、青、アルファの各成分を8ビットで表現する具体的な色型です。

技術的詳細

このコミットの技術的な核心は、PNGデコーダがパレットインデックスを処理する方法の変更にあります。

  1. PLTE チャンク解析時のパレット初期化の変更:

    • 以前は、PLTE チャンクで定義されたエントリ数 (np) に応じて d.palettemake([]color.Color, np) で直接初期化していました。
    • 変更後、d.palette はまず make(color.Palette, 256) で最大サイズ(256エントリ)のパレットとして初期化されます。
    • その後、PLTE チャンクで実際に定義された np 個の色がパレットに設定されます。
    • 残りの np から255までのエントリは、color.RGBA{0x00, 0x00, 0x00, 0xff} (不透明な黒) で初期化されます。
    • 最後に、d.palette = d.palette[:np] とすることで、パレットの論理的な長さは np に戻されます。しかし、これにより、内部的には256エントリ分のメモリが確保され、np を超えるインデックスが参照された場合でも、配列の範囲外アクセスエラーではなく、初期化された不透明な黒が返されるようになります。
  2. tRNS チャンク解析時のパレット長チェックの変更:

    • tRNS チャンクの長さ (n) が d.palette の現在の長さ (len(d.palette)) を超える場合、以前は FormatError("bad tRNS length") を返していました。
    • 変更後、len(d.palette) < n の場合に d.palette = d.palette[:n] とすることで、tRNS チャンクの長さに合わせてパレットの長さを調整します。これにより、tRNS チャンクが PLTE チャンクよりも多くのアルファ値を提供している場合でも、エラーを回避し、パレットの長さを適切に設定します。これは、PLTE チャンクの後に tRNS チャンクが来る場合、tRNS チャンクが PLTE チャンクの長さを超えてはならないというPNG仕様の制約を考慮しつつ、柔軟性を持たせるための変更です。
  3. ピクセルデータデコード時のパレットインデックス検証の変更:

    • 以前は、ピクセルから読み取ったパレットインデックス idxmaxPalette (パレットの最大インデックス、len(d.palette) - 1) を超える場合に FormatError("palette index out of range") を返していました。
    • 変更後、len(paletted.Palette) <= int(idx) という条件でチェックを行います。この条件が真の場合、つまり idx が現在のパレットの論理的な長さを超えている場合、paletted.Palette = paletted.Palette[:int(idx)+1] とすることで、パレットのスライスを idx+1 の長さまで拡張します。これにより、idx がパレットの論理的な長さを超えていても、先に256エントリで初期化されたパレットの範囲内であれば、不透明な黒が返されるようになります。この変更は、cbP1, cbP2, cbP4, cbP8 の各カラータイプ(ビット深度)のデコードロジックに適用されています。
    • maxPalette 変数は不要になったため削除されました。

これらの変更により、GoのPNGデコーダは、不正なパレットインデックスを持つPNGファイルに対しても、エラーで処理を中断するのではなく、libpng と同様に不透明な黒で該当ピクセルを描画することで、より堅牢なデコード挙動を実現しています。

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

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

--- a/src/pkg/image/png/reader.go
+++ b/src/pkg/image/png/reader.go
@@ -193,10 +193,18 @@ func (d *decoder) parsePLTE(length uint32) error {
 	d.crc.Write(d.tmp[:n])
 	switch d.cb {
 	case cbP1, cbP2, cbP4, cbP8:
-		d.palette = color.Palette(make([]color.Color, np))
+		d.palette = make(color.Palette, 256)
 		for i := 0; i < np; i++ {
 			d.palette[i] = color.RGBA{d.tmp[3*i+0], d.tmp[3*i+1], d.tmp[3*i+2], 0xff}
 		}
+		for i := np; i < 256; i++ {
+			// Initialize the rest of the palette to opaque black. The spec isn't
+			// clear whether palette index values outside of those defined by the PLTE
+			// chunk is an error: libpng 1.5.13 falls back to opaque black, the
+			// same as we do here, ImageMagick 6.5.7 returns an error.
+			d.palette[i] = color.RGBA{0x00, 0x00, 0x00, 0xff}
+		}
+		d.palette = d.palette[:np]
 	case cbTC8, cbTCA8, cbTC16, cbTCA16:
 		// As per the PNG spec, a PLTE chunk is optional (and for practical purposes,
 		// ignorable) for the ctTrueColor and ctTrueColorAlpha color types (section 4.1.2).
@@ -221,8 +229,8 @@ func (d *decoder) parsetRNS(length uint32) error {
 	case cbTC8, cbTC16:
 		return UnsupportedError("truecolor transparency")
 	case cbP1, cbP2, cbP4, cbP8:
-		if n > len(d.palette) {
-			return FormatError("bad tRNS length")
+		if len(d.palette) < n {
+			d.palette = d.palette[:n]
 		}
 		for i := 0; i < n; i++ {
 			rgba := d.palette[i].(color.RGBA)
@@ -279,7 +287,6 @@ func (d *decoder) decode() (image.Image, error) {
 	}
 	defer r.Close()
 	bitsPerPixel := 0
-	maxPalette := uint8(0)
 	pixOffset := 0
 	var (
 		gray     *image.Gray
@@ -308,7 +315,6 @@ func (d *decoder) decode() (image.Image, error) {
 		bitsPerPixel = d.depth
 		paletted = image.NewPaletted(image.Rect(0, 0, d.width, d.height), d.palette)
 		img = paletted
-		maxPalette = uint8(len(d.palette) - 1)
 	case cbTCA8:
 		bitsPerPixel = 32
 		nrgba = image.NewNRGBA(image.Rect(0, 0, d.width, d.height))
@@ -421,8 +427,8 @@ func (d *decoder) decode() (image.Image, error) {
 			b := cdat[x/8]
 			for x2 := 0; x2 < 8 && x+x2 < d.width; x2++ {
 				idx := b >> 7
-				if idx > maxPalette {
-					return nil, FormatError("palette index out of range")
+				if len(paletted.Palette) <= int(idx) {
+					paletted.Palette = paletted.Palette[:int(idx)+1]
 				}
 				paletted.SetColorIndex(x+x2, y, idx)
 				b <<= 1
@@ -433,8 +439,8 @@ func (d *decoder) decode() (image.Image, error) {
 			b := cdat[x/4]
 			for x2 := 0; x2 < 4 && x+x2 < d.width; x2++ {
 				idx := b >> 6
-				if idx > maxPalette {
-					return nil, FormatError("palette index out of range")
+				if len(paletted.Palette) <= int(idx) {
+					paletted.Palette = paletted.Palette[:int(idx)+1]
 				}
 				paletted.SetColorIndex(x+x2, y, idx)
 				b <<= 2
@@ -445,18 +451,18 @@ func (d *decoder) decode() (image.Image, error) {
 			b := cdat[x/2]
 			for x2 := 0; x2 < 2 && x+x2 < d.width; x2++ {
 				idx := b >> 4
-				if idx > maxPalette {
-					return nil, FormatError("palette index out of range")
+				if len(paletted.Palette) <= int(idx) {
+					paletted.Palette = paletted.Palette[:int(idx)+1]
 				}
 				paletted.SetColorIndex(x+x2, y, idx)
 				b <<= 4
 			}
 		case cbP8:
-			if maxPalette != 255 {
+			if len(paletted.Palette) != 255 {
 				for x := 0; x < d.width; x++ {
-					if cdat[x] > maxPalette {
-						return nil, FormatError("palette index out of range")
+					if len(paletted.Palette) <= int(cdat[x]) {
+						paletted.Palette = paletted.Palette[:int(cdat[x])+1]
 					}
 				}
 			}

コアとなるコードの解説

func (d *decoder) parsePLTE(length uint32) error 内の変更

 	case cbP1, cbP2, cbP4, cbP8:
-		d.palette = color.Palette(make([]color.Color, np))
+		d.palette = make(color.Palette, 256)
 		for i := 0; i < np; i++ {
 			d.palette[i] = color.RGBA{d.tmp[3*i+0], d.tmp[3*i+1], d.tmp[3*i+2], 0xff}
 		}
+		for i := np; i < 256; i++ {
+			// Initialize the rest of the palette to opaque black. The spec isn't
+			// clear whether palette index values outside of those defined by the PLTE
+			// chunk is an error: libpng 1.5.13 falls back to opaque black, the
+			// same as we do here, ImageMagick 6.5.7 returns an error.
+			d.palette[i] = color.RGBA{0x00, 0x00, 0x00, 0xff}
+		}
+		d.palette = d.palette[:np]
  • 変更前: PLTE チャンクで定義された np 個のエントリ数に基づいて、正確なサイズのパレットを作成していました。
  • 変更後: まず、最大256エントリ分のパレットを確保します。PLTE チャンクで実際に定義された np 個の色をパレットの先頭に設定した後、np から255までの残りのエントリを不透明な黒 (0x00, 0x00, 0x00, 0xff) で初期化します。最後に d.palette = d.palette[:np] とすることで、パレットの論理的な長さは np に戻りますが、基盤となる配列は256エントリ分の容量を保持しています。これにより、np を超えるインデックスが参照された場合でも、配列の範囲外アクセスではなく、初期化された不透明な黒が返されるようになります。コメントにもあるように、これは libpng の挙動に合わせたものです。

func (d *decoder) parsetRNS(length uint32) error 内の変更

 	case cbP1, cbP2, cbP4, cbP8:
-		if n > len(d.palette) {
-			return FormatError("bad tRNS length")
+		if len(d.palette) < n {
+			d.palette = d.palette[:n]
 		}
 		for i := 0; i < n; i++ {
 			rgba := d.palette[i].(color.RGBA)
  • 変更前: tRNS チャンクの長さ n が現在のパレットの長さ len(d.palette) を超える場合、エラーを返していました。
  • 変更後: len(d.palette) < n の場合、パレットのスライスを n の長さまで拡張します。これにより、tRNS チャンクが PLTE チャンクよりも多くのアルファ値を提供している場合でも、エラーを回避し、パレットの長さを適切に調整します。これは、PLTE チャンクの後に tRNS チャンクが来る場合、tRNS チャンクが PLTE チャンクの長さを超えてはならないというPNG仕様の制約を考慮しつつ、柔軟性を持たせるための変更です。

func (d *decoder) decode() (image.Image, error) 内の変更

 	bitsPerPixel := 0
-	maxPalette := uint8(0)
 	pixOffset := 0
  • maxPalette 変数が削除されました。これは、パレットインデックスの検証ロジックが変更されたため、不要になりました。
 	case cbP1, cbP2, cbP4, cbP8:
 		bitsPerPixel = d.depth
 		paletted = image.NewPaletted(image.Rect(0, 0, d.width, d.height), d.palette)
 		img = paletted
-		maxPalette = uint8(len(d.palette) - 1)
  • maxPalette の初期化行が削除されました。

ピクセルデータデコードループ内の変更 (cbP1, cbP2, cbP4, cbP8)

 	// cbP1 (1-bit palette)
 	// ...
 				idx := b >> 7
-				if idx > maxPalette {
-					return nil, FormatError("palette index out of range")
+				if len(paletted.Palette) <= int(idx) {
+					paletted.Palette = paletted.Palette[:int(idx)+1]
 				}
 				paletted.SetColorIndex(x+x2, y, idx)
 	// ...

 	// cbP2 (2-bit palette)
 	// ...
 				idx := b >> 6
-				if idx > maxPalette {
-					return nil, FormatError("palette index out of range")
+				if len(paletted.Palette) <= int(idx) {
+					paletted.Palette = paletted.Palette[:int(idx)+1]
 				}
 				paletted.SetColorIndex(x+x2, y, idx)
 	// ...

 	// cbP4 (4-bit palette)
 	// ...
 				idx := b >> 4
-				if idx > maxPalette {
-					return nil, FormatError("palette index out of range")
+				if len(paletted.Palette) <= int(idx) {
+					paletted.Palette = paletted.Palette[:int(idx)+1]
 				}
 				paletted.SetColorIndex(x+x2, y, idx)
 	// ...

 	// cbP8 (8-bit palette)
 	// ...
 		case cbP8:
-			if maxPalette != 255 {
+			if len(paletted.Palette) != 255 {
 				for x := 0; x < d.width; x++ {
-					if cdat[x] > maxPalette {
-						return nil, FormatError("palette index out of range")
+					if len(paletted.Palette) <= int(cdat[x]) {
+						paletted.Palette = paletted.Palette[:int(cdat[x])+1]
 					}
 				}
 			}
  • 変更前: 各ビット深度(1, 2, 4, 8ビット)のパレットインデックスデコードループ内で、読み取った idxmaxPalette を超える場合に FormatError("palette index out of range") を返していました。
  • 変更後: if len(paletted.Palette) <= int(idx) という条件でチェックを行います。この条件が真の場合、つまり idx が現在のパレットの論理的な長さを超えている場合、paletted.Palette = paletted.Palette[:int(idx)+1] とすることで、パレットのスライスを idx+1 の長さまで拡張します。これにより、idx がパレットの論理的な長さを超えていても、先に256エントリで初期化されたパレットの範囲内であれば、不透明な黒が返されるようになります。これは、PLTE チャンク解析時の変更と連携して、不正なインデックス値に対する「優雅な劣化」を実現します。

これらの変更により、GoのPNGデコーダは、PNG仕様の曖昧な部分や、一部の不正なPNGファイルに対しても、より堅牢かつ互換性のあるデコード挙動を提供するようになりました。

関連リンク

参考にした情報源リンク