JPEGに30年眠っていた:静止画像がアニメーションになる

JPEGに30年眠っていた:静止画像がアニメーションになる

JPEG画像技術史豆知識

データソース:Lobsters + web research

何気ない「疑問」が、30年前の隠された能力を暴いた

7月17日、著名なセキュリティ研究者 lcamtuf(Michał Zalewski)が Mastodon に気軽な考えを投稿した:

“Progressive JPEGs send the low-frequency data first, then gradually fill in the detail. I bet you could use it the other way around — make a ‘regressive JPEG’ that looks fine at first and slowly degrades into something terrible.”

この投稿は技術界隈で大規模な議論を呼ばなかった——しかし誰かがそれを見て、そして手を動かした。

二日後、開発者の Maurycy が技術コミュニティ Lobsters に成果を投稿した:「退化画像」を作るだけでなく、GIFのようにJPEGファイル一つを動かせるのだ。 この投稿は △95 点を獲得し、その日の最高票コンテンツになった。

regressive JPEG の例:読み込み中に「草地の猫」から「コンクリートの上の別の猫」へと切り替わる猫の画像

▲ 出典:maurycyz.com。同じJPEGファイルが、読み込みの段階ごとに異なる猫を表示する——ブラウザは自分がまだ「徐々に鮮明になっている」と思っているが、実際には内容がすり替えられている。

JPEG規格は1992年に生まれ、現在で34年。デジカメからスマホのアルバム、ウェブの画像まで、世界中で毎日数十億枚のJPEGが閲覧・転送・保存されている。そして先月、この規格には誰も使ったことのない能力——アニメーションを作ること——が眠っていることが発見された。


まずボケを送り、それからシャープを補う——天才的な設計

この発見を理解するために、筆者はまず90年代の設計を説明しなければならない。

初期のインターネットを知る人なら覚えているだろう:回線が遅いとき、大きな画像を開くと、画面は上から一行ずつスキャンされていった。これを「ベースラインJPEG」と呼ぶ——データは順番に並び、ブラウザは受け取った分だけ表示する。

後にJPEG規格に「プログレッシブJPEG」という選択肢が加わった。その働きは全く異なる:まず画像の「輪郭」を送り、その後徐々にディテールを補う。

身近な比喩を出そう:

信号の悪い山の中にいる友人が、あなたに集合写真を送りたいとする。古いやり方なら、写真が上から一マスずつ刷り出されるのを見るだけだ。しかし「プログレッシブ」なら、友人はまず1/16サイズのぼやけたサムネイルを送れる——あなたは一瞬でこれが集合写真だと見分けられる。その後ディテールのデータを送り、人物の表情、服の生地、背景の葉っぱが、少しずつ鮮明になっていく。

この設計の技術的な実現方法は、画像データを複数の「バッチ」(規格では scan と呼ぶ)に分割することだ。各バッチには、自分がどの精度範囲を担当するかを示すラベルが付く。最初のバッチは最も大まかな情報だけを含み、後続のバッチがより高い周波数のディテールを補う。

鍵となるのは:規格は各バッチがどの範囲をカバーするかを宣言することを定めているが、後続のバッチが記述する内容が前面と同じ画像でなければならないとは、一度も定めていないことだ。


まるで配送伝票に「同じ人が受け取る」と書いてないようなもの

lcamtuf の当初のアイデアは「退化JPEG」だった:最初のバッチにきれいな画像を置き、後続のバッチで徐々に醜いものへと上書きしていく。たとえば美味しそうな食事の写真が、読み込み半ばでカビた食べ物になる、といった具合に。

しかし Maurycy はさらに先へ行けると気づいた。

後続のバッチがそれ以前にレンダリングされた内容を上書きできるなら、なぜ異なる複数の画像を同じファイルに押し込まないのか。その具体策は驚くほど単純だ:

同じサイズの複数のJPEG画像の「データセグメント」をつなぎ合わせ、間に挟まる余分なマーカーヘッダを取り除けば、ブラウザはそれらを同一画像の複数の「精度層」として順にレンダリングする。

比喩してみよう:

あなたが配送会社に一連の荷物を送る。各荷物の送り状には「配送先:李四の家、品物:写真」と書いてある。配達員は一つずつ届け、そのたびに李四の家の「写真」を更新する。しかし李四が受け取る二枚目の写真は一枚目と全く別のものだ——配送システムは「写真」というカテゴリが合っているかしかチェックせず、同じ写真かはチェックしない。

