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

  • SOS, RST は Length フィールドがなく、次の Marker が来るまでです。(これが少々やっかいな仕様)

  • 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

  • 実行結果

さて、ここから本題です。

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

  • 圧縮テーブルと、その残りの chunk を別々に抜出し、連結したデータを DefineBitsJPEG2 のコンテンツに差し込みます。
  • コマンド実行

  • 入れ替え前の SWF (orz.swf)
  • 入れ替え後の SWF (ethnyan.swf)

方法B) erroneous header 付加

  • JPEG の先頭に、EOI,SOI (FF D9 FF D8 の 4 byte)をつけます。

DefineBitsJPEG2 erroneous ethnyan

  • 単純に 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 として用いる事にします。

縦 120 x 横 120 = 14400 で正しそうです。
この ethnyan-mask.R を $alphadata として以下のように処理します。

  • コマンド実行

  • 入れ替えのイメージ図

DefineBitsJPEG3 ethnyan

  • 入れ替え後

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

参考URL

予告

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

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

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

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

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