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 で分かれています。

JPEG Chunk (*2)

  • SOI(Start of Image) で始まり、EOI(End of Image) で終わります。
  • 基本は TLC (Tag, Length, Content)構造ですが、Lenght フィールドを持たない chunk もあります。
  • Length フィールドは 2byte です。SWF と異なり BigEndian で、かつ Lengthフィールド(2byte)を含めた長さです。

JPEG Chunk Length

% 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 が来るまでです。(これが少々やっかいな仕様)
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
$swf_jpeg = new IO_SWF_JPEG();
$swf_jpeg->input($jpegdata);
$swf_jpeg->dumpChunk();
  • 実行結果
% 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 の中に一つだけ置けます。

JPEGTables & DefineBits(*3)

  • 圧縮テーブルを JPEGTables に共通して持つ事で、スペース(SWF全体のサイズ)が節約できます。
  • しかしながら実際には、画像に合った圧縮テーブルを使わないと、圧縮率や画質に悪影響が出ます。
  • そこで、DefineBitsJPEG2 の登場です。

DefineBitsJPEG2

  • DefineBitsJPEG2 は SWF version 2 で追加された tag です。
  • 情報的には通常の JPEG ファイルと同等ですが、 JPEG chunk の並びが異なる上に、SOI, EOI が2つづつ存在します。
  • chunk を良く見ると JPEGTables と DefineBits をそのまま繋げた形式です。
  • それ故、JPEG ファイルのデータを DefineBitsJPEG2 にそのままでは入れられませんし、DefineBitsJPEG2 からデータを抜き出しても、画像ビューアで表示できません。

JPEGTables & 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 画像を劣化ありで取り込む事で、このタグを生成できます。

DefineBitsJPEG3

  • 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 のタグの中身を入れ替えます。

DefineBitsJPEG2 ethnyan

$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 のコンテンツに差し込みます。
  • コマンド実行
% 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)をつけます。

DefineBitsJPEG2 erroneous ethnyan

$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)を用意します。

ethnyan mask

これを SWF の Bitmap Alpha Data の形式に変換します。
libgif の gif2rgb を利用すると楽です。
RGB の R 成分を Alpha Data として用いる事にします。

% 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 として以下のように処理します。

$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);
  • コマンド実行
% php swfreplacejpeg.php blueorz.swf 1 ethnyan.jpg ethnyan-mask.R > ethnyan.swf
  • 入れ替えのイメージ図

DefineBitsJPEG3 ethnyan

  • 入れ替え後

このように簡単に入れ替えられます。

参考URL

予告

DefineLossless(可逆圧縮ビットマップ画像), DefineShape(ベクター画像)、DefineSprite(ムービークリップ)のいずれかを取り上げる予定です。

それでは失礼いたします。

*1: SWF バージョン 8 から PNG や GIF をそのまま入れられる事になりました。BitsJPEG というタグ名と実情が合っていませんが。

*2: JPEG chunk の並びはある程度融通が利くので、この図と異なる事があります。

*3: 空の JPEGTables を置く事で、DefineBits に生の JPEGデータを入れる方法もあるようですが、未検証なので今回取り上げません

Author: よや