ごんれのラボ

iOS、Android、Adobe系ソフトの自動化スクリプトのことを書き連ねています。

CEP ExtensionsとInDesignをCallbackで仲良くしてもらう方法

概要

今日は CEP Extensions のお話。
これの続き。

www.macneko.com

やりたかったこと

とりあえず、Extensions と InDesign とで、値のやり取りができないことには面白くないので、やりとりする方法を知りたかった。

道のり

Illustrator版のCallbackのサンプルコードを見つける

まず真っ先に調べるのはいつも Twitter で質問攻めにしているてまりさんのサイト。
Callbackについての記事を発見。

Call Back from Extendscript - 手抜きLab@DTPの現場

しかし、飲み込みの悪い私はこれを読んでも全然わからない…。
どうしたものかと頭を悩ませていると、なんとてまりさんが Github で公開されていたサンプルプロジェクトの中に callbacktest なるものがあるじゃないですか。
これは参考にするしかないということで、早速ゲット。

github.com

こちらは Illustrator 用に書かれていたので、取り急ぎ Illustrator で動作確認してみたら、バッチリ動いた。
これは manifest.xml を書き換えたら InDesign でも簡単に動くでしょー。

現実は甘くなかった

InDesign に前述の Extensions を突っ込んで動作確認してみたが、Callback を経由しての応答がない。
どうやらそう簡単に Adobe 神は微笑んでくれないようだ…。

改めて、InDesign 用の Callback について調査をしたところ、下記のサイトにヒントが書いてあった。

aphall.com

ツールがネイティブに発送するイベント以外にも、好きなイベントをツールVMから発送するExtendScriptのAPIもある。(現時点ではJSFL用のAPIは無いので、ネイティブイベントを利用する必要となる。)

方法は、PlugPlugExternalObject というプラグインを使う。現状ではPhotoshop、Illustrator、Premiere ProとAfter Effectsはそれぞれこのプラグインを内部的に実装するので、エクステンションに追加する必要は無い。InDesign、InCopyで使うには、プラグインをエクステンションのソースに置いて、CEPドキュメンテーションのp41のようなコードを実装する必要がある。

なるほど、InDesign と Illustrator では、実装されている内容というか形式が違うのね。
InDesign はユーザー数が Photoshop、Illustrator、Premiere Pro と After Effects より少ないから実装が先送りになっているのかなぁ(邪推)

PlugPlugExternalObject を使おうとして、四苦八苦

まず、PlugPlugExternalObject をダウンロードする。
私は Mac ユーザーなので、PlugPlugExternalObject-Mac.zip をダウンロードした。

github.com

解凍したら PlugPlugExternalObject.framework が入っているので、これを (project_dir)/jsx/ の中に入れる。

続いて、 CEP ドキュメント を参考にして、PlugPlugExternalObject を jsx から読み込めるように実装する。
ドキュメントに書いてあるコードがこちら。

try {
    var xLib = null;
    var ppLibFile =
        File(File($.fileName).parent).fullName+"/PlugPlugExternalObject");
    if (ppLibFile.exists) {
        var xLib = new ExternalObject("lib:"+ ppLibFile.fullName);
    }
    else {
        throw new Error("Can't find PlugPlugExternalObject: " 
                        +ppLibFile.fullName+,$.fileName,$.line);
    }
}
catch(e) { alert(e); }

これで動くと思うでしょ?
ざーんねん、このソースコードはそもそもシンタックスエラーで実行すらできません!
ExtendScript Toolkit を使っていれば気づいたんだろうけど、Brackets でさくさく開発環境を楽しんでいたので、気づくのが遅れた。
久しぶりの Adobe クオリティを突きつけられたので、Twitter で報告したよ。

そして、修正したコードは ExtendScript Toolkit では動作するものの、Extensions に組み込むと動作しないという二重の罠。
どうやら jsx の Helper Object ($ で表現されているやつ)がうまく動作していない模様。

