illustratorでUIデザイン:スクリプトでExtension風パネルを実装

2015/9/5追記 いくつか認識違いがあったので記事を書きなおしました。

f:id:two-hats:20150510065750g:plain
はじめに断っておきますが完全に時代遅れな内容の記事です。CCから実装できるようになったHTML5を使って作るExtensionが主流になると思いますので、これからやろうという方はそちらを調べたほうが良いと思います。「それでもスクリプトが好き!」という酔狂な方のためのニッチな情報になっています。


スクリプトは便利な機能をサクッと作れる分、「ファイル>スクリプト>目的のスクリプト」と操作しなければならなく起動するのが面倒ですよね。Macではスクリプトにショートカットを割り当てられるアプリもありますがWindowsにはありません。どうにかしてスクリプトをワンクリックで起動できないかなーってずっと考えていました。


スクリプトでUIを実装する場合、大抵ウィンドウオブジェクトを使います。ウィンドウオブジェクトは「window、dialog、palette」の3種類ありdialogとpaletteはモーダルかモードレスの違いで、windowは閉じるや最小化ボタンを持った一般的なウィンドウを作成できます。

dialogはモーダルなのでdialog表示中に他の操作はできませんが、paletteやwindowはモードレスなので他の操作もできます。これらを使えばスクリプトのウィンドウを表示したままillustratorでお絵かき出来たり、描いたものを選択してウィンドウ上のボタンをワンクリックして特定の処理を行ったりすることができそうです。夢が広がりますね。

windowだといわゆるタスクバーに表すれるような普通のwindowなのでillustratorのウィンドウの背面に隠れてしまうことが多々あります。paletteはillustratorのウィンドウ上に表示され続けるので、使い勝手的にはpaletteの方がExtensionに近い動作をすると言えます。この記事ではpaletteを使って実装します。

Paletteの実装方法

paletteを表示するコードは下記になります。このコードをExtendscript Toolkit上で入力するとボタン1個だけのパレットウィンドウが表示されます。

#target illustrator

var win = new Window("palette");
win.add("button", undefined, "ボタン");
win.show();

f:id:two-hats:20150902062920p:plain
ただ、このコードを.jsxとしてイラレスクリプトフォルダに置いてイラレから起動した場合は動作しなくなります。
(一瞬ウィンドウっぽいのが表示されるだけ)

これは#targetengineが指定されていないからです。
targetengineを語弊を恐れずに言うと、JSのグローバルオブジェクトのような役割をもったものと認識しています。

Extendscript Toolkitで実行している時には特に記述しなくても自動的に「main」というものが指定されています(実行の三角アイコンの左のボックスに書かれているmainがそれ)。このmainというグローバルオブジェクトのようなモノはイラストレーターを終了するまで存在し続けます。

ですが、プログラムをイラレスクリプトフォルダに移しイラレから実行すると、「transient」という一時的なengineが出来、スクリプトの終了と供に破棄されてしまいます。そのため、targetengineを指定せずにpaletteやwindowを実行しようとすると、スクリプトの終了と供にpaletteやwindowのオブジェクトが維持できなくウィンドウが一瞬で消えてしまいます。

これを解決するにはプログラムに「#targetengine hogehoge」の一文を入れてengineを作っておく必要があります。hogehogeはどんな文字列でもかまいません。但し、Extendscript Toolkit上で編集しているときには「main」以外の文字列でengineを作成することが出来ないので、開発時はtargetengineの指定をコメントアウトしておくと良いと思います。

//#targetengine hogehoge

とりあえず実装してみる

まずは単純にウィンドウに置いたボタン押して、現在選択されているオブジェクトの数を取得するプログラムを作ってみようと思います。
イラレのドキュメント上にあるパスや画像のことを、この記事ではオブジェクトと呼ぶことにします。

#target illustrator
//#targetengine hogehoge
//とりあえずウィンドウからAppのプロパティであるselectionにアクセスしてみる。

var win = new Window("palette");
var getBtn = win.add("button", undefined, "Get the number of selections");
getBtn.onClick = function(){
    alert(app.activeDocument.selection.length); //→動作しない
};
win.center();
win.show();

