SWFバイナリ編集のススメ第七回 (Shape基本構造)

こんにちは。プラットフォーム開発のよやです。

今回は DefineShape タグについて解説します。DefineShape タグは Flash SWF がベクター画像を格納するタグです。
第3回~第5回でとりあげた Bitmap 系(DefineBitsJPEG, DefineBitsLossless)タグと、この DefineShape タグの機能と構造を理解すれば、(fla ファイル無しでも) Flash の任意の画像を好きなように編集できます。
DefineShape タグの大まかな機能とバイナリ構造を紹介して、最後にバイナリデータを編集する PHP のコードと動作サンプルを示す。といった流れで進めます。

読み解くポイント

  • 一般的な描画ツールで絵を書くときには、まずパレットで色や太さを指定してから線を引きます。DefineShape はそれをなぞったようなフォーマットです。
  • 現在のペンの位置からの差分の値で描画領域を指示するので、描画コンテキストとして current drawing point を持つ必要があります。
  • DefineShape タグはサイズを減らす為の工夫がなされていて、その分、ビットフィールドの構造が複雑です。バイナリを読むのに慣れていないと 解読作業がとっても楽しい 読み解くのに多少時間がかかると思います。
  • Edge で囲んだ部分を Fill で定義された色やパターンで塗る際に、Edge の左側を FillStyle0, 右側を FillStyle1 として 2 つの Fill を参照します。恐らく、Shape の中でこれが一番理解しにくいです。(途中で少し説明します)

DefineShape タグの全体構成

STYLE (Fill & Line Styles)で線(Line)や塗り(Fill)の描画の表現(何色でどんな太さか等)を定義し、SHAPE (Shape Records)で、使う Style を切り替え(Change)ながら、直線(Straight Edge)又は曲線(Curved Edge)で描画領域を決めていきます。

  • “diagram illustrates the SHAPEWITHSTYLE structure.” (SWF仕様書から引用)

The SHAPEWITHSTYLE structure

Styles と Shape Records

Styles には以下の表現があります。

  • 線の描画 (Line Styles)

    • 単色
  • 塗りの描画 (Fill Styles)

    • 単色
    • グラデーション (Gradient)
    • ビットマップ(の貼り付け) (Bitmap)

Shape Records の Edge には以下の表現があります。

  • 直線 (Straight Edge)
  • 曲線 (Curved Edge) – 2次ベジエ曲線 (2次なので、2次 B-スプライン曲線相当)

今回はそれらの表現に共通する基本構造である Styles (描画ツールのパレットのようなもの)と Shape Records (描画のベクターデータ)の関係に絞って説明します。
(グラデーションやビットマップの構造は、次回以降で)

DefineShape タグの種類

Flash Lite1.x/2.x では DefineShape , DefineShape2, DefineShape3, DefineMorphShape が使われます。これらを順に解説します。

DefineShape タグ

  • DefineShape の全体構造としては、ID が先頭にきて、描画枠を RECT で定義し、その後ろに STYLE と SHAPE が続きます。

DefineShape全体イメージ

  • 尚、この図のレベルでは、DefineShape2, DefineShape3 も共通です。

STYLE (Fill & Line Styles)

塗り(Fill)と線(Line)の描画スタイルを定義します。

  • 塗り(Fill)は TYPE によってその表現が決まっていて、それに応じてデータ要素もかわります。

FillStyles 構造

  • 線(Line)の定義は線の太さと色だけです。単純です。

LineStyles の構造

SHAPE (NumBits & Shape Records)

上記の FILLSTYLE や LINESTYLE を参照して、それをどの位置に描画するのかを定義していきます。

  • Shape Records の直前で Style を参照する値を格納するのに必要なビット数を示し、その後ろに ChangeStyle, Edge, End の Record が続きます。

DefineShape SHAPE構造

  • Style Change Record では FillStyle0, FillStyle1, LineStyle で Style のインデックスを参照してペンをはじめに置く場所を指定します。Style のインデックスは 1 数えで、0 を未指定(undefined)として扱います。

