「服を剥がすツール」が出力するwebmファイルを解析し、画像データを取り出してみた

# 「服を剥がすツール」とは

「服を剥がすツール」は、iota氏が公開しているツールである。
2枚の画像と軌跡を指定すると、1枚目の画像を軌跡に沿って破き、後ろにある2枚目の画像の一部が現れるというアニメーションを作成することができる。
作成したアニメーションは、アニメーションGIFおよびwebmファイルとして出力することができる。

[服を剥がすツール](http://mygamestudio.x.fc2.com/paperpeel/)

:::note warn
このリンク先では、大人向けな内容を含む広告が表示されることがある。
広告の内容について、筆者は一切関与しない。
また、広告はサーバーの業者が自動で挿入しているものであり、ツールの公開者も関与していないと推測できる。
:::

## 生成例

|1枚目 red.png|2枚目 green.png|生成結果 output.gif|
|:-:|:-:|:-:|
|![red.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/492f9a3e-279a-53fa-c2d5-ceec3cde1c5c.png)|![green.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/8e92fabc-1399-afa2-405b-fc085b64ac53.png)|![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/0ad849a7-11dd-14f1-5f8b-519ede16c1dc.gif)|

## 出力形式について

前述の通り、このツールでは出力形式として

* アニメーションGIF
* webmファイル

を選択することができる。
アニメーションGIFはGIF形式の制約により256色までしか用いることができず、色の多い写真などを入力画像に用いると色が不自然に変化してしまうことがある。
webmファイルはGoogle Chromeのみ対応とされており、たとえばFirefoxでツールを開くと以下のように選択するラジオボタンが無効化される。

![Firefoxにおける出力形式選択UI](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/c523c82b-4f94-dd3e-f25c-9807a33446df.png)

また、Google Chromeでwebm形式で出力すると、動画が再生されない。

![Google Chromeにおけるwebm出力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/dc05b2dc-baaa-16ea-b3c6-ce7b38857cda.png)

生成したwebmファイルをダウンロードし、ffmpegで変換を試みても、「Invalid start code」などと表示され、画像を取り出すことはできなそうだった。

![生成したwebmファイルをffmpegに入力した結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/19885f39-d59e-4a1d-72d0-965922bd9915.png)

しかし、このwebmファイルにも何らかの画像の情報が含まれているのではと考え、今回解析を試みた。

:::note info
ここで生成される「webmファイル」は有効なWebM形式の動画ファイルではないと考えられるため、この記事では「WebM」ではなく「webm」と表記する。
:::

# 「服を剥がすツール」の出力データの解析

## アニメーションGIFの設定の確認

アニメーションGIFは再生できるものが生成できるので、前述の「生成例」のデータでアニメーションGIFを作成し、[Giam](https://www.vector.co.jp/soft/win95/art/se041233.html)で読み込んでみた。
すると、以下のことがわかった。

* 全部で17枚のフレームからなる
* 永久に繰り返す設定になっている
* 最初と最後のフレームは1秒、他のフレームは0.1秒のウェイトが設定されている
* 消去手段は全フレームで「何もしない」になっている

![生成したアニメーションGIFをGiamで開いた結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/74259b99-8833-ff52-daaf-c19986a4bd50.png)

## webmファイルを観察してみる

生成したwebmファイルをバイナリエディタで観察してみた。
(今回は [HxD](https://mh-nexus.de/en/hxd/) を用いている)

ファイルの先頭部分。よくわからないが、「webm」や「VP8」といったフォーマットに関係しそうな文字列が見える。

![ファイルの先頭部分](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/0f8a2529-bfc1-6b8d-b751-7ab3e61af1e1.png)

文字列「ICCP」に続いて、ゼロやASCII文字が多めのエリアがみられた。

![ゼロやASCII文字が多めのエリア](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/747c33a7-4378-fcaa-4296-55b0b2dcf5f3.png)

その後は、よくわからないが複雑そうなデータがあった。
画像を表していると推測できる。

![複雑そうなデータのエリア](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/fad41b99-2b69-b657-de37-1b4ac21654ee.png)

この後も、ゼロやASCII文字が多めのエリアと、よくわからないが複雑そうなデータのエリアが交互に出てきた。

## WebP形式の画像と比較してみる

「web」がつくメディアファイルの形式には、[WebM](https://www.webmproject.org/)のほかに画像の[WebP](https://developers.google.com/speed/webp?hl=en)が存在する。
そこで、生成例で使用した `red.png` をImageMagickでWebP形式に変換し、観察してみた。

```
magick convert red.png red.webp
```

![red.png をWebP形式に変換した結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/d7aa4fd8-a2ae-250b-d406-8ef03afc7aaf.png)

先ほどの「よくわからないが複雑そうなデータ」のエリアと同様に、「VP8」という文字列の後に複雑そうなデータが来ている。
しかも、「デコードされたテキスト」の表示に左下方向の斜めの薄い部分が見えるという特徴も一致している。
これは深い関係がありそうだと推測できる。

また、WebP形式のデータの先頭に「RIFF」という文字列が見える。
RIFFといえば、wavファイルで用いられる形式である。
上のWebP形式のデータをよく見ると、「RIFF」や「VP8␣」に続いてその後のデータのバイト数が4バイトのリトルエンディアンで記録されているようであることがわかる。

## 透過部分がある画像を入力してみる

透過部分を持つ以下のファイル `red_trans.png` を1枚目と2枚目の両方に設定し、webmファイルとしてアニメーションを出力してみた。

![red_trans.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/84633d9c-4ec7-d2e4-d9e0-0a02f7b27b29.png)

すると、「ゼロやASCII文字が多めのエリア」の後の文字列が「VP8」ではなく「ALPH」となった。

![文字列「VP8」があったところが「ALPH」になっている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/6a8a015b-9451-2410-afa8-4e0745aa0618.png)

少し進むと、文字列「VP8」もあった。

![その先に文字列「VP8」もあった](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/5902fb51-e5ab-2857-28a7-6215b3220606.png)

`red_trans.png` をWebP形式に変換すると、以下のようになった。
最初の行にあった「VP8」が「VP8X」となっており、その下に文字列「ALPH」もみられる。

![red_trans.png をWebP形式に変換した結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/55177f70-8639-c801-9a54-179a53b24573.png)

## WebP形式のフォーマットを確認する

WebP形式のデータの定義は、
[WebP Container Specification  |  Google for Developers](https://developers.google.com/speed/webp/docs/riff_container?hl=en)
にわかりやすく書かれている。

`red.png` を変換した `red.webp` の構造は「[Simple File Format (Lossy)](https://developers.google.com/speed/webp/docs/riff_container?hl=en#simple_file_format_lossy)」の章に書かれているものであり、12バイトのWebPのヘッダーに続いて画像データを表す「VP8␣」チャンクがある。

`red_trans.png` を変換した `red_trans.webp` の構造は「[Extended File Format](https://developers.google.com/speed/webp/docs/riff_container?hl=en#extended_file_format)」の章に書かれているものであり、以下の要素からなる。

* WebPのヘッダー (12バイト)
* VP8Xチャンク (8+10バイト)
  * 使用されている機能を表すフラグ (1バイト)
  * 予約領域 (3バイト)
  * 画像の幅 (3バイト)
  * 画像の高さ (3バイト)
* 透明度情報 ([ALPHチャンク](https://developers.google.com/speed/webp/docs/riff_container?hl=en#alpha))
* 画像データ (VP8␣チャンク)

`red_trans.webp` の `VP8X` チャンクのデータは、以下のものである。

```
10 00 00 00 73 01 00 4E 01 00
```

これは、以下の意味である。

|データ|意味|説明|
|---|---|---|
|`10`|使用機能フラグ|透明度を表す `L` は1、その他は0となっている|
|`00 00 00`|予約|ゼロを入れることになっている|
|`73 01 00`|画像の幅|横方向のピクセル数-1 (リトルエンディアン)|
|`4E 01 00`|画像の高さ|縦方向のピクセル数-1 (リトルエンディアン)|

## webmファイルをもう一度観察してみる

`output.webm` をもう一度観察してみると、最初の文字列「ICCP」の前に以下のデータがあることがわかった。

```
A3 20 03 F8 81 00 00 80 20 00 00 00 FF 01 00 FF 01 00
```

また、2番目の文字列「ICCP」の前には、以下のデータがあった。

```
A3 20 03 F8 81 00 43 80 20 00 00 00 FF 01 00 FF 01 00
```


さらに、`output_trans.webm` の最初と2番目の文字列「ICCP」の前には、それぞれ以下のデータがあった。

```
A3 20 10 00 81 00 00 80 30 00 00 00 73 01 00 4E 01 00
```

```
A3 20 10 00 81 00 43 80 30 00 00 00 73 01 00 4E 01 00
```

これらのデータを観察すると、以下の構造になっていそうであると推測できた。

|データ例|バイト数|意味|
|---|:-:|---|
|`A3 20`|2|ヘッダ？|
|`03 F8`|2|このデータと次のヘッダ？ `A3 20` の間のバイト数|
|`81`|1|未解明 固定っぽい|
|`00 43`|2|未解明 ファイルの後になるほど大きい値になる 時刻関係？|
|`80 20`|2|未解明 2バイト目が `20` のときと `30` のときがあるようだ|
|`00 00 00`|3|未解明 固定っぽい|
|`73 01 00`|3|WebPと同様の画像幅 (横方向のピクセル数-1)|
|`4E 01 00`|3|WebPと同様の画像高さ (縦方向のピクセル数-1)|

画像幅と画像高さはリトルエンディアンだが、バイト数と「時刻関係？」は**ビッグエンディアン**である。
なぜ統一されていないかは謎だが、とにかくこのようになっているようである。

なお、文字列「ICCP」はWebPの「[Color Profile](https://developers.google.com/speed/webp/docs/riff_container?hl=en#color_profile)」を表していると考えられる。
このデータの後 (文字列「ICCP」から次のヘッダ？ `A3 20` の直前まで) にWebP形式のチャンクデータが格納されているようである。
チャンクデータは

* チャンクの種類 (4バイト)
* チャンクデータのバイト数 (種類、バイト数の情報、パディングは含まない) (4バイト)
* チャンクデータ
* パディング (チャンクデータのバイト数が奇数なら1バイトのゼロ、偶数ならなし)

を並べたデータのようである。
(このパディングはWebP形式でも用いられる)

## 複雑な画像を入力してみる

webmファイルを観察した結果、画像データ1枚のサイズが2バイトで格納されるらしいことがわかった。
では、画像データのサイズが0xffffバイトを超える場合はどうなるだろうか？

入力画像を大きくして (ピクセル数を増やして) も縮小されて出力されるようなので、以下のランダムデータの画像 `random.png` を生成し、1枚目と2枚目の両方として用いた。

![random.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/6c1f8d6b-2521-f802-970e-fce244e90a6f.png)

すると、出力されたwebmファイルでは、以下のようなデータがみられた。

```
A3 10 03 3A 88 81 00 00 80 20 00 00 00 7F 02 00 DF 01 00
```

以前のデータと比べて

* 最初のヘッダ？が `A3 20` ではなく `A3 10` になっている
* バイト数が2バイトではなく3バイトになっている

という違いがあるが、その他の部分は同様であると思われる。

`ICCP` チャンクのデータが256バイト以上あるため、データサイズが1バイトで表されることはないだろう。
また、ピクセル数は増やせなそうなので、データサイズを4バイトで表すのも現実的ではないだろう。
よって、2バイトと3バイトのパターンがわかれば十分であるといえるだろう。

# まとめ

「服を剥がすツール」が出力するwebmファイルは、ヘッダに続き、`A3` から始まる画像データが連続した構造になっていそうであることがわかった。
それぞれの画像データは、`A3` から始まるメタデータ (データサイズや画像の大きさなどが格納されている) と、WebP形式のチャンクの列からなっていそうであることがわかった。
ここで出現するチャンクは、

* ICCP
* ALPH
* VP8␣

がみられた。
したがって、これらのチャンクを抜き出し、WebP形式のヘッダおよび `VP8X` チャンクを追加することで、WebP形式の画像を取り出すことができそうである。
また、WebP形式の画像を1枚ずつ取り出すだけでなく、まとめてアニメーションを行うWebP形式の画像にしてもいいだろう。

# 実装

これまでの解析結果を踏まえ、webmファイルから画像データを取り出すツールを実装した。

[再生できないwebmファイルから画像を取り出すツール](https://mikecat.github.io/webm_image_extractor/)

# おまけ

Firefoxで「服を剥がすツール」を開き、webmファイルで出力するラジオボタンの無効化を開発者ツールで解除する。
該当のラジオボタンについている属性 `disabled=""` を削除すればよい。

![hagasu-hack-20240121-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/8c9fcb31-9d0b-c929-d33b-b5e94f86dbb4.png)

そして、このラジオボタンを選択し、画像を選択して「アニメ生成」ボタンを押すと…

![hagasu-hack-20240121-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/97e6cb1e-6487-c1ff-38d7-ff5c86e5612d.png)

なんと、普通に生成できた！
しかも、Google Chromeで生成した時と違って、一発で**再生できる有効な動画ファイル**ができた！
「Google Chromeのみ対応」というのは**嘘っぱち**、むしろ**Google Chromeには非対応**ともいえる仕様だったのである！

ちなみに、Microsoft EdgeでもGoogle Chromeと同様の「最初からwebmファイルでの出力の選択肢は有効、生成されるファイルは動画としては無効」という挙動だった。

なお、Google Chromeでは動画としては無効なデータが生成されるというのは、使用しているライブラリ[whammy](https://github.com/antimatter15/whammy)の[既知の問題](https://github.com/antimatter15/whammy/issues/70)のようである。