2011/06/12

Android端末のスクリーンショット保存を補助するスクリプト

今回は、過去に紹介したいくつかの機能を組み合わせて、Android端末のスクリーンショット撮影を補助するツールを作成する例を紹介します。
Android端末のスクリーンショットを保存する方法としては、Android SDKに同梱のDDMSというプログラムを使用するのが代表的ですが、1枚の画像を保存するために「更新ボタンを押す→保存ボタンを押す→保存ファイル名を指定」という手間が必要で、あまり使い勝手がよくありません。
そこで、特定のキーを押すだけで最新の画面を連番ファイル名で保存出来るようにします。
var Clipboard=require('Clipboard').Clipboard;
var dir=cwd().directory("E:\\docs\\work\\NR_android\\ss");
var prefix="ss_", len=3, num=1;

Main.createNotifyIcon();
Main.process.priority=Process.priority.high;

require('Hotstrokes').Hotstrokes.defineConditions({
    DDMS:function()(this.activeWindow.title=='Device Screen Capture'),
}).map('DDMS',{
    "NumEnter":function(){
        Thread.create(function(){
            var fmt=prompt('change prefix / number'), m;
            if(fmt.match(/^\d+$/)){
                num=fmt*1;
            }else if(m=fmt.match(/^(.*?)(\d+)$/)){
                prefix=m[1];
                len=m[2].length;
                num=m[2]*1;
            }else{
                prefix=fmt;
            }
            Main.notifyIcon.showInfo('Next: '+dir.file(prefix+num.format(len)+'.png'));
        });
    },
    "NumMinus":function(){
        num--;
        Main.notifyIcon.showInfo('Next: '+dir.file(prefix+num.format(len)+'.png'));
    },
    "NumPlus":function(){
        num++;
        Main.notifyIcon.showInfo('Next: '+dir.file(prefix+num.format(len)+'.png'));
    },
    "Num0":function(){
        Thread.create(function(w){
            w.control('Button',0).lbuttonClick();
            sleep(800);
            w.control('Button',3).lbuttonClick();
            sleep(500);
            var file=dir.file(prefix+num.format(len)+'.png');
            try{
                var img=Clipboard.image;
                img.save(file);
            }finally{
                free(img);
            }
            Main.notifyIcon.showInfo('Saved: '+file);
            num++;
        },this.activeWindow);
    },
}).register();
キーボード操作への割り当てにはHotstrokesを使用します。端末の操作をしながら撮影操作を行なうため、押しやすいテンキーの0に割り当てることにします。
DDMSのキャプチャウィンドウがアクティブな時だけ有効になるように、ウィンドウタイトルで判定する条件を定義して、その条件下への割り当てを行ないます。

画像の保存は、ウィンドウ上のボタンの自動クリックにより画面の更新とクリップボードへのコピーを実行させた後、Clipboardユニットの画像取得機能で取得した画像を保存することで実現しています。
ボタンを押してからDDMS側で処理が完了するまでにはタイムラグがあるので、適当にsleep()を入れて待機します。
一連の処理には時間がかかるため、キー操作をブロックしないように新規スレッド上で行なうようにしています。

Clipboard.imageで取得したImageオブジェクトはfree()で解放しなければならないことに注意が必要です。
(Clipboardユニットの開発時点では、取得の度にコピーを生成しなければならないケースを想定していなかったため、このような仕様になっています)

その他、プレフィクスや番号をプロンプトで指定する機能や、間違えて撮影したときのために連番を巻き戻す機能を用意しました。
保存先ディレクトリは、頻繁に変更するものではないので、スクリプト上にハードコーディングしています。

簡単なスクリプトですが、通常のスクリーンショット保存手順に比べるとかなり手間が軽減できるはずです。

2011/05/31

ZIPファイル内のファイルを解凍したり直接読み込む

NILScriptのZLibユニットに用意されているZIPクラスを利用すれば、ZIPファイルの圧縮や解凍を行なえます。

ZIPファイル内の全てのファイルを指定のディレクトリに解凍するには、以下のようにZIPクラスのextract()メソッドを使用します。
require('ZLib').ZIP.extract(zipfile, directory);

extractText()やextractBytes()メソッドでは、ZIPファイル内のファイルをローカルファイルに保存することなく直接変数に読み込むことが出来ます。
extractText()は文字列、extractBytes()はポインタオブジェクトを返します。
以下の例では、ZIPファイル内の「changelog.txt」の内容を表示します。
巨大なZIPファイル内の小さなファイルだけを利用したい場合などには、特に効率化が期待できます。
println(require('ZLib').ZIP.extractText('F:\\downloads\\nil.zip','changelog.txt'));