ChangeStyle構造

  • FillStyle0 と FillStyle1 は Edge の向きの左側を塗る Fill と右側を塗る Fill を同時に指定する為のものです。

    • http://codeazur.com.br/fitc/HackingSWF.pdf の画像がイメージし易いので引用します。FillStyle0 and FillStyle1 Image
    • 何故、左と右の2つを指定するのかですが、Edge でくくった領域をオーバーラップさせた場合に一筆書きで表現し易いといったメリットがあるようです。詳しくは SWF 仕様書(v10 spec であれば p137)を参照して下さい。↓以下のような図で説明されています。

fillStyle0 vs fillStyle1

  • 描画する領域を決めるのは、Edge Records です。Straight Edge(直線)と Curved Edge(曲線)の2種類があります。

  • Edge のうち Straight Edge は 直線を表すレコードです。

    • 現在のペン位置からどのくらい移動するか(MoveDeltaX, MoveDeltaY)の値が入ります。
    • 初めに、MoveDeltaX, MoveDeltaY の値を表すのに最低限必要なビット数を入れて、そのビット数のフィールドが続きます。
    • 水平線や垂線の場合は、各々 x 又は y のみのフィールドを持ちます。

  • Curved Edge は曲線を表すレコードです。

    • 制御点と終点の座標値を差分で持ちます。
    • 2次ベジエ曲線として描画されます。

DefineShape2 タグ

DefineShape は Style を 0xff (=255) 個までしか持てませんが、DefineShape2 では 0xffff (=65535)個まで保持できます。また、Shape Records の途中で新しい Styles を再定義する事もできます。
初めに全 Styles を置けばシンプルなのにと思いますが、Change Record 中で Styles を指し示す Index の値フィールド長が可変(NumFillBits, NumLineBits)なので、Shape Records の途中で必要になった時点で Styles を再定義した方が全体としてのサイズが少なくて済む事が多いです。

STYLE (Fill & Line Styles)

DefineShape とほぼ同じですが、Style 数のカウンタが異なります。

SHAPE (NumBits & Shape Records)

  • NumFillBits, NumLineBits に続いて Shape Records が並ぶ、SHAPE の基本構造は DefineShape と同様です。

DefineShape SHAPE構造

  • Change Records の 2bit 目が StateNewStyles フラグとして機能します。このフラグを 1 にする事で、Styles を再定義出来ます。そこから後ろの Shape Records は この(前の Styles を忘れて) 新しい Styles を参照します。

  • Styles が変われば、Styles を参照する値フィールドのビット数もあわせる必要があるので、NumFillBits, NumLineBits も再定義します。

DefineShape3 タグ

  • 透明度(alpha channel)対応です。DefineShape2 の STYLE は色を RGB の 24 bit で表現しますが、それを RGBA の 32 bit に拡張します。それだけの違いです。

STYLE (Fill & Line Styles)


SHAPE (Shape Records)

  • NewStyles で定義し直す STYLE の色が RGBA である事以外は、DefineShape2 と同じです。

DefineMorphShape タグ

  • 情報要素の種類は DefineShape3 とほぼ同じで、Morph 開始時と終了時とで2重に Styles と Shape Records(Edges) を持ちます。
  • 開始と終了で Edge の数が各々同じで、終了時の Edge に ChangeStyle を含めない等の縛りがあります。
  • DefineMorphShape で独特なのは Offset フィールドです。 Flash Player は、この Offset を元に EndEdges フィールドにアクセスする為、DefineMorphShape を編集した際には併せて Offset フィールドを更新しないと正しく表示されません。


PHP で処理

DefineShape タグの parse & dump & build 処理を PHP で実装しました。

openpear の IO_Bit, IO_SWF パッケージを利用します。

pear channel-discover openpear.org
pear install openpear/IO_Bit
pear install openpear/IO_SWF

DefineShape の冒頭

  • RECORDHEADER(tag code & length) の直後に Shape Id (= Character ID) と描画枠(RECT Type)があり、その後ろに SHAPEWITHSTYLE が続きます。
$this->_shapeId = $reader->getUI16LE();
$this->_shapeBounds = IO_SWF_Type::parseRECT($reader);
<SHAPE WITH STYLE の処理>

STYLE

FILLSTYLE と LINESTYLE の配列です。
FILLSTYLE の Gradient 処理の引用は省略していますが、svn.openpear.org には処理が存在します。興味があればそちらを参照して下さい。

$this->_parseFILLSTYLEARRAY($reader);
$this->_parseLINESTYLEARRAY($reader);