ブラウザも同様だ。各バッチのラベル(精度範囲、カラーチャンネル)がJPEG規格に適合するかをチェックするが、「この画像が前のものと同じ内容か」は一度もチェックしない。新しいデータを古い画面に忠実に上書きする。そして——

あなたが見ている画面は変わる。


「画像のすり替え」から「動画の再生」へ

最初の方式には一つの制限があった:普通のプログレッシブJPEGは約10のバッチ(scan)を含む。ほとんどのブラウザのデコーダは約10バッチ受け取ると「この画像はもう読み込み終わったはずだ」と考え、それ以上の受け取りを拒む。つまり八九フレームしか押し込めず、アニメーションを作るには遠く及ばない。

Maurycy は最適化を続けた。彼は「最小化バッチ」を作れると発見した:各フレームにDCスキャンを一回だけ使う(最も基礎的な色情報だけを含み、ディテールは含まない)。こうすれば画像サイズは完全版の1/16の鮮明さになるが、一フレームのアニメーションとしては十分だ。

この方法で、Chromeブラウザはあきらめる前に約90フレームをレンダリングでき、Firefoxの忍耐はさらに高い。 90フレームあれば、短い動画には十分だ。

JPEGファイルで実現した「動画」:遠くからカメラへ歩み寄る黒猫

▲ 出典:maurycyz.com。静的なJPEGファイルが、プログレッシブバッチの上書き特性を利用して、読み込み中に黒猫が歩くアニメーションをフレームごとに再生する。これは完全にJPEG規格に準拠したファイルで、どのブラウザでも開ける。


これ、何に使うの?

率直に言って——使い道はない。

このハックには致命的な欠陥がある:再生速度を制御できない。なぜならアニメーションの「フレームレート」は完全に回線速度に依存するからだ——ダウンロードが速ければ速く再生し、遅ければ遅く再生する。GIFや動画フォーマットにあるような時間制御の仕組みは持たない。

しかもほとんどの画像ビューアは画像が「読み込み完了」すると表示を止め、ループ再生はしない。アニメーション効果を見るには、回線が遅い環境で少しずつ読み込ませるか、特別な表示方法を使う必要がある。

しかしそれは重要ではない。

本当に面白いのはこの発見そのものだ:30年前に書かれた国際規格が、30年間無数のエンジニアに読まれ、実装され、使われてきたのに、誰一人それがアニメーションできるとは気づかなかった——誰かが何気なく一つ尋ねるまで。


見過ごされてきた規格、そして技術的好奇心の勝利

筆者はこの物語に特別な魅力を感じる。

JPEG規格——国際標準化機構(ISO)が発行し、数千ページの技術文書で、世界中のすべての画像ソフトに実装されている——それはそこに、じっとして、34年。その每行の仕様は公開されており、誰でも読める。

プログレッシブJPEGの「スキャン上書き」機構は最初から規格に書かれていた。lcamtuf が発見したのは何か新しい脆弱性ではない。彼がしたのは、規格が「できない」と一度も言っていない問いを投げただけだ:後続のスキャンのデータが前面と異なっていたら、何が起きるのか?

規格はこの問いに答えられない——そもそも誰かがそうするとは考えていなかったからだ。

そして Maurycy は何の複雑なプログラムも書かなかった。彼のコードは短いCファイル一つで、本質的にやっていることは「何枚かの画像の真ん中部分をつなぎ合わせ、前後のマーカーを取り除く」ことだ。本当の難しさはそうやっていいと気づくことにある。

技術界隈では「ベストプラクティス」「パフォーマンス最適化」「アーキテクチャ設計」を巡る議論が多すぎる。時折こうした物語が現れる——誰かが30年前の古い文書を引っ張り出し、一行を指して「ここには禁止と書いてない」と言う——ハッカー精神の本来の姿を思い出させる。


最後に

自分でも試してみたいなら、Maurycy はブログでコードとサンプル画像をすべて公開している。数百KBのJPEGファイルをブラウザで開けば、読み込み中にゆっくりと「息づき」始める。

もちろん、これは何か画期的な技術突破ではない。GIFを置き換えることも、画像フォーマットの勢力図を変えることもない。しかしそれは思わず微笑んでしまう発見だ——古い家の床下に誰も開けたことのない隠し扉を見つけたようなもので、扉の向こうに宝は何もないが、それを開けた瞬間そのものが一種の喜びなのだ。

参考リンク:

  • Lobsters 討論: Regressive JPEGs
  • lcamtuf の元の Mastodon での質問
  • Maurycy の技術ブログ: Regressive JPEGs