画像や動画のエンコード/デコードの理屈はわかるけど実際どうなっているかはよくわかってないのでとりあえず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と見比べてみると
0xFFD8Start of image : ファイルの先頭0xFFDBDefine quantization table(s) : そのまんま、量子化係数のテーブル0xFFC0Baseline DCT : SOF(start of frame)。圧縮の方式はbaseline(progressiveやlosslessじゃないやつ)で、non-differentialで離散コサイン変換(DCT)。0xFFC4Define Huffman table(s) : これもそのまんま、ハフマン符号表。0xFFDAStart of scan : Figure B.2からみるとここに符号化データが入ってる。0xFFD9End of image : ファイルの終端
ハフマン符号化は元データのヒストグラムに偏りがある方が圧縮効率が良くなるので、元画像の特性によってアルゴリズムを変更できるようになっている。
次はこの中のどれかのフィールドの中身を見ていこう。