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を含め正常にインストールされていることを前提とする.
設計と実装
動作手順は次のようにする.
- クライアントアプリケーションが loadMovie を使ってサーバにファイルを問い合わせる.(このときクエリも付加できる.)
- サーバプログラムが要求を受け取ると,rails アプリはテンプレート用 xml の内容を書き換える.(この テンプレート用 xml は事前に作成しておく)
- rails アプリは書き換えた xml を,swfmill を使って swf に変換する.
- rails アプリは完成した swf ファイルをクライアントアプリケーションに提供する.
実装を行う.ここでは例として,本の一覧を携帯で見るためのシステムを作ってみる.
クライアント用の swf
クライアントはボタンを押したら getURL を呼び出すだけ.適当にボタンを配置して,下のようなアクションを設定するだけで良い.開発環境は Flash CS3 である.
on (KeyPress "<Enter>"){ getURL("http://localhost:3000/swfgen/?page=0"); }
テンプレート用 XML
テンプレート用の XML は,書き換えたい変数名に法則性を持たせるなど,プログラムが書き換え易いように作っておかなければならない.
本記事では以下のようにした.
- テキストフィールドを5つ作り,それぞれに変数名を指定する.
- 1フレーム目の ActionScript で,テキストフィールドそれぞれに指定した変数にダミーの値を代入する.
- ページ番号を保存するための offset という変数を用意して,ダミーの値を代入する.
- 「次へ」ボタンを作り,そのボタンを押すとページ番号を1増やしたクエリを付加して getURL の要求を出す.
プログラム側では,この ActionScript 内のダミーの値を書き換えれば良い.本記事では,テンプレートをFlash CS3 で作成し,それを swfmill で変換して使った.(できる人は XML をガリガリ書いても良いと思う.)
- 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