ZIPクラスのインスタンスオブジェクトを作成すれば、更新日時や拡張子などの条件に一致するファイルだけを解凍したり、ファイルの解凍や読み込みは行なわずにサイズや更新日時などの情報を取得して利用するなど、様々な処理を行なえます。
以下の例では、ZIPファイル内のファイルとローカルファイルを比較し、ZIPファイルにだけ存在するファイルやZIPファイル内の方が新しいファイルを列挙します。
var dir=Main.scriptDirectory;
var file="F:\\downloads\\nil.zip";
try{
    var zip=require('ZLib').ZIP.open(file);
    for(let f in zip.descendants){
        var lf=dir.file(f.path);
        if(f.isDirectory){
        }else if(!lf.exists){
            println('[new] '+f.path);
        }else if(f.time>lf.mtime){
            println('[updated] '+f.path);
        }
    }
}finally{
    free(zip);
}
ZIPオブジェクトのdescendantsプロパティで、ZIPファイル内の全てのファイルがZIPFile/ZIPDirectoryオブジェクトとして列挙できます。
ZIPDirectoryオブジェクトを判別するために、isDirectoryプロパティが用意されているので、これを利用して除外します。
「Main.scriptDirectory」などのDirectoryオブジェクトのfile()メソッドの引数にZIPFileオブジェクトのpathプロパティを指定すれば、ZIP内のパスに対応するローカルファイルのFileオブジェクトを得られます。
ZIPFileのtimeプロパティでは、更新日時をDateオブジェクトとして得られます。これをFileオブジェクトのmtimeプロパティと比較すれば、ファイルが新しいかどうかが分かります。

その他、ZIPFile/ZIPDirectoryオブジェクトには、指定のローカルファイルとして解凍するcopy()メソッドなどが用意されています。ZIP関連の機能の詳細は、NILScriptに同梱のdoc\ZLib.txtを参照して下さい。

2011/05/29

ファイルをZIP圧縮する

ZIPなどのアーカイブファイル形式は、複数のファイル群や巨大な圧縮されていないファイルをネット経由で公開・送受信したい場合に役立ちます。
今回は、NILScriptでZIPファイルを作成する方法について説明します。

ZIPファイルの読み書きを行なうには、ZLibユニットのZIPクラスを使用します。
単に一つのディレクトリ配下のファイルをZIPファイルにするだけなら、以下のようにZIPクラスのcompress()メソッドをZIPファイルとディレクトリのパスを引数にして呼び出すだけです。
require('ZLib').ZIP.compress(zipfile, directory);

以下の例では、正規表現に一致するファイルを除外して、残りのファイルだけを圧縮します。
var exclude=/\\nil\\(release|test\d*)\.ng|\\nil\\users|\\users\\Owner/;
var src=Main.scriptDirectory;
var dest="E:\\www\\public_html\\nil.zip";

try{
    var zip=require('ZLib').ZIP.create(dest);
    (function(from,to){
        for(var f in from.children){
            if(!String(f).match(exclude)){
                println(f.path);
                if(f instanceof Directory){
                    arguments.callee(f, to.directory(f.name).create(f.mtime));
                }else{
                    to.add(f);
                }
            }
        }
    })(src,zip);
}finally{
    free(zip);
}
まず、ZIPファイルのパスを引数にしてZIP.create()を呼び出し、ZIPオブジェクトを取得します。
次に、Directoryオブジェクトのchildrenプロパティで圧縮対象ディレクトリ内のファイル/ディレクトリを列挙し、条件に一致するファイルをZIPファイルに追加していきます。

ZIPファイルの直下にファイルを追加するには、追加したいファイルを表すFileオブジェクトを引数として、ZIPオブジェクトのadd()メソッドを呼び出します。

Directoryオブジェクトを引数にadd()メソッドでを呼び出せば、配下のファイルを全て追加できますが、今回はサブディレクトリ内もフィルタリングしたいので、ZIPファイル内にディレクトリを作成したあと、再帰呼び出しで配下のファイルの処理を行なうことにします。

ZIPファイル内にディレクトリを作成するには、ZIPオブジェクトのdirectory()メソッドでZIPDirectoryオブジェクトを取得し、そのcreate()メソッドを呼び出します。create()メソッドの返り値は、そのZIPDirectoryオブジェクト自身です。
create()メソッドの引数には、格納するディレクトリの更新日時を表すDateオブジェクトを指定できます。

ZIPDirectoryオブジェクトも、ZIPオブジェクトと同様に、add()メソッドでディレクトリ内のファイルとしてファイルを追加したり、directory()メソッドで子ディレクトリを表すZIPDirectoryオブジェクトを取得できるので、共通の関数で再帰処理を行なえます。