function _parseFILLSTYLEARRAY(&$reader, $tagCode) {
    // FillStyle
    $fillStyleCount = $reader->getUI8();
    if (($tagCode > 2) && ($fillStyleCount == 0xff)) {
       // DefineShape2 以降は 0xffff サイズまで扱える
       $fillStyleCount = $reader->getUI16LE();
    }
    for ($i = 0 ; $i < $fillStyleCount ; $i++) {
        $fillStyle = array();
        $fillStyleType = $reader->getUI8();
        $fillStyle['FillStyleType'] = $fillStyleType;
        switch ($fillStyleType) {
          case 0x00: // solid fill
            if ($tagCode < 32 ) { // 32:DefineShape3
                $fillStyle['Color'] = IO_SWF_Type::parseRGB($reader);
            } else {
                $fillStyle['Color'] = IO_SWF_Type::parseRGBA($reader);
            }
              break;
          case 0x10: // linear gradient fill
          case 0x12: // radial gradient fill
            <略>
        break;
          // case 0x13: // focal gradient fill // 8 and later
          // break;
          case 0x40: // repeating bitmap fill
          case 0x41: // clipped bitmap fill
          case 0x42: // non-smoothed repeating bitmap fill
          case 0x43: // non-smoothed clipped bitmap fill
            $fillStyle['BitmapId'] = $reader->getUI16LE();
            $fillStyle['BitmapMatrix'] = IO_SWF_Type::parseMATRIX($reader);
            break;
          default:
            // 受理できない旨のエラー出力
            break 2; // ループ終了
        }
        $this->_fillStyles[] = $fillStyle;
    }
}
function _parseLINESTYLEARRAY(&$reader, $tagCode) {
    $lineStyleCount = $reader->getUI8();
    if (($tagCode > 2) && ($lineStyleCount == 0xff)) {
       // DefineShape2 以降は 0xffff サイズまで扱える
       $lineStyleCount = $reader->getUI16LE();
    }
    for ($i = 0 ; $i < $lineStyleCount ; $i++) {
        $lineStyle = array();
        $lineStyle['Width'] = $reader->getUI16LE();
        if ($tagCode < 32 ) { // 32:DefineShape3
            $lineStyle['Color'] = IO_SWF_Type::parseRGB($reader);
        } else {
            $lineStyle['Color'] = IO_SWF_Type::parseRGBA($reader);
        }
        $this->_lineStyles[] = $lineStyle;
    }
}

SHAPE WITH STYLE

StyleChangeRecord で対応する STYLE と描画の初期位置を決めて、そこから Edge で線や曲線を引き、また、StyleChangeRecord で。というのを繰り返して、End で終了します。StyleChangeRecord の StateNewStyles が 1 の場合は、新規に STYLE を読みなおします。

$this->_parseFILLSTYLEARRAY($reader);
$this->_parseLINESTYLEARRAY($reader);
$reader->byteAlign();
// 描画コンテキスト
$numFillBits = $reader->getUIBits(4);
$numLineBits = $reader->getUIBits(4);
$currentDrawingPositionX = 0;
$currentDrawingPositionY = 0;
$currentFillStyle0 = 0;
$currentFillStyle1 = 0;
$currentLineStyle = 0;

$done = false;
// ShapeRecords
while ($done === false) {
    $typeFlag = $reader->getUIBit();
    if ($typeFlag == 0) {
        $endOfShape = $reader->getUIBits(5);
        if ($endOfShape == 0) {
            $done = true;
        } else {
            // StyleChangeRecord
            $reader->incrementOffset(0, -5);
            $stateNewStyles = $reader->getUIBit();
            ...
            if ($stateNewStyles) {
                $this->_parseFILLSTYLEARRAY($reader);
                $this->_parseLINESTYLEARRAY($reader);
                $reader->byteAlign();
                $numFillBits = $reader->getUIBits(4);
                $numLineBits = $reader->getUIBits(4);
            }
        }
    } else {
        $straightFlag = $reader->getUIBit();
        if ($straightFlag) {
             // StraightEdgeRecord
             ...
        } else {
             // CurvedEdgeRecord
             ...
        }
    }
}

動作サンプル

