Rails における swfmill を用いた動的 Flash(swf) 生成の一手法

概要

 携帯電話用の Flash(Flash Lite 1.x) の作成にはいろいろ厄介な制限がある*1.とりわけキツいのは,HTTP 通信が,ユーザからの入力1つにつき1回のみに制限されている点である.つまり,1クリックに画像1つだけとか,テキスト一つだけしか取得できない.これはデータベースと連携してコンテンツを表示するようなアプリケーションを作成するとき,非常に厄介な問題となる.
 そのような場合の解決策として,loadMovie 関数とサーバーサイドによる swf 動的生成を組み合わせた方法がよく使われている*2.この方法は,サーバサイドで画像やテキストなどを一つの Flash ファイルに埋め込み,それをクライアントアプリケーションが getURL を使って読み込む方法である.getURL は引数に指定された URL が示すファイルを読み込んで,ブラウザで表示する機能である.同じような動作をする関数に loadMovie があるが,loadMovie で読み込んだファイルは unloadMovie で明示的に解放しない限り端末に蓄積していくので,ファイルサイズの制限に引っかかる場合があり,おすすめしない.

 重要なのは,「どのようにして SWF を動的生成するか」という問題である.そのひとつに swfmill を使う方法がある*3.swfmill は xml と swf の相互変換を行えるソフトウェアで,オープンソースで公開されている*4
 今回は,swfmill を rails から使って flash を動的に生成する方法を考えてみた.なお,swfmill は,文字コード用のパッチ*5を含め正常にインストールされていることを前提とする.

設計と実装

 動作手順は次のようにする.

  1. クライアントアプリケーションが loadMovie を使ってサーバにファイルを問い合わせる.(このときクエリも付加できる.)
  2. サーバプログラムが要求を受け取ると,rails アプリはテンプレート用 xml の内容を書き換える.(この テンプレート用 xml は事前に作成しておく)
  3. rails アプリは書き換えた xml を,swfmill を使って swf に変換する.
  4. rails アプリは完成した swf ファイルをクライアントアプリケーションに提供する.

 実装を行う.ここでは例として,本の一覧を携帯で見るためのシステムを作ってみる.

クライアント用の swf

 クライアントはボタンを押したら getURL を呼び出すだけ.適当にボタンを配置して,下のようなアクションを設定するだけで良い.開発環境は Flash CS3 である.

 作成画面

 ActionScript

on (KeyPress "<Enter>"){
  getURL("http://localhost:3000/swfgen/?page=0");
}
テンプレート用 XML

 テンプレート用の XML は,書き換えたい変数名に法則性を持たせるなど,プログラムが書き換え易いように作っておかなければならない.

 本記事では以下のようにした.

  1. テキストフィールドを5つ作り,それぞれに変数名を指定する.
  2. 1フレーム目の ActionScript で,テキストフィールドそれぞれに指定した変数にダミーの値を代入する.
  3. ページ番号を保存するための offset という変数を用意して,ダミーの値を代入する.
  4. 「次へ」ボタンを作り,そのボタンを押すとページ番号を1増やしたクエリを付加して getURL の要求を出す.

プログラム側では,この ActionScript 内のダミーの値を書き換えれば良い.本記事では,テンプレートをFlash CS3 で作成し,それを swfmill で変換して使った.(できる人は XML をガリガリ書いても良いと思う.)

作成画面

ActionScript

  • 1フレーム目
name0 = "name0_value";
name1 = "name1_value";
name2 = "name2_value";
name3 = "name3_value";
name4 = "name4_value";
page = "page_value";
  • 「次へ」ボタン
on(press){
  page += 1;
// loadMovie を使うとファイルサイズ制限に引っかかるので,getURL を使う.
  getURL("http://localhost:3000/swfgen/?page=" add page);
}

 XML に変換したものは以下.