試してみるとわかりますがこれは動作しません。なぜかwindowやpaletteからappのプロパティでアクセスできないものがあるようです。ちなみにdialogにするとアクセスできますがモーダルなのでExtension風のパネルとは程遠い操作になってしまいます。


BridgeTalkを使ってみる

八方塞がりか・・・と諦めかけましたが、色々と調べてみたところBridgeTalkというものを使えばpaletteやwindowからappのプロパティにアクセスできるようでした。BridgeTalkとはAdobe製品の異なるアプリ間でデータをやりとりしたり、処理を依頼して結果をもらってくることができる仕組みです。スクリプトでできることの幅が広がりそうですね。


BridgeTalkはイラレからPhotoshopを呼び出すなど異なるアプリ間で処理をするというのが一般的ですが、イラレからイラレを呼び出すようにするとpaletteやwindowからappのプロパティにアクセスできるようになります。

BridgeTalkをインスタンス化し、targetプロパティに対象のアプリ名とbodyに実行したい処理を書き、sendで実行します。BridgeTalkはbodyに処理したい内容を文字列にして引き渡しているところが特徴的ですね。

#target illustrator
//#targetengine hogehoge
//BridgeTalkを使ってAppのプロパティにアクセスしてみる。
var win = new Window("palette");
var getBtn = win.add("button", undefined, "Get the number of selections");
getBtn.onClick = function(){
    //BridgeTalk
    var bt = new BridgeTalk();
    bt.target="illustrator";
    bt.body = "alert(app.activeDocument.selection.length);"; //→アラートに選択数を表示可能
    bt.send();
};
win.center();
win.show();

これを実行するとめでたく現在選択されている数をアラートポップで表示させることができました。ウィンドウを消さずに選択する数を変更してボタンを押してみてください。選択されている数がリアルタイムに反映されていますね。


ちょっと処理を変えて選択されているアイテムを動かしてみます。

#target illustrator
//#targetengine hogehoge
//BridgeTalkを使って選択されているアイテムを操作してみる。
var win = new Window("palette");
var moveBtn = win.add("button", undefined, "Move to the right");
moveBtn.onClick = function(){
    //BridgeTalk
    var bt = new BridgeTalk();
    bt.target="illustrator";
    bt.body = "app.activeDocument.selection[0].left += 100;"; //右に100ずつ動く
    bt.send();
};
win.center();
win.show();

f:id:two-hats:20150510065835g:plain
現在選択されているアイテムがボタンを押すごとに右に移動するようになりました。初めこの動きができたとき感動してしまいました。これで「ファイル>スクリプト>目的のスクリプト.jsx」っていう面倒な操作から開放されると・・・。
このようにBridgeTalkを使えばスクリプトでもExtension風の常駐パネルを作成できるようになります。


BridgeTalkの非同期処理を体感する

もう少しBridgeTalkに詳しくなるため色々と試してみましょう。まずは選択されている数を取得し変数に代入してみます。

#target illustrator
//#targetengine hogehoge
//BridgeTalkを使ってAppのプロパティを変数に代入してみる。

var num = 0;

var win = new Window("palette");
var getBtn = win.add("button", undefined, "Get the number of selections");
getBtn.onClick = function(){
    //BridgeTalk
    var bt = new BridgeTalk();
    bt.target="illustrator";
    bt.body = "num = app.activeDocument.selection.length;";
    bt.send();
    
    alert("The number of selections:" + num); //→常に0
};
win.center();
win.show();

これを試してみると正常に動作しません。選択されている数がnumに反映されなく常に0になってしまうのです。これはBridgeTalkが非同期処理だからです。

