SWFバイナリ編集のススメ第三回 (JPEG)
こんにちは。メディア開発のよやです。
前回、SWF書き換えの簡単なサンプルを示しました。
今回は、JPEG 画像入れ替えについてです。
SWFにおける画像の扱い
SWF はベクター画像とビットマップ画像の両方に対応しています。
画像情報を格納する tag には幾つかの種類があります。(tag 名の後ろにつく数字は省略)
- DefineShape (ベクター画像。どのペンで何処に線を引いたといった情報)
- DefineBitsJPEG (JPEG画像) (*1)
- DefineBitsLossless (PNG的な可逆圧縮画像, シンプルなフォーマットをZlib圧縮)
これらの中で比較的差し替えが楽な JPEG画像を取り上げます。
JPEG tag について
-
Flash Lite 1.1/2.0 で対応する JPEG 系 tag は以下の4つです。
- JPEGTables, DefineBits, DefineBitsJPEG2, DefineBitsJPEG3
- JPEG のデータはそのままでは、これらの tag に入りません。(そのまま入れても IE, FireFox では表示できますが、携帯では大抵表示できませんし、仕様的にもイリーガルです)
- JPEG の chunk 構造と SWF のタグの変遷から、その歴史的事情を追ってみます。
JPEG chunk (基礎知識)
- JPEG はメタデータ、圧縮テーブル、(圧縮された)イメージデータ等の chunk で分かれています。
(*2)
- SOI(Start of Image) で始まり、EOI(End of Image) で終わります。
- 基本は TLC (Tag, Length, Content)構造ですが、Lenght フィールドを持たない chunk もあります。
- Length フィールドは 2byte です。SWF と異なり BigEndian で、かつ Lengthフィールド(2byte)を含めた長さです。
1 2 3 4 5 6 7 |
% hexdump -C orz.jpg 00000000 ff d8 ff e0 00 10 4a 46 49 46 00 01 01 01 00 60 |......JFIF.....`| <---> <----|-----|------------------------------ SOI APP0 len=0x10 00000010 00 60 00 00 ff db 00 43 00 08 06 06 07 06 05 08 |.`.....C........| ----------> <----|-----|------------------------- DQT len=0x43 |
- SOS, RST は Length フィールドがなく、次の Marker が来るまでです。(これが少々やっかいな仕様)
1 2 3 4 5 6 |
00000140 22 ff da 00 0c 03 01 00 02 11 03 11 00 3f 00 a0 |"............?..| <----|--------------------------------------- SOS (↑lengthでは無い) 000008d0 88 80 88 88 08 88 80 88 88 08 88 83 ff d9 |..............| -----------------------------------> <---> EOI |
- SOS や RST のデータ中に Marker 相当の 2byte が含まれる事もあり得るので、その場合は FF ?? が FF 00 ?? にエスケープされます。そこで、SOS や RST の終端を探す際は、 FF 00 以外の FF ?? をチェックするという面倒な処理になります。
- 以上の処理を PHP で実装してみました。 > IO_SWF_JPEG function _splitChunk()
- http://svn.openpear.org/IO_SWF/branches/1.0/IO/SWF/JPEG.php
1 2 3 |
$swf_jpeg = new IO_SWF_JPEG(); $swf_jpeg->input($jpegdata); $swf_jpeg->dumpChunk(); |
- 実行結果
1 2 3 4 5 6 7 8 9 10 11 12 |
% php jpeg_dump.php dump orz.jpg SOI: APP0: length=14 md5=6231819e4c55dc62557f1cda73329e99 DQT: length=65 md5=d0eaa368737f17f6037757d393a22599 DQT: length=65 md5=0ccab5367a4a1a6b9d4f5dc5b1b6da3f SOF0: length=15 md5=c83e80b171b05e3d1daaecc9f7a9d4b2 DHT: length=26 md5=c30a44c47fc7a3bf3dc08b0d0c16cb24 DHT: length=54 md5=ad4a54e8cffc484a1d57d8b9619159e8 DHT: length=21 md5=f5142d552f32b0fd9b6f01287d6187cd DHT: length=27 md5=5340e2d349d844450c870b9a37698f85 SOS: length=1945 md5=8003cab9c3564d49fb37d5f4a30d8a1e EOI: |
さて、ここから本題です。
JPEG tag の構造
JPEGTables + DefineBits
- (Flash の前身) Future Splash (SWF version 1 相当)では、JPEG 画像は2つの tag を用いて表現していました。
- JPEG chunk のうち、圧縮テーブル(量子化テーブル、ハフマン符号化テーブル)がJPEGTables に入り、残り(メタデータ、圧縮された画像データ等)を DefineBits に入れます。各々 SOI と EOI で囲みます。
- JPEGTables は SWF の中に一つだけ置けます。
(*3)
- 圧縮テーブルを JPEGTables に共通して持つ事で、スペース(SWF全体のサイズ)が節約できます。
- しかしながら実際には、画像に合った圧縮テーブルを使わないと、圧縮率や画質に悪影響が出ます。
- そこで、DefineBitsJPEG2 の登場です。
DefineBitsJPEG2
- DefineBitsJPEG2 は SWF version 2 で追加された tag です。
- 情報的には通常の JPEG ファイルと同等ですが、 JPEG chunk の並びが異なる上に、SOI, EOI が2つづつ存在します。
- chunk を良く見ると JPEGTables と DefineBits をそのまま繋げた形式です。
- それ故、JPEG ファイルのデータを DefineBitsJPEG2 にそのままでは入れられませんし、DefineBitsJPEG2 からデータを抜き出しても、画像ビューアで表示できません。
erroneous header
- 但し、FF D9 FF D8 + 生の JPEG データ形式も可能となっています。(SWF version 8 より前限定で)
- 仕様書では FF D9 FF D8 を erroneous header と呼びます。JPEG chunk では、FF D9 = EOF (End of Image)、FF D8 = SOI (Start of Image) に相当します。
- この仕様に頼れば JPEG 入れ替えはとても簡単です。(固定の4byteを付加するのみ)
- erroneous とはよく言ったもので、EOI (End of Image) から始めるという JPEG 的にあり得ない出だしにする事で、今までの JPEGTables + DefineBits の形式と区別しているようです。(SOI と EOI が 2つあればよい説もありますが、SOI EOI を頭に付けた場合に画像が表示されない端末もあるので、EOI SOI をつけるのが無難です)
DefineBitsJPEG3
- SWF version 3 で JPEG 画像に透明度の情報を付加できるようになりました。
- JPEG そのものは透明度を扱う事が出来ないので、JPEG データを格納するフィールドの後ろに、独立して透明度の情報(BitmapAlphaData)をつける形式となります。
-
非矩形画像を扱う場合に便利です。例えばキャラクター絵で輪郭の外を透明にする等です。
- 透明度付きPNG 画像を劣化ありで取り込む事で、このタグを生成できます。
- Alpha Data は、画像の左上から横方向に scan して、各々の pixel の透明度を 255:不透明、0:透明、その中間を半透明とした alpha 値 8 bit を並べた配列で表現します。
- この pixel 分(縦サイズと横サイズをかけた数)の長さの配列を Zlib 圧縮したものが、 DefineBitsJPEG3 のタグの最後の部分に保存されます。
SWF の tag 書き換え
tag の中身を書き変えて tag の長さが変わった場合は、tag の length フィールドを書き換えるだけでなく、
SWF Header の FileLength フィールドも書き換える必要があります。
逆にいうとそれだけで良いです。
FileLength も 4bytes 固定で処理は簡単です。
JPEG を入れ替える
- ここまでの知識を元に、DefineBitsJPEG2 の JPEG データの入れ替えを行います。
- 以下の画像(ethnyan.jpg)に入れ替えます。
方法A) chunk 並び替え
- JPEG chunk を圧縮用テーブルとその他(メタデータ&画像データ)に分けて、DefineBitsJPEG2 のタグの中身を入れ替えます。
- http://svn.openpear.org/IO_SWF/branches/1.0/sample/swfreplacejpeg.php
- http://svn.openpear.org/IO_SWF/branches/1.0/IO/SWF/JPEG.php
1 2 3 4 5 6 |
$swf_jpeg = new IO_SWF_JPEG(); $swf_jpeg->input($jpegdata); $jpeg_table = $swf_jpeg->getEncodingTables(); $jpeg_image = $swf_jpeg->getImageData(); // 21: DefineBitsJPEG2 $swf->replaceTagContentByCharacterId(21, $image_id, $jpeg_table.$jpeg_image); |
- 圧縮テーブルと、その残りの chunk を別々に抜出し、連結したデータを DefineBitsJPEG2 のコンテンツに差し込みます。
- コマンド実行
1 |
% php swfreplacejpeg.php orz.swf 1 ethnyan.jpg > ethnyan.swf |
- 入れ替え前の SWF (orz.swf)
- 入れ替え後の SWF (ethnyan.swf)
方法B) erroneous header 付加
- JPEG の先頭に、EOI,SOI (FF D9 FF D8 の 4 byte)をつけます。
1 2 3 |
$erroneous_header = pack('CCCC', 0xFF, 0xD9, 0xFF, 0xD8); // 21: DefineBitsJPEG2 $swf->replaceTagContentByCharacterId(21, $image_id, $erroneous_header.$jpegdata); |
- 単純に erroneous header (FF D9 FF D8) を JPEG の頭につけて、それを DefineBitsJPEG2 に差し込みます。
- 入れ替え後の SWF (ethnyan-erroneous.swf)
- A, B どちらの方法でも、携帯端末で画像を表示できます。
- 実際の SWF の DefineBitsJPEG2 JPEG chunk 構造を調べた限りでは、昔に作成された SWF は方法A(JPEGTabes+DefineBits相当)、最近作成された SWF は方法B(errorneous header 付加) が多いようです。
JPEG画像をマスク付きで入れ替える
DefineBitsJPEG2 の入れ替えでは、背景の色が変わった時に都合の悪い事があります。
- 背景を青にした場合 (ethnyan-blue.swf)
DefineBitsJPEG3 を用いてマスク付きで画像を入れ替える事で対処できます。
まず、以下のようなマスク画像(ethnyan-mask.gif)を用意します。
これを SWF の Bitmap Alpha Data の形式に変換します。
libgif の gif2rgb を利用すると楽です。
RGB の R 成分を Alpha Data として用いる事にします。
1 2 3 |
% gif2rgb -o ethnyan-mask ethnyan-mask.gif % ls -l ethnyan-mask.R -rw-r--r-- 1 yoya develop 14400 8月 27 20:53 ethnyan-mask.R |
縦 120 x 横 120 = 14400 で正しそうです。
この ethnyan-mask.R を $alphadata として以下のように処理します。
1 2 3 4 5 6 |
$tag_code = array(6, 21, 35); $jpeg_data = $jpeg_table.$jpeg_image; $compressed_alphadata = gzcompress($alphadata); $content = pack('v', $image_id).pack('V', strlen($jpeg_data)).$jpeg_data.$compressed_alphadata; $tag = array('Code' => 35, 'Content' => $content); $ret = $swf->replaceTagByCharacterId($tag_code, $image_id, $tag); |
- コマンド実行
1 |
% php swfreplacejpeg.php blueorz.swf 1 ethnyan.jpg ethnyan-mask.R > ethnyan.swf |
- 入れ替えのイメージ図
- 入れ替え後
このように簡単に入れ替えられます。
参考URL
- http://www.m2osw.com/swf_tag_definebitsjpeg
- http://siisise.net/jpeg.html#format
- http://pwiki.awm.jp/~yoya/?Flash/SWF/format/Jpeg
予告
DefineLossless(可逆圧縮ビットマップ画像), DefineShape(ベクター画像)、DefineSprite(ムービークリップ)のいずれかを取り上げる予定です。
それでは失礼いたします。
*1: SWF バージョン 8 から PNG や GIF をそのまま入れられる事になりました。BitsJPEG というタグ名と実情が合っていませんが。
*2: JPEG chunk の並びはある程度融通が利くので、この図と異なる事があります。
*3: 空の JPEGTables を置く事で、DefineBits に生の JPEGデータを入れる方法もあるようですが、未検証なので今回取り上げません