[インデックス 15740] ファイルの概要
このコミットは、Go言語の image/jpeg
パッケージにおけるJPEGデコードの堅牢性を向上させるものです。具体的には、JPEGストリーム内に存在する余分な(extraneous)データを libjpeg
と同様に無視するように変更し、不正な形式のJPEGファイルでもデコードを可能にすることで、Issue #4705で報告された問題を修正しています。
コミット
image/jpeg: libjpeg と同様に、余分なデータを無視するように変更。
Issue #4705 を修正。
libjpeg は、余分なバイトが多数ある場合は標準エラー出力に警告を出力しますが、ハフマンデコード用の int32 ビットバッファに収まる程度の余分なバイトであれば、警告を出力しない場合があります。Issue #4705 の画像を作成したエンコーダが、厳密には無効な JPEG を生成していることに気づかなかったのは、このためだと推測されます。この問題に添付された画像には、2バイトの余分なデータが含まれています。
例えば、以下のプログラムを libjpeg の djpeg プログラムにパイプすると、N が 20 であっても「18 extraneous bytes」という警告が出力されます。
$ cat main.go
package main
import (
"bytes"
"image"
"image/color"
"image/jpeg"
"os"
)
const N = 20
func main() {
// 1x1 の赤い画像をエンコードする。
m := image.NewRGBA(image.Rect(0, 0, 1, 1))
m.Set(0, 0, color.RGBA{255, 0, 0, 255})
buf := new(bytes.Buffer)
jpeg.Encode(buf, m, nil)
b := buf.Bytes()
// 最後の "\xff\xd9" EOI マーカーを削除する。
b = b[:len(b)-2]
// SOS データに N 個のダミー 0x80 バイトを追加する。
for i := 0; i < N; i++ {
b = append(b, 0x80)
}
// "\xff\xd9" EOI マーカーを元に戻す。
b = append(b, 0xff, 0xd9)
os.Stdout.Write(b)
}
$ go run main.go | djpeg /dev/stdin > /tmp/foo.pnm
Corrupt JPEG data: 18 extraneous bytes before marker 0xd9
結果として得られる /tmp/foo.pnm は、完全に正常な 1x1 の赤い画像です。
R=r
CC=golang-dev
https://golang.org/cl/7750043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a3d1c1bdce6101212465a59ef24107402d920dda
元コミット内容
commit a3d1c1bdce6101212465a59ef24107402d920dda
Author: Nigel Tao <nigeltao@golang.org>
Date: Wed Mar 13 10:44:45 2013 +1100
image/jpeg: ignore extraneous data, the same as what libjpeg does.
Fixes #4705.
Note that libjpeg will print a warning to stderr if there are many
extraneous bytes, but can be silent if the extraneous bytes can fit
into its int32 bit-buffer for Huffman decoding. I'm guessing that
this is why whatever encoder that produced the image filed for issue
4705 did not realize that they are, strictly speaking, generating an
invalid JPEG. That issue's attached image has two extraneous bytes.
For example, piping the program below into libjpeg's djpeg program
will print an "18 extraneous bytes" warning, even though N == 20.
$ cat main.go
package main
import (
"bytes"
"image"
"image/color"
"image/jpeg"
"os"
)
const N = 20
func main() {
// Encode a 1x1 red image.
m := image.NewRGBA(image.Rect(0, 0, 1, 1))
m.Set(0, 0, color.RGBA{255, 0, 0, 255})
buf := new(bytes.Buffer)
jpeg.Encode(buf, m, nil)
b := buf.Bytes()
// Strip the final "\xff\xd9" EOI marker.
b = b[:len(b)-2]
// Append N dummy 0x80 bytes to the SOS data.
for i := 0; i < N; i++ {
b = append(b, 0x80)
}
// Put back the "\xff\xd9" EOI marker.
b = append(b, 0xff, 0xd9)
os.Stdout.Write(b)
}
$ go run main.go | djpeg /dev/stdin > /tmp/foo.pnm
Corrupt JPEG data: 18 extraneous bytes before marker 0xd9
The resultant /tmp/foo.pnm is a perfectly good 1x1 red image.
R=r
CC=golang-dev
https://golang.org/cl/7750043
変更の背景
この変更の背景には、Go言語の image/jpeg
パッケージが、一部の「厳密には不正な」JPEGファイルをデコードできないという問題がありました。具体的には、JPEGストリームのマーカー間に予期せぬデータ(extraneous data)が存在する場合、Goのデコーダはエラーを返していました。
しかし、広く使われている libjpeg
ライブラリは、このような余分なデータが存在しても、それを無視してデコードを続行する「寛容な」挙動を示します。Issue #4705では、このような libjpeg
では問題なくデコードできるが、Goの image/jpeg
ではエラーとなるJPEGファイルが報告されました。これは、一部のJPEGエンコーダが、厳密な仕様に準拠しない形でファイルを生成している可能性があることを示唆しています。
この挙動の違いは、Goの image/jpeg
パッケージの互換性と実用性に影響を与えていました。多くの既存のJPEGファイルが libjpeg
の寛容なデコードに依存しているため、Goのデコーダがそれらを拒否すると、ユーザーは不便を感じることになります。このコミットは、libjpeg
の挙動に合わせることで、より多くのJPEGファイルをGoで処理できるようにし、実用的な互換性を高めることを目的としています。
コミットメッセージの例では、意図的にSOS (Start of Scan) マーカーとEOI (End of Image) マーカーの間にダミーバイトを挿入したJPEGファイルを作成し、libjpeg
の djpeg
コマンドが警告を出しつつも正常にデコードできることを示しています。この挙動をGoのデコーダでも再現することが、この変更の主要な動機です。
前提知識の解説
JPEGファイルフォーマットの基本
JPEG (Joint Photographic Experts Group) は、主に写真などの連続階調画像を圧縮するための標準的な画像フォーマットです。JPEGファイルは、複数の「セグメント」で構成されており、各セグメントは「マーカー」と呼ばれる2バイトのコードで始まります。マーカーは 0xFF
に続く1バイトのコードで表現されます。
主要なマーカーには以下のようなものがあります。
- SOI (Start of Image):
0xFFD8
- 画像データの開始を示します。 - APPn (Application-specific):
0xFFE0
から0xFFEF
- アプリケーション固有のデータ(Exif情報など)を含みます。 - DQT (Define Quantization Table):
0xFFDB
- 量子化テーブルを定義します。 - SOF (Start of Frame):
0xFFC0
から0xFFCF
- フレームの開始、画像サイズ、コンポーネント数などを定義します。 - DHT (Define Huffman Table):
0xFFC4
- ハフマンテーブルを定義します。 - SOS (Start of Scan):
0xFFDA
- スキャンデータの開始を示します。このセグメントの後に実際の圧縮された画像データが続きます。 - EOI (End of Image):
0xFFD9
- 画像データの終了を示します。
JPEGデコーダは、これらのマーカーを読み取りながら、ファイルの構造を解析し、圧縮された画像データをデコードしていきます。
ハフマン符号化とビットバッファ
JPEGの画像データは、ハフマン符号化という可変長符号化方式で圧縮されています。デコーダは、ビットストリームからハフマンコードを読み取り、対応するシンボル(DCT係数など)に変換します。この際、デコーダは通常、ビットバッファと呼ばれる内部バッファを使用して、ビット単位でデータを読み込みます。
libjpeg
のような実装では、このビットバッファが一定量のデータを先読みすることがあります。もし、SOSデータ(圧縮された画像データ)の終わりに、EOIマーカーの前に予期せぬバイトが存在する場合、ビットバッファがそれらのバイトを読み込んでしまう可能性があります。
エスケープシーケンス 0xFF00
JPEGデータストリーム内で、データバイトが 0xFF
となる場合、それはマーカーと誤解される可能性があります。これを避けるため、JPEG仕様では、データバイトが 0xFF
の場合は、その後に 0x00
を挿入して 0xFF00
というシーケンスとしてエンコードするルールがあります。デコーダは 0xFF00
を読み取った場合、0xFF
データバイトとして解釈し、0x00
は無視します。
libjpeg
の挙動
libjpeg
は、JPEGデコードのデファクトスタンダードとも言えるライブラリです。その設計思想の一つに「堅牢性」があります。これは、厳密なJPEG仕様に完全に準拠していないファイルであっても、可能な限りデコードを試みるというものです。
コミットメッセージにもあるように、libjpeg
は以下のような挙動を示します。
- マーカー間の余分なデータ: マーカー(特にSOSとEOIの間)に予期せぬデータが存在する場合、
libjpeg
はそれを「extraneous bytes」(余分なバイト)として扱い、警告(JWRN_EXTRANEOUS_DATA
)を出力しつつも、デコードを続行します。 - ハフマンデコード中の先読み:
jdhuff.c
のjpeg_fill_bit_buffer
のような関数は、ハフマンデコードのために可能な限り多くのバイトを読み込みます。これにより、スキャンデータの終わりを超えて、場合によってはEOIマーカーまで読み込んでしまうことがあります。この際、読み込んだデータの中にマーカーが含まれていれば、それを「戻す」処理が行われますが、非マーカーデータは戻されず、結果として少量の余分な非マーカーバイトが「黙って」無視されることがあります。 0xFF
フィルバイト: JPEG仕様 (B.1.1.2) では、任意のマーカーの前に任意の数の0xFF
フィルバイトを置くことができるとされています。libjpeg
はこれらのフィルバイトも適切に処理します。
このコミットは、Goの image/jpeg
パッケージが、これらの libjpeg
の寛容な挙動を模倣するように変更することで、より多くの「実世界の」JPEGファイルを扱えるようにすることを目指しています。
技術的詳細
Goの image/jpeg
パッケージのデコーダは、JPEGストリームを読み進める際に、常に 0xFF
バイトがマーカーの開始を示すことを期待していました。しかし、実際のJPEGファイルには、この 0xFF
バイトの前に、あるいはマーカー間に、予期せぬデータ(extraneous data)が含まれることがあります。従来のGoのデコーダは、このような状況に遭遇すると FormatError("missing 0xff marker start")
を返してデコードを中断していました。
このコミットの主要な変更点は、reader.go
内のマーカー読み込みロジックを修正し、libjpeg
と同様に、0xFF
バイトが見つかるまで余分なデータを読み飛ばすようにしたことです。
具体的には、d.tmp[0]
が 0xFF
でない場合にエラーを返すのではなく、d.tmp[0]
が 0xFF
になるまでバイトを読み進めるループが導入されました。このループ内で読み飛ばされるバイトは、厳密にはフォーマットエラーですが、libjpeg
が警告を出しつつもデコードを続行するのと同様に、Goのデコーダもこれを許容します。
さらに、0xFF00
というシーケンスがデータストリーム中に現れた場合、これは 0xFF
データバイトのエスケープシーケンスであり、0x00
は無視されるべきです。従来のデコーダは 0xFF00
をマーカーとして誤解釈する可能性がありましたが、この変更により、0xFF
の後に 0x00
が続く場合は、それを余分なデータとして扱い、読み飛ばすように修正されました。これにより、エスケープされた 0xFF
データバイトが正しく処理されるようになります。
この変更は、reader_test.go
に TestExtraneousData
という新しいテストケースを追加することで検証されています。このテストは、意図的にSOSマーカーとEOIマーカーの間にランダムな余分なデータを挿入したJPEGストリームを作成し、それがGoのデコーダによってエラーなくデコードされ、元の画像とほぼ同じ結果が得られることを確認します。これにより、デコーダが余分なデータを適切に無視できるようになったことが保証されます。
また、writer_test.go
では、averageDelta
関数が独立したヘルパー関数として抽出され、コードの重複が解消されています。これは直接的な機能変更ではありませんが、テストコードの品質向上に貢献しています。
コアとなるコードの変更箇所
変更の中心は src/pkg/image/jpeg/reader.go
の decode
メソッド内のマーカー読み込み部分です。
--- a/src/pkg/image/jpeg/reader.go
+++ b/src/pkg/image/jpeg/reader.go
@@ -245,10 +245,38 @@ func (d *decoder) decode(r io.Reader, configOnly bool) (image.Image, error) {
if err != nil {
return nil, err
}
- if d.tmp[0] != 0xff {
- return nil, FormatError("missing 0xff marker start")
+ for d.tmp[0] != 0xff {
+ // Strictly speaking, this is a format error. However, libjpeg is
+ // liberal in what it accepts. As of version 9, next_marker in
+ // jdmarker.c treats this as a warning (JWRN_EXTRANEOUS_DATA) and
+ // continues to decode the stream. Even before next_marker sees
+ // extraneous data, jpeg_fill_bit_buffer in jdhuff.c reads as many
+ // bytes as it can, possibly past the end of a scan's data. It
+ // effectively puts back any markers that it overscanned (e.g. an
+ // "\xff\xd9" EOI marker), but it does not put back non-marker data,
+ // and thus it can silently ignore a small number of extraneous
+ // non-marker bytes before next_marker has a chance to see them (and
+ // print a warning).
+ //
+ // We are therefore also liberal in what we accept. Extraneous data
+ // is silently ignored.
+ //
+ // This is similar to, but not exactly the same as, the restart
+ // mechanism within a scan (the RST[0-7] markers).
+ //
+ // Note that extraneous 0xff bytes in e.g. SOS data are escaped as
+ // "\xff\x00", and so are detected a little further down below.
+ d.tmp[0] = d.tmp[1]
+ d.tmp[1], err = d.r.ReadByte()
+ if err != nil {
+ return nil, err
+ }
}
marker := d.tmp[1]
+ if marker == 0 {
+ // Treat "\xff\x00" as extraneous data.
+ continue
+ }
for marker == 0xff {
// Section B.1.1.2 says, "Any marker may optionally be preceded by any
// number of fill bytes, which are bytes assigned code X'FF'".
コアとなるコードの解説
変更された reader.go
のコードは、JPEGデコードの堅牢性を高めるための重要なロジックを含んでいます。
-
for d.tmp[0] != 0xff
ループ:- 以前は
d.tmp[0]
が0xff
でない場合に即座にFormatError
を返していましたが、このループが追加されたことで、0xff
バイトが見つかるまでストリームを読み進めるようになりました。 d.tmp
はデコーダの内部バッファで、d.tmp[0]
とd.tmp[1]
にそれぞれ現在のバイトと次のバイトが格納されます。- ループ内では、
d.tmp[0]
にd.tmp[1]
の値をコピーし、d.tmp[1]
にはストリームから新しいバイトを読み込みます。これにより、1バイトずつストリームを「スキップ」していくことになります。 - このスキップ処理は、
libjpeg
が余分なデータを警告しつつも無視する挙動を模倣しています。コメントにもあるように、これは厳密にはフォーマットエラーですが、実用的な互換性のために許容されます。 - このループは、マーカーの開始を示す
0xff
バイトが来るまで、非0xff
バイトを読み飛ばし続けます。
- 以前は
-
if marker == 0
チェック:for
ループを抜けた後、d.tmp[0]
は0xff
になっています。marker
変数にはd.tmp[1]
の値、つまり0xff
の次のバイトが格納されます。if marker == 0
の条件は、0xff00
というバイトシーケンスを検出するためのものです。- JPEGの仕様では、データストリーム中に
0xff
というバイト値が現れる場合、それがマーカーと誤解されないように、その後に0x00
を挿入して0xff00
というエスケープシーケンスとしてエンコードされます。デコーダは0xff00
を読み取った場合、0xff
をデータバイトとして解釈し、0x00
は無視する必要があります。 - この
if marker == 0
のチェックとそれに続くcontinue
は、0xff00
シーケンスを「余分なデータ」として扱い、現在のマーカー処理をスキップして次のバイトの読み込みに進むことを意味します。これにより、エスケープされた0xff
データバイトが正しく処理され、デコードが続行されます。
これらの変更により、Goの image/jpeg
デコーダは、JPEGストリーム内の予期せぬ非マーカーバイトや 0xff00
エスケープシーケンスをより堅牢に処理できるようになり、libjpeg
と同等の寛容性を持つようになりました。
関連リンク
- Go Issue #4705: https://code.google.com/p/go/issues/detail?id=4705 (Google Codeのアーカイブ)
- Go CL 7750043: https://golang.org/cl/7750043 (Goのコードレビューシステム)
参考にした情報源リンク
- JPEG File Interchange Format (JFIF) Specification: https://www.w3.org/Graphics/JPEG/jfif3.pdf
- ITU-T T.81 (JPEG) Information technology – Digital compression and coding of continuous-tone still images – Requirements and guidelines: https://www.itu.int/rec/T-REC-T.81-199210-I/en
- libjpeg source code (jdmarker.c, jdhuff.c):
libjpeg
の具体的な挙動を理解するために、そのソースコード(特にマーカー処理とハフマンデコード関連のファイル)を参照しました。これは特定のURLではなく、libjpeg
の配布に含まれるファイルです。 - Go image/jpeg package documentation: https://pkg.go.dev/image/jpeg
- Stack Overflow and other technical forums: JPEGフォーマットの「extraneous data」や
0xFF00
エスケープシーケンスに関する一般的な議論や説明を参考にしました。