swfmill で setProperty がうまく変換できない件

 現行バージョンの swfmill (version 0.2.12) は, swf の setProperty 部分をうまく XML に変換できない.このバグを修正したプログラムが swfmill の作者によって公開されている.

 公式ページには修正についての記述がない.修正済みのプログラムは以下からダウンロードできる.
http://www.mail-archive.com/swfmill@osflash.org/msg00864.html

 文字コードパッチを当てて make しなおしです…(TwT;)

ruby で外部プログラムを利用する.

 ruby から外部プログラムを利用する方法として,IO クラスの popen メソッドを使うか,Open3 ライブラリを使う方法があるようだ.後者は入出力ストリームを指定できる.

 これらを利用すると,前回の記事の swf2xml と xml2swf は以下のように書き直せる.

  # _target_ :: Path of swf template.
  # Return value :: String of xml.
  def swf2xml(target)
    return IO.popen("swfmill -e cp932 swf2xml #{target} stdout") {|io| io.read }
  end

  require "open3"
  # @xml is string of xml.
  # Return value :: Binary of swf.
  def xml2swf
    bin = ""
    Open3.popen3("swfmill -e cp932 xml2swf stdin stdout"){|stdin, stdout|
      stdin.puts @xml
      stdin.close
      bin = stdout.read
    }
    return bin
  end

これで要求ごとに一時ファイルを書き出すようなめんどくさいことをせずに,外部プログラムの出力結果を取得できる.勉強になった.

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

TextMate で任意の文字列を自動ハイライトする設定

12月になり,卒業論文を書き始めました.というわけで TextMate で論文 (Tex) を書くときに便利かもしれない tipsです.昔 Matetips にあったのですが,今見れなくなっているので書いてみます.

(推敲前なので文章が微妙ですが)
任意の文字列(、。全角スペースなど)を自動でハイライトできます.句読点を「,」「.」に統一したかったり,全角スペースが入って欲しくない場合に有効です.
僕は句読点の標準設定が「,」「.」なのでいいのですが,Textmate は縦長フォントの関係上全角スペースが紛れ込みやすいので重宝しています.

設定は以下のようにします.
Bundle Editor で Edit Languages を選び,Tex のバンドルにて Patterns に以下を追加します.

{ match = '( )';
captures = { 1 = { name = 'meta.character.DBSpace'; }; };
},
{ match = '(、|。)';
captures = { 1 = { name = 'meta.character.ZKkuto'; }; };
},

それが終わったら,つぎに,Preferences の Fonts&Colors の Element を以下の画像のように追加します(scope selector の設定を忘れない事).

ここで設定した色とフォントが,上記のパターンにマッチする文字列に適用されます.
ここでは句読点と全角スペースのハイライト色を分けたかったので個別に表記していますが,同じでいいなら

{ match = '(、|。| |)';
captures = { 1 = { name = 'meta.character.Thesis'; }; };
},

といった感じでよいでしょう.

条件式の注意点

基本だけど忘れ易いことを思い出したのでメモする.

  • (a OR b) と (b OR a) の違いについて.

基本的に条件式は左の要素から評価される.例えば (a OR b) では,a が偽であるとき初めて b が評価される.
だから,"a が NULL であるか,そうでなければ b が 0 のとき真" になる条件式は ( a == NULL OR b == 0 ) になるが,これを ( b == 0 OR a == NULL ) と書いてはいけない(場合がある).後者は "b が 0 であるか,そうでなければ NULL のとき真"になる条件式であって,前者とは少し意味が変わる.たとえば a が int のポインタだったとき,b を a が示す値 (*a) とすると ( a == NULL OR *a == 0 ) は正しいが,もし ( *a == 0 OR a == NULL ) と書いてしまったら Bus error になる.
当たり前ですが忘れ易いのでメモしておきました.以下のコードで検証できます.

#include <stdio.h>

int main (void){
  int *a;
  a = NULL;
  if( a == NULL || *a == 0 ){
  // if( *a == 0 || a == NULL ){ // Cause of bus error.
    printf("a is NULL or *a is zero.\n");
  }else{
    printf("a is %p and *a is %d\n", a, *a);
  }
  return 0;
}

2008.12.3 id:invent に星もらった.ありがたい.

Lost connection to MySQL server during query のエラー

ActiveRecord を使ってバッチ処理や大量のクエリを飛ばしたりなどしていると,"Lost connection to MySQL server during query" というエラーが頻繁に出るので困っていた.ローカルの DB でもリモートの DB でも現象は同じように発生するので,今回の場合は rails にセットでついてくる mysql のアダプタが原因だ.なので,いろいろ調べた結果,gem でアダプタをインストールすることにしたが,パッケージが何かおかしいらしくちゃんとインストールされない.mysqldmg パッケージでインストールしたが,ライブラリは macports でインストールしたものを使うことにして,

sudo gem install mysql -- --with-mysql-lib=/opt/local/lib/mysql5/mysql --with-mysql-include=/opt/local/include/mysql5/mysql

としてから,

# cd /usr/local/mysql/lib
# mkdir mysql
# cd mysql
# ln -s /usr/local/mysql/lib/lib* .

という感じでシンボリックリンクをはるとちゃんと動いた.Lost connection も出なくて快適.

参考URI
http://d.hatena.ne.jp/urekat/20071002/1191304499

OpenTerminalHere を使っていたら,terminal.app の振る舞いがおかしくなった.
なぜか新規シェル呼び出し時に cd コマンドがかかって,カレントディレクトリをヘンなディレクトリにしてしまうのだ.

一生懸命調べた結果,

~/Library/Preferences/com.apple.Terminal.plist

の中にある
ExecutionString の value が 'cd /hogehoge/fugafuga' みたいになっていた.この value を空白にすると現象はなおったが,なぜこの値が書き変わったのか全く分からない.

.bash_profile や .bashrc にも cd の記述が無いので,terminal の状態 XML を保存してみて,中に ExecutionString の項目と,その value が cd になっているのを発見したから plist のせいだと分かったのだけれど,そこにたどり着く迄にすごく苦労したぞ.

いったん plist のせいだと分かったので検索してみると,やはりこの問題に悩んだ人々がいた.

http://slashdot.jp/~gigo/journal/217310
http://forums.macosxhints.com/archive/index.php/t-22744.html
http://d.hatena.ne.jp/cyk0/20080808
http://no-item-retrieved.blogspot.com/2007/12/tex-tools-for-mi.html

メニューバーの「ファイル>設定をデフォルトとして使用」を選んでしまうとこういうことが起きるらしい。

なるほど!そういうことかぁ.

収穫は, .bashrc 読み込み後に何かさせたいときは plist の ExecutionString に書くと良いということ.そしてそれは,terminal の「設定をデフォルトとして使用」を選ぶことで書き変わるということだった.