処理が完了したら、free()関数でZIPオブジェクトを解放する必要があることに注意して下さい。

この他のZIPファイル読み書き機能については、NILScriptに同梱のdoc\ZLib.txtを参照して下さい。

2011/05/11

生成したプロセスの標準入出力を別のプロセスにリダイレクト

NILScriptでは、「Process.create()」やそのエイリアスである「run()」で外部プロセスを起動できます。
このメソッドでは、第2引数にオブジェクトとして様々なオプションを指定する事で、標準入出力の扱いなどを柔軟に設定出来ます。
例えば、「stdin」オプションにあらかじめProcess.create()で生成しておいた別のProcessオブジェクトを指定すると、そのプロセスの標準出力を次のプロセスの標準入力にリダイレクト出来ます。
コマンドプロンプトで「|」で複数のコマンドを繋いだときのように、出力と入力を直接繋ぐので、スクリプト側で読み出しと書き込みを行うよりも効率的に動作します。
他にも、ファイルやNULデバイスへのリダイレクトも可能です。これらの機能の詳細は、NILScriptに同梱のdoc\base_task.txtを参照して下さい。

以下は、この機能の利用例です。
//プログラムのパスと保存先を指定
var rtmpdump='D:\\bin\\rtmpdump-2.1d\\rtmpdump.exe';
var ffmpeg='D:\\bin\\ffmpeg.exe';
var dir='F:\\downloads';

var url=prompt('Enter MySpace URL(artist or song)','http://www.myspace.com/astralmess');
var swf='http://lads.myspacecdn.com/videos/MSMusicPlayer.swf';
var http=new (require('HTTP').HTTP)();

var downloadSong=function(sid){
    var xml=http.getText({
        url:'http://www.myspace.com/music/services/player?songId='+sid
          +'&ptype=4&action=getSong&el='+encodeURIComponent(url)+'&sample=0',
        referer:swf,
    });
    var title=xml.match(/<title>([^>]*)<\/title>/)[1].unescapeHTML();
    var rtmp=xml.match(/<rtmp>([^>]*)<\/rtmp>/)[1].unescapeHTML();
    println('Downloading: '+title);
    var p1=Process.create('"'+rtmpdump+'" -r '+rtmp+' -W '+swf+' -o -',{
        show:0
    });
    var p2=Process.create('"'+ffmpeg+'" -i - -acodec copy -metadata title="'
                    +title+'" "'+dir+'\\'+title.format('asfE')+'.mp3"',{
                        stdin:p1,
                        stdout:"",
                    });
    p2.wait('exit');
};