こういうときは、てまりさんに教えてもらうに限る。
いただいた回答によれば、CSInterface クラスに systemPath クラスがあるので、そこから extensions のパスを取得したりできるとのこと。
なるほど、Extensions で自身のパスを取得して、それを jsx に渡せばいいのね。
csInterface.evalScript() で引数を扱う書き方がわからなくて、だいぶ試行錯誤したけど、

csInterface.evalScript('methodName("' + argument + '")'); 

という書き方になるようだ。

そんなわけで、

main.js

(function () {
    'use strict';
    var csInterface = new CSInterface();
    function init() {
        themeManager.init();
        // プロジェクト内のjsxディレクトリのパスを取得
        var jsxDirPath = csInterface.getSystemPath(SystemPath.EXTENSION) + "/jsx";
        // jsxにjsxディレクトリのパスを渡す
        csInterface.evalScript('setDirPath("' + jsxDirPath + '")');
    }
    init();
}());

hostscript.jsx

// グローバル変数でjsxディレクトリのパスを保持
var dirPath = "";

// 引数で渡ってきたパスをグローバル変数に代入する
function setDirPath(jsxDirPath) {
    dirPath = jsxDirPath;
}

これで jsx から (project_dir)/jsx/ を知ることができるようになった。

紆余曲折あったけど、念願のCallback処理にたどり着いた

とりあえず Callback が動作していることがわかればいいので、ボタンを押したら jsx から渡されたメッセージをテキストエリアに反映するという簡単なものを。

main.js

(function () {
    'use strict';
    var csInterface = new CSInterface();
    var message = "";
    function init() {
        themeManager.init();
        // プロジェクト内のjsxディレクトリのパスを取得
        var jsxDirPath = csInterface.getSystemPath(SystemPath.EXTENSION) + "/jsx";
        // jsxにjsxディレクトリのパスを渡す
        csInterface.evalScript('setDirPath("' + jsxDirPath + '")');

        // コールバックを受け取って、テキストエリアに反映する
        csInterface.addEventListener("getCallback", function (evt) {
            message = evt.data;
            $("#textarea").text(message);
        });
        // ボタンをクリックしたらコールバックをセットする
        $("#button").click(function () {
            var str = "sample";
            csInterface.evalScript('setCallback("' + str + '")', function (e) {
            });
        });
    }
    init();
}());

hostscript.jsx

// グローバル変数でjsxディレクトリのパスを保持
var dirPath = "";

// 引数で渡ってきたパスをグローバル変数に代入する
function setDirPath(jsxDirPath) {
    dirPath = jsxDirPath;
}

// コールバックをセットする
function setCallback(str) {
    try {
        var xLib = null;
        // PlugPlugExternalObject.frameworkのパスを生成
        var ppLibFile = File(dirPath + "/PlugPlugExternalObject.framework");
        if (ppLibFile.exists) {
            // ExternalObjectを生成
            xLib = new ExternalObject("lib:"+ ppLibFile.fullName);
        }
        else {
            throw new Error("Can't find 'PlugPlugExternalObject.framework'.");
        }
    }  catch(e) { alert(e); }

    // コールバック処理
    var eventObj = new CSXSEvent();
    eventObj.type = "getCallback";
    eventObj.data = createMessage(str);
    eventObj.dispatch();
    xLib.unload();
    return true;
}

function createMessage(str) {
    return "Hello extension " + str + ".";
}

テキストエリアに入力した文字を加工して表示させるほうが良かったかもと思ったけど、疲れたのでスルー。

わからないこともたくさんある

eventObj.data に入れるものは string じゃないとInDesignが落ちてしまうんだけど、たとえば Object を Extensions にそのまま渡したい時はどうしたらいいんだろうか。
ParagraphStyles なんて膨大なプロパティがあって、その中に別の Object も格納されているわけで、そのまま渡ってこないと厳しいよね。

サンプルコードはこちら

github.com