% php IO_SWF/sample/swfdump.php negi.swf
<略>
ShapeId: 1
ShapeBounds:
        (-7.75, -7.75) - (7.75, 7.75)
FillStyles:
        solid fill: #0066ff
LineStyles:
ShapeRecords:
        ChangeStyle: MoveTo: (-7.75, -7.75)  FillStyle: 0|1  LineStyle: 0
        StraightEdge: MoveTo: (7.75, -7.75)
        StraightEdge: MoveTo: (7.75, 7.75)
        StraightEdge: MoveTo: (-7.75, 7.75)
        StraightEdge: MoveTo: (-7.75, -7.75)

デフォルメの実験

デフォルメの仕組み

ここまでに説明してきた内容を元に、DefineShape 中の Edge の数を減らして画像をデフォルメしてみます。
StyleChange と Edge リストの組がひとつの線になるので、まずはその単位で処理を分割します。

function deforme($threshold) {
    $startIndex = null;
    foreach ($this->_shapeRecords as $shapeRecordIndex => $shapeRecord) {
        if (($shapeRecord['TypeFlag'] == 0) && (isset($shapeRecord['EndOfShape']) === false)) {
            // StyleChangeRecord
            $endIndex = $shapeRecordIndex - 1;
            if (is_null($startIndex) === false) {
                $this->deformeShapeRecordUnit($threshold, $startIndex, $endIndex);
            }
            $startIndex = $shapeRecordIndex;
        }
        if (isset($shapeRecord['EndOfShape']) && ($shapeRecord['EndOfShape']) == 0) {
            // EndShapeRecord
            $endIndex = $shapeRecordIndex - 1;
            $this->deformeShapeRecordUnit($threshold, $startIndex, $endIndex);
        }
    }
    $this->_shapeRecords = array_values($this->_shapeRecords);
}

一定距離の閾値を $threshold 引数で受けて、距離内に曲線を直線に、距離内にある2つの直線を1つの直線に。という戦略をとります。
SWF 内部の座標は TWIPS 単位の値を取りますので、20 倍した値が pixel の距離に相当します。

function deformeShapeRecordUnit($threshold, $startIndex, $endIndex) {
//  return $this->deformeShapeRecordUnit_1($threshold, $startIndex, $endIndex);
    return $this->deformeShapeRecordUnit_2($threshold, $startIndex, $endIndex);
}
  • 試行錯誤中なのでコメントアウトで切り替えているのは御愛嬌で。
  • コード量がそこそこあるので deformeShapeRecordUnit の先は引用しません。興味のある方は IO/SWF/Shape.php を参照して下さい。

デフォルメ実行

  • サンプル素材 SWF (sample.swf)

=拡大=>

  • デフォルメ実行 (閾値:100)
php sample/swfdeformeshape.php sample.swf 100 > deforme-100.swf

=拡大=>

この場合で 40KByte が 37KByte に減ります。
違いが分かりにくいかもしれないので、更に強めにデフォルメしてみます。

  • デフォルメ実行 (閾値:1000)
php sample/swfdeformeshape.php sample.swf 1000 > deforme-1000.swf

=拡大=>

35KByte になりました。ちょっとこれはやり過ぎですね。

デフォルメの敷居値をうまく決める事が出来れば、SWF ファイルの転送量を減らしたり、Flash Player のメモリ消費量を減らしたりと嬉しい事があるかもしれません。

備考

  • ChangeStyle の座標値には MoveDeltaX, MoveDeltaY の名前が付いていて、その説明でも、続くレコードは “relatice to the current drawing position” と書かれているのですが、実際に試すと毎回 origin (0, 0) からの Delta で計算しないと都合が合わないので、図には MoveX, MoveY と表記しました。
  • Bit 単位のデータ処理では(bit padding による) byte alignment が重要ですが、DefineShape は大抵、alignment が必要な所で Byte 要素を読みだす事が多いので滅多に問題になりません。それで油断してると、StateNewStyles = 1 の時の NumFillBits (4bit field) や、DefineMorphShape でビットマップ画像を扱う時の EndBitmapMatrix で Byte Alignment を取り忘れて 1/8 の確率で罠にはまったりします。

参考

次回予定

次回は FILLSTYLE のうちの Bitmap について説明する予定です。
それでは失礼いたします。

Author: よや