var m;
if(m=url.match(/^http:\/\/www\.myspace\.com\/[^\/]*\/music\/songs\/.*\-(\d+)$/i)){
    downloadSong(m[1]);
}else if(m=url.match(/^http:\/\/www\.myspace\.com\/[^\/]*\/?$/)){
    var html=http.getText(url);
    m=html.match(/<param[^>]*value="[^>"]*MSMusicPlayer\.swf"[^>]*>[\s\S]*?<param name="flashvars" value="([^">]*)"/)
    var param={};
    for(let pair in $G(m[1].split(/&/g))){
        var [,name,value]=pair.match(/^(.*?)=(.*)$/);
        param[name]=value;
    }
    http.getText({
        url:'http://www.myspace.com/music/services/player?artistUserId='
            +param.profid+'&playlistId='+param.plid
            +'&action=getArtistPlaylist&artistId='+param.artid,
        referer:swf,
    }).grep(/<song\s(?=[^>]*playType="FullSong")[^>]*songId="(\d+)"/g).map("1").execute(downloadSong);
}
音楽系SNS「MySpace」の指定したURLのページからプレイリストを抽出し、ストリーミングURLを「rtmpdump」でダウンロードし、「ffmpeg」でFLVからMP3を抽出して保存するというスクリプトですが、rtmpdumpの出力をffmpegにリダイレクトすることで、一時ファイルを作らずに処理を完了しています。
太字になっている部分が、ffmpegの標準入出力を指定している部分です。
「stdout:""」は、NULへのリダイレクトを示します。このようにして進捗などの出力を読み捨てないと、出力バッファが一杯になって処理が止まってしまう可能性があるので、注意が必要です。

2011/04/28

種文字列から一定の乱数列を生成する

NILScriptのCipherユニットには、種文字列を指定して乱数列を得るRandomクラスが用意されています。
通常のMath.random()と違うのは、同じ種文字列を指定すると常に同じ乱数列が生成されるという点です。
この機能を利用すれば、ゲームのステージ生成などで、生成されたステージを丸ごと保存しなくても、種文字列を保存しておくだけで過去に生成したステージを再現できるようになります。

下記の例は、Cipher::Randomを利用して迷路を生成するというスクリプトです。
コマンドライン引数に横の広さ、縦の広さ、種文字列を指定すると、迷路が生成され表示されます。
種文字列が省略された場合は、String.random()で生成したランダムな文字列が使用されます。
var w=(Main.params[0]||32)*2-1, h=(Main.params[1]||18)*2-1;
var r=new (require('Cipher').Random)(Main.params[2]||String.random(16)); //(1)
var map=[],joints=[];
//周辺の枠の生成
map.push(('-'.times(w-1)+'+').split(''));
for(var x=2,m=w-2;x<m;x+=2){
    joints.push({x:x,y:0});
    joints.push({x:x,y:h-1});
}
for(var y=1;y<(h-1);++y){
    map.push(('|'+' '.times(w-2)+'|').split(''));
    if(!(y&1)){
        joints.push({x:0,y:y});
        joints.push({x:w-1,y:y});
    }
}
map.push(('+'+'-'.times(w-1)).split(''));

//壁の生成
var j;
while(joints.length){
 //新たに生やし始める箇所を選択する
    if(!j){
        j=joints[r.next(joints.length)]; //(3)
    }
    //壁を延ばす方向の候補を列挙
    var c=[];
    if((map[j.y-2]||[])[j.x]==' '){
        c.push({via:{x:j.x  ,y:j.y-1}, next:{x:j.x  ,y:j.y-2}, mark:'|'});
    }
    if((map[j.y+2]||[])[j.x]==' '){
        c.push({via:{x:j.x  ,y:j.y+1}, next:{x:j.x  ,y:j.y+2}, mark:'|'});
    }
    if((map[j.y]||[])[j.x-2]==' '){
        c.push({via:{x:j.x-1,y:j.y  }, next:{x:j.x-2,y:j.y  }, mark:'-'});
    }
    if((map[j.y]||[])[j.x+2]==' '){
        c.push({via:{x:j.x+1,y:j.y  }, next:{x:j.x+2,y:j.y  }, mark:'-'});
    }
    //延ばせる方向があれば、そのうちのどこかに延ばす
    if(c.length){
        var m=c[r.next(c.length)]; //(4)
        map[m.via.y][m.via.x]=map[m.next.y][m.next.x]=m.mark;
        if(map[j.y][j.x]!=m.mark){
            map[j.y][j.x]='+';
        }
        joints.push(j=m.next);
    }else{
        joints.remove(j);
        j=null;
    }
}
map[1][0]=map[h-2][w-1]=' ';
//表示
for(var y=0;y<h;++y){
    var l=[];
    for(var x=0;x<w;++x){
        l.push(map[y][x]);
        if(x&1){
            l.push(map[y][x]);
        }
    }
    println(' '+l.join(''));
}
println('Seed='+r.seed);
free(r); //(2)

最初に「r」変数にRandomオブジェクトを格納します。(1)
Randomオブジェクトは、不要になったら「free()」でリソースを解放する必要があります。(2)

迷路の生成には、既存の壁から壁を生やして、他の壁にぶつからないように延ばしていくという方式を使用しています。
新たに壁を生やし始める箇所を選択する処理(3)と、壁を延ばす方向を選択する処理(4)で乱数を使用しています。
Randomオブジェクトのnext()メソッドは、Number.random()と同じように、指定した範囲の整数を返します。

上記のスクリプトを「20 10 XYZ」を引数にして実行すると、以下のような迷路が表示されるはずです。
 ---------------------------------------------------------+
                                                          |
 |  ---+  +-----------+  +-----+---  +--------+  +--+  +--+
 |     |  |           |  |     |     |        |  |  |  |  |
 |  +--+  |  +-----+  |  |  |  +-----+  +--+  |  |  |  |  |
 |  |     |  |     |     |  |           |  |  |  |  |  |  |
 |  +--+  +--+---  +-----+  +--------+  |  |  +--+  +--+  |
 |     |                             |  |  |           |  |
 |  +--+  +--------+  +-----+-----+--+  |  +--+  +--+  |  |
 |  |     |        |  |     |     |     |     |  |  |     |
 |  +-----+  +-----+  +--+  |  +--+  +--+--+  +--+  +--+  |
 |           |           |  |  |     |     |           |  |
 +--+  ---+  +--------+  |  |  +-----+  +--+  +-----+  |  |
 |  |     |           |  |  |           |     |     |  |  |
 |  +--+  |  +---  +--+  |  |  |  +-----+  +--+  +--+  |  |
 |     |  |  |     |  |     |  |  |        |  |  |     |  |
 |  ---+--+  +-----+  +-----+--+  +---  +--+  |  +-----+  |
 |                                      |
 +--------------------------------------+------------------