[インデックス 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デコーダがパレットインデックスを処理する方法の変更にあります。
-
PLTE
チャンク解析時のパレット初期化の変更:- 以前は、
PLTE
チャンクで定義されたエントリ数 (np
) に応じてd.palette
をmake([]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
を超えるインデックスが参照された場合でも、配列の範囲外アクセスエラーではなく、初期化された不透明な黒が返されるようになります。
- 以前は、
-
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仕様の制約を考慮しつつ、柔軟性を持たせるための変更です。
-
ピクセルデータデコード時のパレットインデックス検証の変更:
- 以前は、ピクセルから読み取ったパレットインデックス
idx
がmaxPalette
(パレットの最大インデックス、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ビット)のパレットインデックスデコードループ内で、読み取った
idx
がmaxPalette
を超える場合に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ファイルに対しても、より堅牢かつ互換性のあるデコード挙動を提供するようになりました。
関連リンク
- GitHub Issue: image/png: degrade gracefully for palette index values that aren't defined by the PLTE chunk · Issue #4319 · golang/go
- Go CL (Change List): https://golang.org/cl/6822065
参考にした情報源リンク
- PNG (Portable Network Graphics) Specification, Version 1.2: https://www.w3.org/TR/PNG/ (特に、PLTEチャンクとtRNSチャンクに関するセクション)
- libpng 公式サイト: http://www.libpng.org/
- ImageMagick 公式サイト: https://imagemagick.org/
- Go言語
image
パッケージドキュメント: https://pkg.go.dev/image - Go言語
image/png
パッケージドキュメント: https://pkg.go.dev/image/png - Go言語
color
パッケージドキュメント: https://pkg.go.dev/image/color - PNG: The Definitive Guide: https://www.oreilly.com/library/view/png-the-definitive/9781565925437/ (PNGフォーマットの詳細な解説)
- PNG (Portable Network Graphics) - Wikipedia: https://ja.wikipedia.org/wiki/Portable_Network_Graphics
- libpng source code (for png_set_PLTE context): 関連するlibpngのソースコードは、libpngの公式リポジトリやリリースアーカイブから参照できます。コミットメッセージに記載されているコメントは、libpngの内部実装に関するものです。