Photoshop Extension 開発におけるデバッグ効率化対策

はじめに
以前、このブログで Live2D のことを書いてました。今はテクニカルアーティスト室所属の dockurage です。 テクニカルアーティスト室では、Maya、Unity、Photoshop といったソフトウェアにおける業務効率化のためにツールを作成しています。 本記事では私が担当していた Photoshop のツール制作についてのデバッグの対策のお話をしたいと思います。
Adobe の拡張機能について
Photoshop でツールを作成する場合、いくつか選択肢があります。 その中でベーシックな、アプリケーションに統合されたパネルを開発することができ る CEP(Common Extensibility Platform)という拡張方法があります。弊社のプロジェクトでは主にこの CEP を使用して業務改善ツールの開発を行なっています。
CEPについて
CEP では2つの VM が動作しておりそれぞれに実装を行う必要があります。 パネルデザイン (UI部分) には HTML5/CSS/JavaScript を使用する Panel VM と Photoshop の動作制御には ExtendScript を使用する Host VM にわかれています。
      
   
        
具合例を挙げて動作の流れを説明すると、「ボタンを押すと新規レイヤーがひとつ追加される機能」の場合、 ユーザーが操作するボタンは Panel VM で動作し、そのクリック操作をフックにして Host VM で新規レイヤーを作成するという流れになります。
CEP の今後について
補足ですが、実は M1 Mac が登場したことにより、以降の機種について、 CEP は Rosetta 2 でしか動作しなくなりました。そのかわりに UXP に移行していく流れがあります。 弊社では Windows / Mac 両方対応しているため、今後 UXP への移行を検討する時期にきています。
CEPのデバッグについて
CEP の UI パネルは Chromium Embedded Framework 上で表示されています。 そのため Chrome の機能が用意されており、デバッグについても同じで Chrome Developer Tool を使用することができます。 ただしこれは Panel VM に限ってであり Host VM のデバッグを効率化できるものではありません。
      
   
        
[補足] デバッグ環境の設定方法について
.debug について
エクステンションフォルダのルートに .debug ファイルを作成し、そこにアプリケーション (Photoshop, Illustrator など) ごとにデバッグ画面をどの Port で表示するかを設定します。
.debug
<?xml version="1.0" encoding="UTF-8"?>
<ExtensionList>
  <Extension Id="extension_name">
    <HostList>
      <!-- Photoshop -->
      <Host Name="PHXS" Port="8188"/>
    </HostList>
  </Extension>
</ExtensionList>Developer Tool の起動について
Chrome ブラウザを起動し URL フォームに下記を入力します。
chrome://inspect/#devices「Devices」>「Remote Target」>「[CEP Extension]」を選択すると CEP Extension を対象とした Developer Tool が起動します。
      
   
        
Panel VM と Host VM の連携について
前項で書いたここについて深堀していきます。
ただしこれは Panel VM に限ってであり Host VM のデバッグを効率化できるものではありません。
基礎的な連携パターン
CEP でアプリケーションを作るにあたり、Panel VM と Host VM はセットで作成する必要が出てきます。 もっとも基礎的な Panel VM から Host VM に処理を投げる場合は下記の関数を使用します。
csInterface.evalScript("<任意のスクリプト文字列>");例えば下記のように記述すると文字列で ExntendScript を実行します。
let csInterface = new CSInterface();
csInterface.evalScript(`var a = 1+1; a += 1; a;`, result => {
  console.log(`Host VM 側で実行した結果 a = ${result} です`);
});[実行結果]
Host VM 側で実行した結果 a = 3 ですJavaScript と ExtendScript でファイルを分割するパターン
もしくは ExtendScript 用のファイルを別に作成して関数を定義し、JavaScript 用のファイルにはその関数の呼び出しだけを書くことでも動作します。
[hostvm.jsx] (ExtendScript)
function test(a, b) {
  return a + b;
}[panelvm.js] (JavaScript)
let csInterface = new CSInterface()
csInterface.evalScript(`test(1, 1)`, result => {
  console.log(`Host VM 側で実行した結果 a = ${result} です`);
})[実行結果]
Host VM 側で実行した結果 a = 2 ですJavaScript と ExtendScript の連携中にエラーを起こした場合
例えば Host Vm 内でシンタックスエラーを起こさせるためにあえて「,」を入れます。
let csInterface = new CSInterface();
csInterface.evalScript(`var a = 1+1; ,; a += 1; a;`, result => {
  console.log(`Host VM 側で実行した結果 a = ${result} です`);
});[実行結果]
Host VM 側で実行した結果 a = EvalScript error. ですするとコールバック関数の引数である「result」には「 EvalScript error.」という文字列が返却されます。Host VM で、なんのエラーによるものなのか、動作はどこで止まっているかなどの判断はできません。 これが CEP 開発時に問題が発生した時に、問題箇所を洗い出すのに時間がかかる原因となります。
ここまでが前談となります。以降はこの問題をどう解消するかの本題になります。
Chrome Developer Tool の Console に Panel VM と Host VM のログを出力させる
CSXSEvent について
Host VM から Panel VM へカスタムイベントを発送することができます。これは PlugPlugExternalObject というプラグインを利用します。 下記は簡単な実例です。
[hostvm.jsx] (ExtendScript)
var xLib = new ExternalObject("lib:PlugPlugExternalObject");
if (xLib) {
  var eventObj = new CSXSEvent();
  eventObj.type = "<イベント名>";
  eventObj.data = "message";
  eventObj.dispatch();
}[panelvm.js] (JavaScript)
csInterface.addEventListener("<イベント名>", event => {
  console.log(event.data);
});このように記述することで Host VM 側から任意のタイミングでイベントを発火させ、Panel VM 上でフックして処理を行うことが可能になります。
Logger クラス
この考え方をもとに Host VM の方に Logger クラスを作成します。
      
   
        
