画像や動画のエンコード/デコードの理屈はわかるけど実際どうなっているかはよくわかってないのでとりあえずJPEGファイルの仕様を調べてみる。
資料 https://www.w3.org/Graphics/JPEG/itu-t81.pdf
JISの日本語の資料もあったけど(JISX4301)、閲覧が面倒くさすぎて英語で読む方がまだマシであった。
上から読んでいっても理解できなさそうなのでJPEGファイルのフォーマットから読みつつ、実際にJPEGファイルを読むコードも書いていく。"Annex B: Compressed data formats"から。
基本的にはデータやパラメータやハフマン符号のテーブルやらのフィールドが並んでいて、それぞれのフィールドは先頭2バイトのMarkerと呼ばれるマジックナンバーで判別できるようになっている。それでフィールドの中にヘッダやペイロードがあったりなかったりして、最後に終端のMarkerがついて終わり。
このMarkerというのは必ず0xFF
から始まる2バイトで、Annex Bの最初の方に全部の定義が書いてある。JPEGファイル内で0xFF
から始まる2バイトはMarkerしかなくて、データ内に偶然に出現する0xFF
は0xFF00
にエスケープされる。ということらしい。
それならとりあえずMarkerだけを取り出して並べて見るとなんとなくの構造がわかりそう。こんなかんじで↓
package main import ( "bufio" "io" "log/slog" "os" "strconv" "strings" ) func marker(i uint64) string { return "0xFF" + strings.ToUpper(strconv.FormatUint(uint64(i), 16)) } func main() { r := bufio.NewReader(os.Stdin) for { b, err := r.ReadByte() if err == io.EOF { slog.Info("FINISHED") return } else if err != nil { slog.Error("ReadByte", "err", err) return } if b != 0xFF { continue } m, err := r.ReadByte() if err == io.EOF { slog.Error("unexpected EOF") return } else if err != nil { slog.Error("read marker", "err", err) return } if m == 0x0 { continue } slog.Info("marker", "val", marker(uint64(m))) } }
これで適当なファイルを読む。
go run main.go < test.jpg INFO marker val=0xFFD8 INFO marker val=0xFFDB INFO marker val=0xFFC0 INFO marker val=0xFFC4 INFO marker val=0xFFDA INFO marker val=0xFFD9 INFO FINISHED
意外と少ない。これを表B.1と見比べてみると
0xFFD8
Start of image : ファイルの先頭0xFFDB
Define quantization table(s) : そのまんま、量子化係数のテーブル0xFFC0
Baseline DCT : SOF(start of frame)。圧縮の方式はbaseline(progressiveやlosslessじゃないやつ)で、non-differentialで離散コサイン変換(DCT)。0xFFC4
Define Huffman table(s) : これもそのまんま、ハフマン符号表。0xFFDA
Start of scan : Figure B.2からみるとここに符号化データが入ってる。0xFFD9
End of image : ファイルの終端
ハフマン符号化は元データのヒストグラムに偏りがある方が圧縮効率が良くなるので、元画像の特性によってアルゴリズムを変更できるようになっている。
次はこの中のどれかのフィールドの中身を見ていこう。