DTPab

DTPにまつわるあれこれ

targetengineとそれを利用した擬似的ライブラリ

f:id:uske_S:20170924180614j:plain
(10/24 10:15 追記)

現れた場所に関係なく、変数の宣言はコードを実行する前に処理されます。var を伴って宣言した変数のスコープは実行コンテキスト (execution context)であり、これは変数を含んでいる関数、または関数の外で宣言された変数はグローバルになります。

関数の外で呼ばれたvar宣言付きの変数はグローバルです*1。ご指摘いただいた@macneko_ayuさん、@cs5_omachiさん、ありがとうございました。
(追記ここまで)


先週土曜日、東京のDTP勉強会にて登壇させていただきました。起こしくださった方々、どうもありがとうございました。現状はフォローアップに向けて最終調整中ですので、サンプルスクリプトの配布、アンケートでいただいた質問の回答等々はもうしばらくお待ちください。

実のところ、はじめは「サンプルスクリプトの紹介」くらいの内容で考えていました。それが全体として「使い方から作り方にフォーカス」することになり、結果としてあのような内容になりました。
ですので、イベントスクリプトなどというそもそも厄介なものを、スクリプト初学者や未経験の方もいる中で発表するということ自体、とても難しかったです。50分という制約の中で、ちゃんと有益なスクリプトを紹介しつつ、それに手を加えるにはどうしたらいいかまで盛り込むというのは苦労しました。
スクリプト自体は現時点で作れる最大限のものをご用意できたと自負していますが、セッションについてはもっといい伝え方があったのでは、、、という反省があります。

セッションでは時間の都合で飛ばしてしまいましたが、今回は#targetengineについて、とても大事な要素なので説明したいと思います(間違いがあればコメントかTwitterでお知らせいただけると助かります)。

まずはAdobeの公式ドキュメントより、targetengineに関する部分を引用します。

デフォルトでは、InDesignJavaScriptを実行すると、メイン(main)のExtendScriptエンジンでスクリプトが解釈されて実行されます。このエンジンは、スクリプトの実行が終了すると破棄されます。このスクリプトで作成されたスクリプトオブジェクトは保持されません。
セッション(session)のエンジンでスクリプトを実行した場合、そこで作成されたオブジェクトは、InDesignを終了するまで保持されます。このオブジェクトは、sessionエンジンで実行される他のスクリプトからも参照できます。InDesign JavaScriptのターゲットとしてsessionエンジンを設定するには、スクリプトの先頭に次の行を追加します。

#targetengine "session"

また、ExtendScriptのパーシスタント(永続的)な解釈および実行環境を独自に作成することもできます。これを行うには、#targetenging文を使用して、独自のExtendScriptエンジンの名前を設定します。次のスクリプトにその例を示します。

#targetengine "adobe"

というわけで、mainエンジン以外に任意のエンジンを指定すれば、

  1. InDesignを終了するまでオブジェクトが保持される
  2. 同じエンジンを指定すれば、他のスクリプトからオブジェクトを参照できる

ということがいえます。それぞれテストしてみましょう。

test1.jsxとして、下記のスクリプトを用意します。

#targetengine "session"
var a = "hoge";
var b = [0, "foo", true];

続いて、test2.jsxとして、下記のスクリプトを用意します。

#targetengine "session"
alert(a);
alert(b[1]);

そうしたら、InDesignから(バージョンは不問です)test1.jsxを実行したあと、test2.jsxを実行してみてください。
test2.jsxで定義していない変数aも変数bも正しく呼び出せたと思います。これが「オブジェクトが保存され」、「他のスクリプトからオブジェクトを参照できる」ということです。
勉強会のセッションでは「targetengineとはスクリプトを実行するレイヤーのようなもの」と漠然とした説明しかしませんでした。何が言いたかったかというと、このように、同じレイヤーのものを共有で利用できるということです。

勘の鋭い方は気づいたかもしれませんが、これはグローバル変数と似たような使い方が可能になります。しかし、グローバル変数と違ってちゃんとvar宣言しているのでローカル変数です。あくまで、同じtargetengineの中でのみ共有できるローカル変数、というわけです。(10/24 訂正:var宣言しても変数自体は関数の外なのでグローバル変数です)
したがって、単純な変数だけでなく、関数も共有することが可能です。
先ほどのtest1.jsxに、下記のような呼び出し関数を記述します。