<?xml version="1.0" encoding="UTF-8"?>
<swf version="4" compressed="0">
  <Header framerate="24" frames="1">
    <size>
      <Rectangle left="0" right="4800" top="0" bottom="6400"/>
    </size>
    <tags>
      <FileAttributes hasMetaData="1" useNetwork="0"/>
      <Metadata>
        <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
          <rdf:Description xmlns:dc="http://purl.org/dc/1.1/" rdf:about="">
            <dc:title>Sony Ericsson - 240x320 (Flash Lite 1.1)</dc:title>
          </rdf:Description>
        </rdf:RDF>
      </Metadata>
      <SetBackgroundColor>
        <color>
          <Color red="51" green="51" blue="51"/>
        </color>
      </SetBackgroundColor>
      <DoAction>
        <actions>
          <PushData>
            <items>
              <StackString value="name0"/>
            </items>
          </PushData>
          <PushData>
            <items>
              <StackString value="name0_value"/>
            </items>
          </PushData>
          <SetVariable/>
          <PushData>
            <items>
              <StackString value="name1"/>
            </items>
          </PushData>
          <PushData>
            <items>
              <StackString value="name1_value"/>
            </items>
          </PushData>
          <SetVariable/>
          <PushData>
            <items>
              <StackString value="name2"/>
            </items>
          </PushData>
          <PushData>
            <items>
              <StackString value="name2_value"/>
            </items>
          </PushData>
          <SetVariable/>
          <PushData>
            <items>
              <StackString value="name3"/>
            </items>
          </PushData>
          <PushData>
            <items>
              <StackString value="name3_value"/>
            </items>
          </PushData>
          <SetVariable/>
          <PushData>
            <items>
              <StackString value="name4"/>
            </items>
          </PushData>
          <PushData>
            <items>
              <StackString value="name4_value"/>
            </items>
          </PushData>
          <SetVariable/>
          <PushData>
            <items>
              <StackString value="page"/>
            </items>
          </PushData>
          <PushData>
            <items>
              <StackString value="page_value"/>
            </items>
          </PushData>
          <SetVariable/>
          <EndAction/>
        </actions>
      </DoAction>
      <DefineFont2 objectID="1" isShiftJIS="1" isUnicode="0" isANSII="0" wideGlyphOffsets="0" italic="0" bold="0" language="0" name="_ゴシック">
        <glyphs/>
      </DefineFont2>
      <DefineEditText objectID="2" wordWrap="1" multiLine="1" password="0" readOnly="1" autoSize="0" hasLayout="1" notSelectable="0" hasBorder="0" isHTML="0" useOutlines="0" fontRef="1" fontHeight="360" align="0" leftMargin="0" rightMargin="0" indent="0" leading="40" variableName="name0">
        <size>
          <Rectangle left="-40" right="4039" top="-40" bottom="552"/>
        </size>
        <color>
          <Color red="255" green="255" blue="255" alpha="255"/>
        </color>
      </DefineEditText>
      <PlaceObject2 replace="0" depth="1" objectID="2">
        <transform>
          <Transform transX="400" transY="387"/>
        </transform>
      </PlaceObject2>
      <DefineEditText objectID="3" wordWrap="1" multiLine="1" password="0" readOnly="1" autoSize="0" hasLayout="1" notSelectable="0" hasBorder="0" isHTML="0" useOutlines="0" fontRef="1" fontHeight="360" align="0" leftMargin="0" rightMargin="0" indent="0" leading="40" variableName="name1">
        <size>
          <Rectangle left="-40" right="4039" top="-40" bottom="552"/>
        </size>
        <color>
          <Color red="255" green="255" blue="255" alpha="255"/>
        </color>
      </DefineEditText>
      <PlaceObject2 replace="0" depth="2" objectID="3">
        <transform>
          <Transform transX="400" transY="1183"/>
        </transform>
      </PlaceObject2>
      <DefineEditText objectID="4" wordWrap="1" multiLine="1" password="0" readOnly="1" autoSize="0" hasLayout="1" notSelectable="0" hasBorder="0" isHTML="0" useOutlines="0" fontRef="1" fontHeight="360" align="0" leftMargin="0" rightMargin="0" indent="0" leading="40" variableName="name2">
        <size>
          <Rectangle left="-40" right="4039" top="-40" bottom="552"/>
        </size>
        <color>
          <Color red="255" green="255" blue="255" alpha="255"/>
        </color>
      </DefineEditText>
      <PlaceObject2 replace="0" depth="3" objectID="4">
        <transform>
          <Transform transX="400" transY="1943"/>
        </transform>
      </PlaceObject2>
      <DefineEditText objectID="5" wordWrap="1" multiLine="1" password="0" readOnly="1" autoSize="0" hasLayout="1" notSelectable="0" hasBorder="0" isHTML="0" useOutlines="0" fontRef="1" fontHeight="360" align="0" leftMargin="0" rightMargin="0" indent="0" leading="40" variableName="name3">
        <size>
          <Rectangle left="-40" right="4039" top="-40" bottom="552"/>
        </size>
        <color>
          <Color red="255" green="255" blue="255" alpha="255"/>
        </color>
      </DefineEditText>
      <PlaceObject2 replace="0" depth="4" objectID="5">
        <transform>
          <Transform transX="400" transY="2783"/>
        </transform>
      </PlaceObject2>
      <DefineEditText objectID="6" wordWrap="1" multiLine="1" password="0" readOnly="1" autoSize="0" hasLayout="1" notSelectable="0" hasBorder="0" isHTML="0" useOutlines="0" fontRef="1" fontHeight="360" align="0" leftMargin="0" rightMargin="0" indent="0" leading="40" variableName="name4">
        <size>
          <Rectangle left="-40" right="4039" top="-40" bottom="552"/>
        </size>
        <color>
          <Color red="255" green="255" blue="255" alpha="255"/>
        </color>
      </DefineEditText>
      <PlaceObject2 replace="0" depth="5" objectID="6">
        <transform>
          <Transform transX="400" transY="3643"/>
        </transform>
      </PlaceObject2>
      <DefineEditText objectID="7" wordWrap="1" multiLine="1" password="0" readOnly="1" autoSize="0" hasLayout="1" notSelectable="0" hasBorder="0" isHTML="0" useOutlines="0" fontRef="1" fontHeight="200" align="0" leftMargin="0" rightMargin="0" indent="0" leading="40" variableName="" initialText="NEXT">
        <size>
          <Rectangle left="-40" right="580" top="-40" bottom="326"/>
        </size>
        <color>
          <Color red="255" green="255" blue="255" alpha="255"/>
        </color>
      </DefineEditText>
      <DefineButton2 objectID="8" menu="0" buttonsSize="12">
        <buttons>
          <Button hitTest="1" down="1" over="1" up="1" objectID="7" depth="1">
            <transform>
              <Transform transX="60" transY="-126"/>
            </transform>
            <colorTransform>
              <ColorTransform2/>
            </colorTransform>
          </Button>
          <Button hitTest="0" down="0" over="0" up="0"/>
        </buttons>
        <conditions>
          <Condition next="0" menuEnter="0" pointerReleaseOutside="0" pointerDragEnter="0" pointerDragLeave="0" pointerReleaseInside="0" pointerPush="1" pointerLeave="0" pointerEnter="0" key="0" menuLeave="0">
            <actions>
              <PushData>
                <items>
                  <StackString value="page"/>
                </items>
              </PushData>
              <PushData>
                <items>
                  <StackString value="page"/>
                </items>
              </PushData>
              <GetVariable/>
              <PushData>
                <items>
                  <StackString value="1"/>
                </items>
              </PushData>
              <AddCast/>
              <SetVariable/>
              <PushData>
                <items>
                  <StackString value="http://localhost:3000/swfgen/?page="/>
                </items>
              </PushData>
              <PushData>
                <items>
                  <StackString value="page"/>
                </items>
              </PushData>
              <GetVariable/>
              <ConcatenateString/>
              <PushData>
                <items>
                  <StackString value="/"/>
                </items>
              </PushData>
              <GetURL2 method="64"/>
              <EndAction/>
            </actions>
          </Condition>
        </conditions>
      </DefineButton2>
      <PlaceObject2 replace="0" depth="6" objectID="8">
        <transform>
          <Transform transX="1919" transY="4743"/>
        </transform>
      </PlaceObject2>
      <ShowFrame/>
      <End/>
    </tags>
  </Header>