[hostvm.jsx] (ExtendScript)
var logger = (function (global) {
  function _check() {
    try {
      var xLib = new ExternalObject("lib:PlugPlugExternalObject");
    } catch (e) {
      alert(e);
      return false;
    }
    return xLib;
  }
  function log(msg) {
    if (_check()) {
      var data = {message: msg};
      var eventObj = new CSXSEvent();
      eventObj.type = "jsx_logger";
      eventObj.data = JSON.stringify(data);
      eventObj.dispatch();
    }
  }
  function warn(msg) {
    if (_check()) {
      var data = {message: msg};
      var eventObj = new CSXSEvent();
      eventObj.type = "jsx_warn";
      eventObj.data = JSON.stringify(data);
      eventObj.dispatch();
    }
  }
  function error(error) {
    if (_check()) {
      var data = error;
      var eventObj = new CSXSEvent();
      eventObj.type = "jsx_error";
      eventObj.data = JSON.stringify(data);
      eventObj.dispatch();
    }
  }
  return {
    log: log,
    warn: warn,
    error: error
  }
})(this);Panel VM の EventListener 処理
Host VM のイベントを受け取れるように、Panel VM に EventLister を記述します。このコールバックには、Host VM で受け取ったメッセージを console で出力します。これで Host VM のログが Chrome Developer Tool で表示できるようになります。
[panelvm.js] (JavaScript)
csInterface.addEventListener("jsx_logger", event => {
  console.log(`[JsxLog] ${event.data.message}`);
});
csInterface.addEventListener("jsx_warn", event => {
  console.warn(`[JsxWarn] ${event.data.message}`);
});
csInterface.addEventListener("jsx_error", event => {
  console.error(`[JsxError:${event.data.name}](${event.data.fileName.split('/').pop()}:${event.data.line}:${event.data.number}) ${event.data.message}`);
});Host VM の エラーをキャッチする
上記だけでもログは Chrome Developer Tool に表示させることができますが、 これから書く方法を取るといちいちエラーログを仕込む必要がなくなるのでオススメです。
csInterface.evalScript で実行した処理のエラーを必ず取得できるように Wrapper を作ります。
まず Host VM は Panel VM が必ず execute という関数を実行するという体で、try/catch 文を仕込み、作成した logger.error を実行するようにします。こうすることで Host VM 側でエラーが発生すると必ずエラーのカスタムイベントが発送されるようになり ます。
[hostvm.jsx] (ExtendScript)
var __global = this;
function execute(_script, _args) {
  var result;
  try {
    result = _script.apply(__global, _args ? JSON.parse(_args) : undefined);
  } catch (e) {
    logger.error(e);
    return false;
  }
  return result;
}あとは Tool VM の execute を実行するように Panel VM に csInterface.evalScript の Wrapper を作成します。
[panelvm.js] (JavaScript)
async function evalScriptWrapper(method) {
  let _arguments = arguments;
  return await new Promise(resolve => {
    let _script;
    if (_arguments.length > 1) {
      let _args = Object.keys(_arguments).map(v => _arguments[v]);
      Array.prototype.shift.call(_args);
      _script = `execute(${method}, '${JSON.stringify(_args)}')`;
    } else {
      _script = `execute(${method})`;
    }
    csInterface.evalScript(_script, result => {
      let parsed;
      try {
        parsed = JSON.parse(result);
      } catch (e) {
        resolve(result);
        return
      }
      resolve(parsed);
    });
  })
}実際に実行してみる
今回のテスト用に Host VM には引数をログ出力する処理を記述した関数を用意します。エラーログも出したいので未定義の変数にアクセスするように「undefinedVariable.test」も書いておきます。
[hostvm.jsx] (ExtendScript)
function test(a, b, c) {
  logger.log(a);
  logger.warn(b);
  undefinedVariable.test;
  logger.log(c);
}evalScriptWrapper の第一引数に Tool VM で定義した使用する関数名、第二引数以降にその関数で使用する引数を設定します。
[panelvm.js] (JavaScript)
await evalScriptWrapper("test", 1, 2, 3)
      
   
        
このように ExtendScript 側の各ログが種類ごとに適切な形式で出力されるようになりました。エラーが発生した箇所は具体的な問題が出力されています。また、エラーが発生した場合はそこで処理が停止するので、最後のログは出力されていません。 もし Photoshop のツール開発でエラー周りで時間を浪費してしまう場合は、このやり方を検討してみてもらえたらと思います。