#targetengine "session"
var a = "hoge";
var b = [0, "foo", true];
function getBool (ary){
    for (var i=0; i<ary.length; i++){
        if (ary[i] === true || ary[i] === false) alert (i+": "+ary[i]);
        }
    }

配列の中にプリミティブ型のBooleanがいれば、それを教えてくれる関数です。
つづいて、test3.jsxとして、下記のようなスクリプトを作成します。

#targetengine "session"
getBool (b);

そうしたら、改めてInDesignからtest1.jsxを実行したあと、test3.jsxを実行してみてください。
問題なければ2: trueと表示されると思います。これはtest1.jsxで定義された変数bを読み込んでいます。
つまり、main(デフォルト)以外の同じtargetengine内であれば、定義した関数をいつでも呼び出せるようになります。このようなよく使う共通の関数を用意しておくことは、スクリプトを作成する上では非常に有益です。
ただし、InDesignを再起動し、test3.jsxから実行するとエラーになります。当然ですがこれは変数や関数をtest1.jsxで定義していたためです。共有の関数を使うために、あるスクリプトを事前に実行しておかなくてはいけないというのは、ちょっと面倒ですね。

そこでstartup scriptsを利用します。今回でいえばtest1.jsxのような、共通で利用したいオブジェクト(変数や関数をひっくるめて)を記述したスクリプトをstartup scriptに入れておくのです。勉強会のセッションでもお話した通り、startup scriptsに入っていればInDesign起動時に自動で実行される=オブジェクトが読み込まれる、いわばスクリプトの環境設定を整えるスクリプトになります。

一見するととても便利なのですが、もちろん注意すべきポイントもあります。

#targetengine "session"
var c = 6, d = 0;
for (var i=0; i<c; i++){
    d++;
    }

仮にInDesign上でこのようなスクリプトを実行したとしましょう。そうすると変数cdには6という値が入っていることはパッとわかると思います。しかし、実は変数iが存在し、それにも6が入っていることを意外と見落としがちです。for文に限らず、宣言したすべての変数、オブジェクトが共有になります。そしてそれらは後から定義されたものに上書きされるのです。
では、test4.jsxとして、下記のような関数だけを書いたスクリプトを作成します。

#targetengine "session"
function getBool (ary){
    for (var i=0; i<ary.length; i++){
        if (ary[i] === false) alert (i+": "+ary[i]);
        }
    }

InDesignからtest1.jsx、test4.jsx、test2.jsxという順番で起動すると、先ほどのtest2.jsxとは結果が異なります(というかアラートダイアログが出なくなります)。これは関数が上書きされてしまったことを意味します。
このように、targetengineを利用してオブジェクトを共有することは、コーディングの省力化にはとても有益である反面、使い方次第では致命的な不具合を起こしかねません。この点はグローバル変数を利用する場合とよく似ています。書き上げたスクリプトには全くエラーがないのに(targetengineが同じ別のスクリプトの関数が原因で)エラーが起きて正しく動作しないということが起こり得るわけです。

したがって、エラーのない正しいコードを書くことは大前提として、このような擬似的ライブラリとしての使用において最も大事なのはその運用方法です。
具体的には、以下のようなことが考えられます。

  • 共有するtargetengineは1つ(ないし管理できる数)に留める
  • 共有したいオブジェクトは1つのスクリプトに記述する(もしくは、共有したいオブジェクトについて1ファイルとし、ファイル名をそのオブジェクト名にする)
  • 共有したくないオブジェクト等をスクリプトの最後で削除(開放)する

ほかにもあるかとは思いますが、いま思いつくのはこれくらいでしょうか。
まずスクリプト間で共有するようなtargetengineは、1つか2つくらいに留めておいたほうが無難です。また、共有したくないけれどもtargetengineを指定しないといけない場合は、他と被らない名前(ユニーク)にする必要があります。
また、オブジェクトを共有するためのスクリプトを、例えばハブのように、1つにまとめることで管理の手間を削減することができるでしょう。もしくは、このスクリプトはこの共有オブジェクトが書かれているということが明示的に分かるように、ファイル名=そのオブジェクト名とすることも有用でしょう。
最後のオブジェクトの削除(開放)というのはなかなか難しいので、現実的ではないかもしれませんが、一考の余地はあります。

少し長くなりましたが、以上がtargetengineとその応用についての補足です。
先述の通り、勉強会のフォローアップは鋭意準備中ですので、改めてよろしくお願いします。今回は以上です。