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

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

本コミットは、Go言語のimage/jpegパッケージにおいて、JPEGデコーダが不正だが無害な末尾の再開マーカー(restart marker)を無視するように修正するものです。これにより、特定のエンコーダによって生成された、仕様に厳密には準拠していないJPEGファイルのデコードが可能になります。

コミット

commit 648c9eb0b5eb6ddfef70299fcf19a8c323913e44
Author: Nigel Tao <nigeltao@golang.org>
Date:   Tue Sep 18 21:57:33 2012 +1000

    image/jpeg: ignore an incorrect but harmless trailing restart marker.
    
    Fixes #4084.
    
    R=r
    CC=golang-dev
    https://golang.org/cl/6526043

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

https://github.com/golang/go/commit/648c9eb0b5eb6ddfef70299fcf19a8c323913e44

元コミット内容

image/jpeg: ignore an incorrect but harmless trailing restart marker. (image/jpeg: 不正だが無害な末尾の再開マーカーを無視する。)

Fixes #4084. (Issue #4084を修正。)

変更の背景

この変更は、Go言語のimage/jpegパッケージが特定のJPEG画像をデコードできない問題(Issue #4084)を解決するために行われました。具体的には、一部のJPEGエンコーダが、JPEG仕様に厳密には準拠しない形で、ファイルの末尾に余分な再開マーカー(restart marker)を付加することがありました。

JPEGの仕様では、再開マーカーはエントロピー符号化セグメント(Entropy Coded Segments, ECS)の間にのみ出現し、最後のECSの後には出現しないとされています(仕様のFigures B.2およびB.16がこれを示唆しています)。しかし、一部のエンコーダは、このルールに反して、ファイルの終端に再開マーカーを出力してしまうことがありました。

Goのimage/jpegデコーダは、このような不正な再開マーカーを予期しないマーカーとして扱い、デコードエラーを発生させていました。このコミットは、このような「不正だが無害な」再開マーカーを検出した場合に、それを無視してデコード処理を続行することで、より多くのJPEGファイルとの互換性を確保することを目的としています。

前提知識の解説

JPEG形式の概要

JPEG(Joint Photographic Experts Group)は、主に写真などの連続階調画像を圧縮するための標準的な画像形式です。JPEGファイルは、様々なセグメント(またはマーカー)で構成されており、それぞれが画像データやメタデータ、圧縮パラメータなどを定義しています。

JPEGマーカー

JPEGファイルは、特定の2バイトのマーカーコード(0xFFに続く1バイト)によってセグメントが区切られています。これらのマーカーは、ファイルの構造やデータの種類を示します。主要なマーカーには以下のようなものがあります。

  • SOI (Start Of Image, 0xFFD8): 画像の開始を示す。
  • EOI (End Of Image, 0xFFD9): 画像の終了を示す。
  • SOS (Start Of Scan, 0xFFDA): 走査(スキャン)の開始を示す。実際の画像データ(エントロピー符号化データ)が続く。
  • DRI (Define Restart Interval, 0xFFDD): 再開間隔を定義する。
  • APPn (Application-specific, 0xFFE0 - 0xFFEF): アプリケーション固有のデータセグメント。
  • COM (Comment, 0xFFFE): コメントセグメント。
  • RSTm (Restart Marker, 0xFFD0 - 0xFFD7): 再開マーカー。画像データが複数の独立したブロックに分割されている場合に使用され、デコード中にエラーが発生した場合でも、次の再開マーカーからデコードを再開できるようにします。RST0からRST7まで8種類あります。

再開マーカー (Restart Marker, RSTm)

再開マーカーは、JPEGのプログレッシブモードや、大きな画像を小さな独立したブロックに分割してエンコードする際に使用されます。これにより、デコード処理の堅牢性が向上し、一部のデータが破損しても画像全体が壊れることを防ぎます。

JPEGの仕様(特にITU-T T.81 | ISO/IEC 10918-1)では、再開マーカーはエントロピー符号化データ(SOSセグメント内のデータ)の途中にのみ出現し、セグメントの境界やファイルの終端に単独で出現することは想定されていません。再開マーカー自体は、その後に続くデータを持たず、単独で存在します。

Go言語のimage/jpegパッケージ

Go言語の標準ライブラリには、JPEG画像をエンコードおよびデコードするためのimage/jpegパッケージが含まれています。このパッケージは、JPEGファイルの構造を解析し、画像データをGoのimage.Imageインターフェースに変換する機能を提供します。デコード処理では、ファイルストリームからマーカーを読み取り、それに応じて適切な処理(セグメントのスキップ、データブロックのデコードなど)を行います。

技術的詳細

このコミットの技術的な核心は、JPEGデコーダがファイルの読み込みループ内で予期せぬ再開マーカーに遭遇した場合の挙動を変更することにあります。

従来のデコーダは、EOIマーカー(画像の終端)に到達するか、既知のセグメントマーカー(SOS, DRI, APPn, COMなど)を検出することを期待していました。もし、これらの期待されるマーカー以外のものが現れた場合、それは「未知のマーカー」としてエラーを報告するか、あるいは不正なファイル構造として処理を中断していました。

しかし、一部のJPEGエンコーダは、最後のエントロピー符号化セグメントの後に、誤って再開マーカー(RST0からRST7のいずれか)を出力することがありました。JPEG仕様のFigures B.2およびB.16は、再開マーカーがECS間でのみ発生し、最後のECSの後には発生しないことを示唆しています。

このコミットでは、デコーダのメインループにおいて、マーカーを読み込んだ直後に、そのマーカーがRST0からRST7の範囲内にあるかどうかをチェックする新しいロジックが追加されました。

if rst0Marker <= marker && marker <= rst7Marker {
    // Figures B.2 and B.16 of the specification suggest that restart markers should
    // only occur between Entropy Coded Segments and not after the final ECS.
    // However, some encoders may generate incorrect JPEGs with a final restart
    // marker. That restart marker will be seen here instead of inside the processSOS
    // method, and is ignored as a harmless error. Restart markers have no extra data,
    // so we check for this before we read the 16-bit length of the segment.
    continue
}

このコードブロックの意図は以下の通りです。

  1. マーカーの識別: 読み込んだmarkerが再開マーカーの範囲(rst0Markerからrst7Marker)内にあるかをチェックします。
  2. 仕様との乖離の認識: コメントで明示されているように、これはJPEG仕様の推奨(再開マーカーはECS間のみ)に反するケースです。
  3. 「無害なエラー」としての扱い: このような末尾の再開マーカーは、画像データ自体には影響を与えないため、「無害なエラー」と見なされます。再開マーカーは追加のデータを持たないため、その後にセグメント長を読み込む必要もありません。
  4. 処理の継続: continueステートメントを使用することで、デコーダは現在のループの残りの部分をスキップし、次のマーカーの読み込みに進みます。これにより、不正な再開マーカーが検出されても、エラーを発生させることなくデコード処理が続行されます。

この変更により、GoのJPEGデコーダは、より多くの「非標準的だが実用上問題ない」JPEGファイルを正常に処理できるようになり、堅牢性と互換性が向上しました。

また、もう一つの小さな変更として、app0Markerからapp15Markerの範囲をチェックする条件式がmarker >= app0Marker && marker <= app15Markerからapp0Marker <= marker && marker <= app15Markerに変更されています。これは機能的には同じですが、Goの慣用的な書き方(定数を左に置く)に合わせたものと考えられます。

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

--- a/src/pkg/image/jpeg/reader.go
+++ b/src/pkg/image/jpeg/reader.go
@@ -403,6 +403,15 @@ func (d *decoder) decode(r io.Reader, configOnly bool) (image.Image, error) {
 	\t\tif marker == eoiMarker { // End Of Image.
 	\t\t\tbreak
 	\t\t}\n+\t\tif rst0Marker <= marker && marker <= rst7Marker {\n+\t\t\t// Figures B.2 and B.16 of the specification suggest that restart markers should\n+\t\t\t// only occur between Entropy Coded Segments and not after the final ECS.\n+\t\t\t// However, some encoders may generate incorrect JPEGs with a final restart\n+\t\t\t// marker. That restart marker will be seen here instead of inside the processSOS\n+\t\t\t// method, and is ignored as a harmless error. Restart markers have no extra data,\n+\t\t\t// so we check for this before we read the 16-bit length of the segment.\n+\t\t\tcontinue\n+\t\t}\n \n \t\t// Read the 16-bit length of the segment. The value includes the 2 bytes for the
 \t\t// length itself, so we subtract 2 to get the number of remaining bytes.
 \t\tvar n int
 \t\tif marker != dhtMarker && marker != dqtMarker && marker != sosMarker && marker != driMarker && marker != app0Marker && marker != comMarker {
@@ -431,7 +440,7 @@ func (d *decoder) decode(r io.Reader, configOnly bool) (image.Image, error) {
 \t\t\terr = d.processSOS(n)
 \t\tcase marker == driMarker: // Define Restart Interval.
 \t\t\terr = d.processDRI(n)
-\t\tcase marker >= app0Marker && marker <= app15Marker || marker == comMarker: // APPlication specific, or COMment.
+\t\tcase app0Marker <= marker && marker <= app15Marker || marker == comMarker: // APPlication specific, or COMment.
 \t\t\terr = d.ignore(n)
 \t\tdefault:
 \t\t\terr = UnsupportedError(\"unknown marker\")

コアとなるコードの解説

変更はsrc/pkg/image/jpeg/reader.goファイルのdecodeメソッド内で行われています。このメソッドは、JPEGストリームを読み込み、解析するデコーダの主要なロジックを含んでいます。

  1. 再開マーカーの無視ロジックの追加: if marker == eoiMarker { break } の直後に新しいifブロックが追加されました。

    if rst0Marker <= marker && marker <= rst7Marker {
        // Figures B.2 and B.16 of the specification suggest that restart markers should
        // only occur between Entropy Coded Segments and not after the final ECS.
        // However, some encoders may generate incorrect JPEGs with a final restart
        // marker. That restart marker will be seen here instead of inside the processSOS
        // method, and is ignored as a harmless error. Restart markers have no extra data,
        // so we check for this before we read the 16-bit length of the segment.
        continue
    }
    

    このコードは、現在読み込んだmarkerRST0からRST7のいずれかの再開マーカーであるかをチェックします。もしそうであれば、コメントで説明されているように、それは不正だが無害な末尾の再開マーカーであると判断し、continueステートメントによって現在のループの残りの処理(セグメント長の読み込みや、マーカーに応じた処理のディスパッチ)をスキップし、次のマーカーの読み込みに進みます。これにより、デコーダはこのような不正なマーカーをエラーとせずに無視し、デコードを続行できます。

  2. 条件式の記述スタイルの変更: case marker >= app0Marker && marker <= app15Marker || marker == comMarker:case app0Marker <= marker && marker <= app15Marker || marker == comMarker: に変更されました。 これは機能的な変更ではなく、Go言語のコーディングスタイルにおける慣習的な変更です。比較演算子において、定数を左側に配置する(例: 定数 <= 変数)のが一般的であり、可読性や一貫性を高めるための修正です。

これらの変更により、image/jpegパッケージは、JPEG仕様に厳密には準拠しないが、実用上問題のないJPEGファイルをより堅牢に処理できるようになりました。

関連リンク

参考にした情報源リンク