非同期処理を例えて言うならば「仕事を外注に委託する」ような働きとでもいいましょうか。ある仕事(bt.body部分)を外注にお願いして、自分はさっさと次の仕事(alert("The number 〜)にとりかかるというイメージです。

動きの順番の違いを体感してみましょう。bt.bodyに選択されている数を表示するalertを入れました。見て分かるようにbodyには文字で代入する必要があるため、複数行の処理を行いたい場合は改行をせずにそのまま繋げた文字列にして指定します。

#target illustrator
//#targetengine hogehoge
//BridgeTalkの非同期処理を体感してみる。

var num = 0;

var win = new Window("palette");
var getBtn = win.add("button", undefined, "Get the number of selections");
getBtn.onClick = function(){
    //BridgeTalk
    var bt = new BridgeTalk();
    bt.target="illustrator";
    bt.body = "num = app.activeDocument.selection.length; alert('The num in BridgeTalk:' + num);"; //→後から表示
    bt.send();
    
    alert("The num in Main:" + num); //→先に表示
};
win.center();
win.show();

f:id:two-hats:20150510065906g:plain
実行結果はどうなったでしょうか。Mainにあるalertが先に表示されてからBridgeTalk内のalertが表示されましたね。このように非同期処理を使うと上の行から処理していくという順番が変わってしまいます。


非同期処理の結果を待ってから処理を進めたい場合はどのようにすればよいのでしょうか。その場合はコールバックという仕組みを使います。BridgeTalkの場合はbt.onResultという関数がコールバック処理に相当します。外注に投げた仕事が納品されたらまず依頼した内容通りになっているかチェック作業を行うように、処理が戻ってきたらすぐに行いたい事をここに書きます。


引数に書かれているresとはresponseの略のことで(本当は引数名はなんでもいいのですが)BridgeTalkで処理した後の様々な状態やデータが含まれたオブジェクトです。bt.bodyの返り値はres.bodyに格納されています。

#target illustrator
//#targetengine hogehoge
//BridgeTalkのonResultを使って取得した数値を表示してみる。

var win = new Window("palette");
var getBtn = win.add("button", undefined, "Get the number of selections");
getBtn.onClick = function(){
    //BridgeTalk
    var bt = new BridgeTalk();
    bt.target="illustrator";
    bt.body = "app.activeDocument.selection.length;";
    bt.onResult = function(res){
        alert("The number of selections:" + res.body); //→選択数を表示
    }
    bt.send();
};
win.center();
win.show();

このようにするとアラートに選択された数が正しく反映させることができるようになりました。


返り値の仕様を理解する

BridgeTalkのbt.bodyは癖があり処理内容を返したい場合はreturnを使いません。bt.bodyに割り当てた処理内容の最後の行に相当する一文の内容が返り値として戻ってくるというちょっと変わった仕様になっています。

#target illustrator
//#targetengine hogehoge
//BridgeTalkの返り値の癖を体感してみる。

var win = new Window("palette");
var getBtn = win.add("button", undefined, "Get result");
getBtn.onClick = function(){
    //BridgeTalk
    var bt = new BridgeTalk();
    bt.target="illustrator";
    bt.body = "app.activeDocument.selection.length; 1+2+3; [123,'def'];";
    bt.onResult = function(res){
        alert("Result:" + res.body); //→Result:123,def
    };
    bt.send();
};
win.center();
win.show();

この場合、bt.bodyの最後に指定した「123,def」が返り値として指定されます。
配列を取得することが出来たので複雑な処理を書くことも出来そうな気がしてきます。

ですが、ちょっと待って下さい。その返り値は本当に配列なのでしょうか?試しに配列の1番目を取得し、typeofを使って何が帰ってきているのか確かめてみます。

#target illustrator
//#targetengine hogehoge
//返り値に配列を指定してみる。
var win = new Window("palette");
var getBtn = win.add("button", undefined, "Get result");
getBtn.onClick = function(){
    //BridgeTalk
    var bt = new BridgeTalk();
    bt.target="illustrator";
    bt.body = "[123,'def'];";
    bt.onResult = function(res){
        alert("Result: " + res.body[0]); //→Result:1
        alert(typeof res.body); //→string
    };
    bt.send();
};
win.center();
win.show();

配列の1番目を返すので「123」が帰ってきそうですが実際は「1」が帰ってきます。更に次のアラートで分かるようにres.bodyで帰ってくるものはどうやら文字列のようです。これではres.body[0]で指定したものは文字列の最初の1文字を返しているだけということが理解出来ました。

では文字列ではなく配列を返すにはどうしたらよいのでしょうか。答えは「toSource()とeval()」を使って変換作業を行うことです。

#target illustrator
//#targetengine hogehoge
//配列を返す。
var win = new Window("palette");
var getBtn = win.add("button", undefined, "Get result");
getBtn.onClick = function(){
    //BridgeTalk
    var bt = new BridgeTalk();
    bt.target="illustrator";
    bt.body = "[123,'def'].toSource();";
    bt.onResult = function(res){
        var result = eval(res.body);
        alert("1st: " + result[0]); //→1st: 123
        alert(result instanceof Array); //→true
    };
    bt.send();
};
win.center();
win.show();

bodyの返り値にはtoSource()で変換しres.bodyはeval()をかけてあげます。するとresult[0]はキチンと「123」という数値を返し、instanceofからresultも配列になっているということが確認できます。
オブジェクトに対しても同じように変換をかけることができます。


配列やオブジェクトを返すことができたので他の参照渡しのパターンも試してみます。現在選択されているアイテムの1つをそのまま返してみます。

#target illustrator
//#targetengine hogehoge
//返り値に選択されているアイテムを指定してみる。
var win = new Window("palette");
var getBtn = win.add("button", undefined, "Get a selected item");
getBtn.onClick = function(){
    //BridgeTalk
    var bt = new BridgeTalk();
    bt.target="illustrator";
    bt.body = "app.activeDocument.selection[0];";
    bt.onResult = function(res){
        alert(res.body.left); //→undefined
        alert(typeof res.body); //→string
    };
    bt.send();
};
win.center();
win.show();

当たり前かもしれませんがこちらは動作しません。toString()やeval()を使ってもダメです。何故ならば一度文字列に変換してから返しているので、参照渡しのキモであるアドレスが破棄されてしまうためです。
これはBridgeTalkの仕様なのでどうすることもできません。返り値にアドレスは指定できないと覚えておきましょう。


このようにBridgeTalkには癖があります。

  • 非同期処理である。
  • 一旦、データは文字列変換してからやりとりする。

この二つの仕様を理解した上で上手く使ってあげれば、今まで出来そうもなかったようなことがスクリプトだけで実現可能になるのではないでしょうか。


BridgeTalkの汎用的な関数を作成する

また、一気にすっ飛ばしてしまいますが、いちいちBridgeTalkを書くのも面倒なので汎用的につかえるselfTalkという関数にしてみました。

  • funcには実行したい処理(返り値はreturnを使ってもOK)を指定
  • argsには与えたい引数を配列で格納
  • cb(コールバック)は値が帰ってきた後に処理したい関数を指定
function selfTalk(func, args, cb){
    var bt = new BridgeTalk();
    bt.target = BridgeTalk.appName;
    args = (args !== undefined) ? args.toSource().toString().slice(1, -1) : "";
    bt.body = func.toSource()+"("+ args +");";
    bt.onResult = function(res){
      if(cb !== undefined) cb(res.body);
    };
    bt.send();
};

ただし、引数に日本語の文字列が指定されていると正しく認識してくれません。Unicodeエンコードされ、例えば「あ」は「\u3042」となってしまいます。その場合は下のサンプルのようにreplaceを使って元の文字に戻すことができます。

#target illustrator
//#targetengine hogehoge

function selfTalk(func, args, cb){
  var bt = new BridgeTalk();
  bt.target = BridgeTalk.appName;
  args = (args !== undefined) ? args.toSource().toString().slice(1, -1) : "";
  bt.body = func.toSource()+"("+ args +");";
  bt.onResult = function(res){
    if(cb !== undefined) cb(res.body);
  };
  bt.send();
};

var win = new Window("palette");
var getBtn = win.add("button", undefined, "Test");
getBtn.onClick = function(){
  var str = "ABCあいう";
  selfTalk(function(s){
      alert(s);
      var decoded = s.replace(/(\u)([0-9A-F]{4})/g, function(match,p1,p2){
        return String.fromCharCode(parseInt(p2, 16));
      });
      alert(decoded);
    },
    [str]
  );
};
win.show();

サンプルプログラム

このサンプルで行っている処理は「選択されているアイテムを右か左のチェックされている方向に移動し、その座標値をアラートで返す」という大変無意味なものですが、ウィンドウ上のボタンを押すごとに処理が実行され値が帰ってくるということは体感していただけるかと思います。

#target illustrator
//#targetengine hogehoge
//BridgeTalkを関数にする。
function selfTalk(func, args, cb){
  var bt = new BridgeTalk();
  bt.target = BridgeTalk.appName;
  args = (args !== undefined) ? args.toSource().toString().slice(1, -1) : "";
  bt.body = func.toSource()+"("+ args +");";
  bt.onResult = function(res){
    if(cb !== undefined) cb(res.body);
  };
  bt.send();
};

//main
var win = new Window("palette");
var radioGroup = win.add("group");
var radioBtnL = radioGroup.add("radiobutton", undefined, "Left");
var radioBtnR = radioGroup.add("radiobutton", undefined, "Right");
radioBtnL.value = true;
var moveBtn = win.add("button", undefined, "Move this item");
moveBtn.onClick = function(){
  selfTalk(function(rbtnLeft){
    var result = {};
    var item = app.activeDocument.selection[0];
    if(rbtnLeft){
        item.left -= 100;
    } else {
        item.left += 100;
    }
    result.x = item.left;
    result.y = item.top;
    /* オブジェクトで返す場合はtoSource()を使う。*/
    /* 複数行コメントはOK。1行コメントでは動作しない */
    return result.toSource();
  },
  [radioBtnL.value],
  function(body){
    //返り値がオブジェクトの場合はeval()を使う。
    var obj = eval(body);
    alert("X:" + Math.round(obj.x) + ", Y:" + Math.round(obj.y));
  });
};
win.show();

f:id:two-hats:20150510070103g:plain


ターゲットエンジン

ターゲットエンジンに指定する文字列は「hogehoge」としていますが、どんな文字列でもOKです。
ちなみにスクリプトAとスクリプトBというもので両方とも同じ「#targetengine hogehoge」と指定してあげると、両方同じグローバルオブジェクトを参照することができます。コラボレーションできるスクリプトなどを作ることができるようになりますね。逆に「#targetengine abc」と指定したスクリプトCからはスクリプトAとスクリプトBで共有しているグローバルオブジェクトにアクセスすることは出来ません。


イラレ起動と同時にパネルを立ち上げる

スクリプトにはstartupという仕組みがありイラレなどアプリを起動したと同時にスクリプトを実行する方法があります。これを使えば立ち上がった時にスクリプトのウィンドウが表示されるようになるので、Extension風のパネルにまた一歩近づきそうですね。

下記の場所に「Startup scripts」というフォルダを作成し起動したいjsxファイルを入れておくだけです。例としてCS6で説明します。

Windows: C:\Program Files\Adobe\Adobe Illustrator CS6 (64 Bit)
Mac:   /Applications/Adobe Illustrator CS6


最後に

スクリプトでExtension風パネルというたいそれたタイトルをつけてみましたが、使い勝手的にも正規の方法で作ったパネルには敵いません。ただ、スクリプトに慣れている方は新たな知識を取り入れなくても良いし、HTMLファイルを用意したりする必要もないので「自分自身のためのスクリプトなんだけど、もうちょっと楽に起動したいな」「繰り返し使うのでワンクリックで起動できるようにしたい」って方には有益な情報なのではないかと勝手に考えています。

また、吉田印刷所が定期的に行っているAdobeアプリのバージョン普及率の最近の調査を見るとCS5、CS6のユーザも結構多いようです。HTML5でパネルを作れるようになったのはCCからなので、まだCCに移行できていないというような方々のためにも使えるテクニックになっているのではないかと思っています。

Illustrator・PhotoshopなどのAdobeのデザイン用アプリで使用されているバージョンのアンケート結果を公開(2015年3月調査)|更新情報|吉田印刷所

でも、実際に試してみるとウィンドウの前面・背面の問題があるんですよね・・・うまい方法で常にウィンドウを最前面に固定できれば良いのですが・・・