</swf>
Rails 側の処理

 特別難しいことはしないが,swfmill を使うために fork と exec を利用している.また,要求ごとに一時ファイルを書き出すような設計になっている(この記事の方法を使うことによって,改善できる).複数の要求に対する策として,一時ファイルのファイル名にはセッション ID を利用している.
以下にコントローラのソースを示す.

class SwfgenController < ApplicationController

  def index
    page = params[:page].to_i
    offset = page * 5
    books = Book.find(:all, :offset => offset, :limit => 5)
    
    # Reading template.
    xml = ""
    File.open("#{RAILS_ROOT}/tmp/template.xml"){|f| 
      xml = f.read
    }
    
    # Rewriting xml.
    books.each_with_index do |book, i|
      xml.sub!(/name#{i}_value/, book.name)
    end
    xml.sub!(/page_value/, page.to_s) # Seting page number.
    
    # Providing swf binary.
    send_data(xml2swf(xml),
      :filename => "list.swf",
      :type => "swf"
    )
    
  end

private

  # This method converts xml to swf using "swfmill" (http://swfmill.org/).
  # And returns the binary as string.
  def xml2swf(xml)
    # Setting paths of temporary files.
    xml_path = "#{RAILS_ROOT}/tmp/#{session.session_id}.xml"
    swf_path = "#{RAILS_ROOT}/tmp/#{session.session_id}.swf"

    # Writing temporary file.
    File.open("#{xml_path}","w"){|f|
      f.puts xml
    }
    # Generating swf file.
    child_pid = fork{
      exec("swfmill -e cp932 xml2swf #{xml_path} #{swf_path}")
    }
    exitpid, status = *Process.waitpid2(child_pid)

    bin = File.open(swf_path){|f| f.read}

    # Clean up temporary files.
    File.unlink(xml_path, swf_path)

    return bin
  end

end

実行例

左から順

以上でおおざっぱな説明を終わる.書籍の名前リストのみだと loadValiables との違いが分かりづらいが,画像などを表示してみると違いがよくわかると思う.画像を置き換える場合には,置き換え時に画像のバイナリを Base64 エンコードする必要があるので注意する.しかもバイナリの先頭に特殊な文字列を付加してからエンコードする必要がある*6

*1:FlashLite 1.1 特有の制限 : http://www.soi.wide.ad.jp/class/20080048/slides/08/55.html

*2:手間をかけずにケータイサイトをFlash化――DB連携も可能な「ケータイサーチビューアー」とは : http://plusd.itmedia.co.jp/mobile/articles/0807/18/news116.html

*3:swfmill を使った携帯サイト作成 : http://www.sj6.org/flashlite_by_swfmill_install/

*4:swfmill : http://swfmill.org/

*5:swfmill で Flash Lite 1.x を使う為のパッチ : http://dsas.blog.klab.org/archives/51174693.html

*6:「swfmillでケータイFlashを動的生成してみよう(画像置換編)」 : http://www.plusmb.jp/2008/12/19/1775.html