SWFバイナリ編集のススメ第四回 (GIF)

こんにちは。メディア開発のよやです。

今回は、GIF 画像入れ替えについてお話します。(PNG 画像入れ替えは次の機会に)

GIF の情報を格納できるタグ

  • DefineBitsLossless, DefineBitsLossless2 が利用出来ます。(*1)
  • DefineBitsLossless は、ビットマップ画像をシンプルに表現して Zlib 圧縮する形式です。
  • この DefineBitsLossless に透明度情報を加えたのが、DefineBitsLossless2 です。

GIF の特徴 (基礎知識)

  • 可逆圧縮のフォーマットです。(JPEGと違って画像の細部が潰れません)
  • 色テーブル(カラーマップ)を持ちます。いわゆるパレット(インデックスカラー)形式です。
  • 最大256色まで扱えます。透明色(半透明はダメ)も扱えます。
  • (蛇足ですが) 複数の画像を格納してアニメーション出来ます。(今回は無関係)

パレット形式

  • GIF はパレット形式でビットマップ画像を保存します。
  • 利用する色のテーブル(ColorMap)を作って、ビットマップの各 pixel の色がカラーマップの何番目なのかのインデックス配列で画像データ(RasterBits)を表現します。

  • ヘッダの TransparentIndex を利用して、色テーブルの何番目を透明色にするか指定できます。

  • ここから本題です。

DefineBitsLossless タグ

DefineBitsLossless (透明色なし)

  • DefineBitsLossless は 3種類のフォーマットを持ちます。
  • GIF はパレット形式なので、format=3 を利用します。
  • format=3(palette) の構造は GIF の持つ情報と以下の対応を持ちます。

  • cid は CharacterID (SWF 内のコンテンツに一意に割り当てる ID) です。
  • Screen 情報から Width, Height (*2) を割り当てます。
  • ColorTableSize は色数を -1 した値です。(1 byte で最大256まで表現する為)
ColorTable & ColormapPixelData
  • ColorTable は 1色につき RGB の 8bit x 3 = 24bit 使います。

  • ColormapPixelData は、ビットマップ画像の各 pixel の色に対応する ColorTable インデックス(何番目の色を使うかの)値の配列です。
  • 画像の左上端から右方向に scan した配列を、上から下までつなげます。
  • 但し、pixel 横一列の単位で、その配列を 4 の倍数の長さに調整する必要があります。

DefineBitsLossless2 (透明色情報つき)

  • ColorTable が RGB から RGBA に変わるだけで後はほぼ同じです。
  • 例えば、色テーブルの一番初めのエントリを透明色として扱う場合以下のようになります。

GIF差し替え処理

DefineBitsLossless

  • ここまでの知識を元に、GIF 画像で差し替えるコードを示します。
  • PHP GD extension を使うと GIF フォーマットの解読をお任せ出来て、とても楽です。
$swf = new IO_SWF_Editor();
$swf->parse($swfdata);
$swf->setCharacterId($swfdata);

$im = imagecreatefromgif($giffile);

$colortable_num = imagecolorstotal($im);

$colortable = '';

for ($i = 0 ; $i < $colortable_num ; $i++) {
    $rgb = imagecolorsforindex($im, $i);
    $colortable .= chr($rgb['red']);
    $colortable .= chr($rgb['green']);
    $colortable .= chr($rgb['blue']);
}

$pixeldata = '';
$i = 0;
$width  = imagesx($im);
$height = imagesy($im);

for ($y = 0 ; $y < $height ; $y++) {
    for ($x = 0 ; $x < $width ; $x++) {
        $pixeldata .= chr(imagecolorat($im, $x, $y));
        $i++;
    }
    while (($i % 4) != 0) {
        $pixeldata .= chr(0);
        $i++;
    }
}

$tag_code = array(6, 21, 35, 20, 36); // DefineBits*

$format = 3; // palette format
$content = pack('v', $image_id).chr($format).pack('v', $width).pack('v', $height);
$content .= chr($colortable_num - 1).gzcompress($colortable.$pixeldata);

$tag = array('Code' => 20, // DefineBitsLossless
             'Content' => $content);
$ret = $swf->replaceTagByCharacterId($tag_code, $image_id, $tag);

echo $swf->build();

DefineBitsLossless2

  • DefineBitsLossless の ColorTable RGB の 3 byte を RGBA の 4 byte に拡張します。
  • GIF の透明インデックス値を取得するのに GD の imagecolortransparent を利用します。
for ($i = 0 ; $i < $colortable_num ; $i++) {
    $rgb = imagecolorsforindex($im, $i);
    $colortable .= chr($rgb['red']);
    $colortable .= chr($rgb['green']);
    $colortable .= chr($rgb['blue']);
}
<略>
$tag = array('Code' => 20, // DefineBitsLossless
             'Content' => $content);
$ret = $swf->replaceTagByCharacterId($tag_code, $image_id, $tag);

↑ これを以下↓のように書き換えます。

$transparent_index = imagecolortransparent($im);
for ($i = 0 ; $i < $colortable_num ; $i++) {
    $rgb = imagecolorsforindex($im, $i);
    if ($i == $transparent_index) {
        $colortable .= chr(0).chr(0).chr(0).chr(0);
    } else {
        $colortable .= chr($rgb['red']);
        $colortable .= chr($rgb['green']);
        $colortable .= chr($rgb['blue']);
        $colortable .= chr(255);
    }
}
<略>
$tag = array('Code' => 36, // DefineBitsLossless2
             'Content' => $content);
$ret = $swf->replaceTagByCharacterId($tag_code, $image_id, $tag);

(*3)

入れ替えデモ

  • 差し替え前 SWF (orz.swf)

  • 差し替え画像 (ahya.gif)

  • コマンド実行
% php swfreplacegif.php orz.swf 1 ahya.gif > ahya.swf
  • 差し替え後 SWF (ahya.swf)

簡単ですね!

参考URL

予告

DefineBitsLossless, DefineBitsLossless2 の format=3 に加え format=5 を利用する事で、
PNG 画像の差し替えが実現できます。次回取り上げる予定です。

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

*1: SWF 8 以降では DefineBitsJPEG 系に GIF データを格納できますが、今回は触れません

*2: 厳密には、GIF は複数コマのイメージを持てるので、GIF ファイル全体でのサイズ(Screen の Width, Height)と、各コマの画像サイズ(Image や page 等の表現があります)の2つがありますが、今回はGIFに画像を1枚のみで、それらのサイズが一致する前提で話しを進めます

*3: $i == $transparent_index の処理で alpha 値補正を考慮していなかったのでコードを修正しました。ご指摘ありがとうございます。 > @_RH__ さん

Author: よや