2009/06/04追記: こちらのサンプルコードは、その後のバージョンアップにより動作しないものが含まれています。大きな変更点としては「テンプレート」と言葉の用法を変更し、これまで「テンプレート」と呼んでいたものを「パーツ」と呼ぶようにしています。詳細は配布ファイルに含まれるテストコードもしくは別エントリ(そのうち書くかも)をご参照ください。
先日こちらで紹介した SwfmillRuby をバージョンアップして、いろいろ便利機能を追加してみました。機能追加以前に、SWF 内のタグの対応数が絶対的に少ないのですが、基本的に SwfmillRuby が対象にしているのは FlashLite 1.1 でパブリッシュされた SWF なので、この用途に限れば、そこそこ使えるものになってきたと思います。ライセンスに変更はありません(GPL2)。無保証です。
今回追加された機能は、大きく次の3点です。
- ムービークリップの検出と入れ替えに対応
- 入れ替えのための事前処理として、ムービークリップをテンプレート化しておけるようにした
- XML Parser を rexml から libxml2 に変更
上記のほか、SwfmillUtil::Swf 初期化の方法が変更されていたり、SwfmillUtil::DefineSprite というクラスが追加されていたりといった、こまごまとした修正が入っています。が、できるだけ外側からは、xml 使って云々とか、SWF のタグがほげほげということは意識しなくても良いように作っているつもりです(といっても、まだまだうまい書き方できそうなところは多いですけど)。
では、使い方は、アーカイブ中の sample を見ていただくとして、以下、それぞれ、日本語で解説していきます。
ムービークリップの検出と入れ替えに対応
Swf#movieclips により、SWF 内のムービークリップのリストが取得できるようになりました。SWF 内部で採番されているID => SwfmillUtil::DefineSprite のハッシュの配列が返ります。
これを使って、以下のような形で片方の SWF のムービークリップを、もう片方の SWF のムービークリップと差し替える、というようなことができます。
require '../lib/swfmill_util'
require 'pp'
################################################################################
# test to replace movieclip
# initialize
swf = SwfmillUtil::Swf.parseSwf(File.open("data/sample_original.swf").read)
swf2 = SwfmillUtil::Swf.parseSwf(File.open("data/sample_original2.swf").read)
# check included movieclips (object_id => SwfmillUtil::Swf::DefineSprite)
pp swf.movieclips.keys #=> ["8", "5"]
pp swf2.movieclips.keys #=> ["6", "3"]
# check included movieclip_ids by instance_name
pp swf.movieclip_ids_named("animation") #=> ["5"]
pp swf2.movieclip_ids_named("animation") #=> ["6"]
# replace movieclip
swf.movieclips["5"] = swf2.movieclips["6"]
# write swf replaced movieclip
swf.write("data/replaced_mc.swf")
上の例では、まず、Swf#movieclips により、ふたつの SWF に含まれるムービークリップの ID 体系を確認しています。ムービークリップの ID を確認する際は、実用上は、そのうしろの行にあるような、Swf#movieclip_ids_named が便利かもしれません。これにより、ステージ上に置かれる際に付与されたインスタンス名を指定し、対象のムービークリップの ID が確認できます。
実際の入れ替えは、上のように、ハッシュを操作するような形式でおこなえます。ムービークリップは、内部で各種形状(シェイプ)や画像(ビットマップ)、テキスト情報などを参照していますが、入れ替え自体は、上のようなハッシュへの代入操作一発で、参照関係にあるリソースを丸ごと入れ替える事ができます。また、このとき、単純に上書きしてしまうと、内部で使用している ID が重複してしまうなど、ID 体系が狂ってしまうことになるので、Swf#write (のなかで呼ばれる Swf#regenerate) したタイミングで、よしなに ID の体系を調整(adjust)する処理が走るようになっています。
入れ替えのための事前処理として、ムービークリップをテンプレートパーツ化しておけるようにした
これは、FlashLite コンテンツの動的生成サイトを作ろうとした際、実用上必要になる事が多いので、ちょっと強引に作ってみた機能です。実際に、前節のようにムービークリップの入れ替えをおこなうと、内部的には SWF を XML に戻し、XML を書き換えて、XML を SWF に戻す、ということをやることになるため、Swfmill プロセスの起動コストがかさんでしまいます。また、ムービークリップの入れ替えでは、関連するリソースも一気に処理対象になるため、前節最後で書いたような、入れ替え後におこなう ID 体系の調整コストも無視できなくなってきてしまいます。
そこで、あらかじめ、入れ替え対象となる(元の) SWF と、そこに差し込むムービークリップをあらかじめ調査しておき、差し込みをおこなうムービークリップをテンプレートパーツ化しておくことを考えてみました。
以下は、対象の SWF を調査し、片方の SWF に含まれるムービークリップをテンプレートパーツとして保存しておく際のコードサンプルです。
require '../lib/swfmill_util'
require 'pp'
################################################################################
# test to templatize movieclip
# initialize
swf = SwfmillUtil::Swf.parseSwf(File.open("data/sample_original.swf").read)
swf2 = SwfmillUtil::Swf.parseSwf(File.open("data/sample_original2.swf").read)
# check included movieclips (object_id => SwfmillUtil::Swf::DefineSprite)
pp swf.movieclips.keys #=> ["8", "5"]
pp swf2.movieclips.keys #=> ["6", "3"]
# check included movieclip_ids by instance_name
pp swf.movieclip_ids_named("animation") #=> ["5"]
pp swf2.movieclip_ids_named("animation") #=> ["6"]
# templatize movieclip specifying the mapping of object_ids
# and available, unused object_id (if you want to adjust object_ids)
File.open("data/animation_template.xml", "w") do |f|
f.puts swf2.movieclips["6"].templatize(true, 6, 5, 1000)
end
上の通り、DefineSprite#templatize は 4 つの引数をとります。これらは手前から:
- あらかじめ ID の調整をおこなうか(true/false)
- 対象のムービークリップに付与されている ID
- 対象のムービークリップを、SWF に入れ込む際の ID
- テンプレート化の際におこなう ID の再編成時に使用できる、未使用の ID の最小値
を表しています。真ん中のふたつは、上記の前半で得られるような、SWF の調査結果をもとに指定します。最後の引数は、明らかに重複の無い大きめの値をバッファを持って指定しておくと良いと思います。なお、引数を何も指定しなければ、テンプレート化の際に ID の調整はおこないませんので、regenerate の際に調整する必要が出てきます。
なお、ここでは、テンプレート化されたムービークリップは XML 化してファイルで永続化していますが、DB に突っ込んだり、容量が許すなら memcache などのキャッシュに載せておいたりという手を使っても良いと思います。
こんなかんじで作成したテンプレートを、以下のようなコードで差し込みます。ここでは、あらかじめ Swfmill#swf2xml した xml から初期化することで、元の SWF を読み込む際に Swfmill 起動が必要ないようにしています。
require '../lib/swfmill_util'
require 'pp'
#################################################################################
# test to regenerate swf using template movieclip
# initialize original swf by preserved xml generated by Swfmill::swf2xml.
# avoid analysing swf's structure using "template_mode".
swf = SwfmillUtil::Swf.parseXml(File.open("data/sample_template.xml").read, true)
# initialize template movieclip by preserved xml generated by Swf#templatize
# avoid analysing swf's structure using "template_mode".
animation = SwfmillUtil::DefineSprite.parseXml(File.open("data/animation_template.xml").read, true)
# check a target object_id
pp swf.movieclip_ids_named("animation") #=> ["5"]
# change movieclip
swf.movieclips["5"] = animation
# write swf changed movieclip.
# avoid adjusting object_id using "adjustment=false"
swf.write("data/regenerated.swf", false)
初期化時の最後の引数は、テンプレートモード (template_mode) を表しています。テンプレートモードで Swf を初期化すると、初期化時に SWF の構成チェック処理がおこなわれません(つまり、Swf#images や Swf#movieclips で、現在の構成をダンプすることができません)。これにより、初期化の際の処理ステップを、ある程度スキップする事ができます。テンプレートモードは、templatize により、テンプレートがあらかじめ作ってある&元の SWF が調査済みで、入れ替えをおこなう相手もわかっているというときに限り、使用される事を想定しています。
また、Swf#write の最後の引数で指定している論理値は、入れ替え時の ID 調整(adjustment)要否を表しています。templatize の際に adjustment=true とし、入れ替え後の ID 調整を先におこなってあれば、ここで adjustment=false とすることで、さらに Swf 生成前のコストを削減する事ができます。
このあたり、冒頭にも書きましたが、ちょっと強引な仕上がりになってますので、ぱっと見た感じ、何が起こるのかわかりにくいのが難点です。もっと使いやすいインタフェースを考えてますが、今のところはこんな感じでご容赦ください。
XML Parser を rexml から libxml2 に変更
オモテからはほとんど意識する必要のない変更点なのですが、これにより今回追加された機能の処理部分のパフォーマンスが劇的(概ね1桁以上、場合によっては2桁)に上がりました。libxml-ruby は Ruby の標準添付ではないので、別途インストールが必要になってしまうのが懸念点だったのですが、実用上こちらの方が望ましいと思いましたので、思い切って全体を書き換えることにしたのでした。
この乗り換えのきっかけは、このライブラリは、内部では XML 処理がごりごりおこなわれているのですが、rexml を使っていたときは、とくにムービークリップ周りの解析や、ムービークリップ入れ替えの際におこなわれる XPath 検索にかなりのコストがかかっていたことがわかった、ということでした。たとえば、50KB のムービークリップが 30 件くらい入った SWF を解析するのに、デスクトップ環境で5-6分かかり、テンプレートを使用したムービークリップの入れ替えに3-4秒かかるといった具合でした。(そもそもの書き方がアレだった部分は重々ありますが。)
解析処理をおこなう際の変数スコープを調整したり、メソッド化してあったところをブロック付きの Hash に書き換えたりすることでソコソコ改善はしたのですが、結局は rexml の XPath 検索が一番のボトルネックになっていたようで、libxml2 利用に置き換えたのが一番効果的だったようです。
libxml2 の利用により、これまで2-3分かかっていた解析処理は3-4秒、入れ替え処理も0.2-0.3秒程度まで短縮できました。同時大量アクセスのあるサイトには厳しいかもしれませんが、他の面でもいろいろ工夫をいれる余地のある環境/状況でしたら、そこそこ実用に耐え得るのかな、と思います。
以上、今回の大きな変更点を紹介しました。まだまだ不具合等内在しているかもですが、ご利用いただけましたら感想など聞かせていただけるとうれしいです。
最近のコメント