ピグでサーバーサイドエンジニアをしています。市川です。
今回はピグパーティやfamchattyのアイテム制作で使用している、AdobeIllustratorのエクステンション開発時に行ったTips等について紹介したいと思います。
はじめに
AdobeIllustratorで拡張機能を作るには、ExtendScriptと呼ばれるスクリプトを使用すれば、必要なレイヤー配下の画像だけPNGで書き出す事やテキストフィールドの内容を読み取ってメタ情報に使う等ができます。
ExtendScriptはWindowsではVBScript、macOSではAppleScriptを用いて利用することも出来ますが、今回は両方のOSで動作することのできるJavaScriptを使用して作成しました。
また実際にAdobeIllustratorをどのような操作ができるかは、ドキュメントを参照しながら必要な機能を開発していきました。
AdobeIllustrator Scripting ドキュメント
スクリプトをJavaScriptで開発ができるため、最近追加されたES6(ES2015)のコードも利用して開発したいと思いましたので、webpackとbabelを用いてES6のコードをES5に変換することにしました。(今回の記事ではwebpackとbabelの説明については省略させて頂きます。)
ただ、ExtendScriptで使用されているJavaScriptのエンジンは、古いJavaScriptのコード(たぶん、ES3ぐらい)しか使用することができないので、Array.forEach、Array.map、Object.keys、JSON等といったES5で追加されたコードを使用することができません。
そこで今回は、独自にES5で追加されたArray.forEach、Array.map、Object.keysといった関数を使用できる変換ライブラリを作成し、JSONはサードパーティのライブラリを追加することにしました。
webpackのconfig設定では、entryで独自に作ったES5変換ライブラリとJSONのライブラリを追加指定して1つのファイルにまとめています。
また、babelのプラグインにはES3用に変換するモノがあったのでそれも利用することにしました。
webpackのconfigファイルのサンプル
module.exports = function() {
return {
entry: [
// ES5で追加された機能のライブラリ
'./lib/polyfill_es5.js',
// JSONのライブラリ
'./lib/json.js',
// ExtendScriptのメインロジックのルートファイル
'./src/es/index.js'
],
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist/es')
},
...
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['es2015'],
plugins: [
require('babel-plugin-transform-es3-member-expression-literals'),
require('babel-plugin-transform-es3-property-literals'),
require('babel-plugin-transform-es5-property-mutators'),
]
}
}
]
}
]
}
ExtendScriptを単体で実行させたい場合はAdobeIllustratorの「ファイル > スクリプト > その他のスクリプト」で実行させる事ができます。
または、ExtendScript Toolkitをインストールすればブレイクポイントを置いてのデバッグ実行やコンソールログを表示させたりすることもできます。
次に書き出しツールの利便性を向上させるために、エクステンションでパネルを作成することにしました。
エクステンションは、Adobe-CEP(Common Extensibility Platform)と呼ばれるコンポーネント上で動作します。
エクステンションの作成には、タイトルや実行できるバージョン等を示したmanifest.xmlとUIを表示させるためのHTML、AdobeIllustratorで書き出しするためのExtendScript、あとは必要に応じてHTMLのデザイン様にCSSや画像などになります。
また、エクステンションからExtendScriptを呼び出すためには、CSInterface.jsというライブラリが必要になります。
作成したパネルがこのような感じこちらになります。
作成したエクステンションを使用するには、インストールをしてから「ウインドウ > エクステンション > (作成したエクステンション名)」で使用することができます。
インストールするためには、zxpファイルという形式にする必要があります。
zxpにするにはZXPSignCMDというコマンドを使用して証明書を作成してからパッケージング化させる必要があります。
(ZXPSignCMDはAdobe-CEPのGithubからダウンロードすることができます。)
次に示すコマンドが証明書生成方法とパッケージ化する方法になります。
証明書生成
# ZXPSignCmd -selfSignedCert 国 地域 組織 名前 パスワード 保存先ファイル名
./ZXPSignCmd -selfSignedCert JP Tokyo CyberAgent PIGG ${PASS} ./pigg.p12
パッケージング化
# ZXPSignCmd -sign ソースフォルダ 保存先ファイル名 証明書 パスワード
./ZXPSignCmd -sign ./dist ./PIGGExporter.zxp ./pigg.p12 ${PASS}
zxpファイルが生成できたらインストールなのですが、
AdobeCC2015からはExtensionManager(エクステンションを管理するアプリケーション)が使えなくなった?みたいなので、ExManCmdのコマンドを使用すればインストールすることができますが、エンジニアでない方にコマンドラインでインストールさせるのは大変ですし社内ツールをわざわざ「Adobe Add-ons」に申請するのも大変なので、今回はExManCmdをラップされたZXPInstallerを使用してインストールする事にしました。
開発手法
ここからは、エクステンションを開発する際に、私が行った開発手法を紹介したいと思います。
エクステンションを作成して実際に動作確認するまでにはいくつかの手順がありとても面倒です。
そこで、私は以下のような方法をとりました。
まず、インストールですが実際は特定ディレクトリにzxpファイル内のファイルが展開されているだけですので、開発しているディレクトリ部分とエクステンションの展開先をシンボリックリンクにすることにしました。(ここで示しているパスはmacOSでのパスになります。)
$ ln -sfn ./dist "/Library/Application Support/Adobe/CEP/extensions/jp.co.cyberagent.pigg.exporter"
次に証明書がなくても起動できるようにするために、下記のコマンドを実行させればデバッグモードということで証明書が無くても動作させる事ができます。
(CSXSの後の数字はAdobeのバージョンが上がると変わって来るので必要に応じて変更してください。)
$ defaults write com.adobe.CSXS.7 PlayerDebugMode 1
これで、毎回zxpパッケージを作成する手間が省けました。
次に、今回はwebpackでJavaScriptファイルを1つにまとめているので、webpackのwatch機能を使用して変更があったら自動的にビルドされるようにしました。
(CLIでwebpackのコマンド使用時に`–watch`オプションを追加するか、webpackのconfigファイルで`watch:true`としておけば大丈夫です。)
これでコードに変更があったらビルドまで出来ました。
ですが今のままですと、エクステンションに変更があってもAdobeIllustratorを再起動しないと反映されません。
そこで、再起動をしなくてもコードを反映させるためにデバッグボタンを用意しました。
これはデバッグビルド時だけ表示されるようにHTMLを修正していて、ボタンが押されるとエクステンション側のJavaScriptのエンジンで
window.location.reload()
のJavaScriptが実行され、UI部分のHTMLがリロードされます。
また、
const CSLibrary = new CSInterface()
const extPath = CSLibrary.getSystemPath(SystemPath.EXTENSION) + '/es/bundle.js' CSLibrary.evalScript(`$.evalFile("${extPath}")`)
この様にExtendScriptを配置しているディレクトリを指定し「$.evalFile」というExtendScriptのGlobal関数を実行してあげれば変更されたコードがAdobeIllustratorの再起動をしなくても変更する事ができるようになります。
余談ですが、ExtendScriptのコードにはExtendScriptTools上のコンソールログに表示させるための「$.writeln」というGlobal関数があり、ログが表示されるので開発中はとても便利です。
ただ、個人的に「$.writeln」という記述方法がExtendScriptの関数であまり馴染みがないので、webpackのregexp-replace-loaderというプラグインを使用して「console.log」と書いた文字列を「$.writeln」に置換するようにしました。
ハマった事
エクステンションを作っていてハマった部分でどのように解決したかを紹介できればと思います。
- Symbolの基準点がスクリプトから分からない
メタ情報に基準点から画像の左上までの距離を入れる必要があったのですが、Symbolの基準点がExtendScript上からは取れそうになかったので、結局基準点となるSymbolを配置してもらう事にして、そのSymbolItemまでの距離を計ることにしました。 - Symbolの解体方法
実際に書き出す画像は細かな状態で書き出す必要があるのですが、イラストの制作ではSymbolを用いてイラストが描かれているため、SymbolItemを解体して中身の画像を取り出す必要がありました。Symbolを解体するメソッドがドキュメント上には存在しなかったのですが、ExtendScriptToolsでデバッグモードで見ていたら、brakLink()メソッドがありそれを用いれば解体できることがわかりました。 - 日本語が文字化けする
エラー文言等を表示する際に、日本語が文字化けしてしまう問題がありました。これはutf8-bomに変換することで解消することができました。
webpackのプラグインにutf8-bom変換するものがあったので、今回はそれを導入しました。 - メタ情報をバイナリデータにしたい
ピグパーティではメタ情報にMessagePackを使用しているため、バイナリデータを生成する必要がありました。
ExtendScriptではバイナリデータを扱うには大変なため、ExtendScriptでJSONの中間ファイルを生成してから、Adobe-CEPのNode.jsのBufferを使用してMessagePackを書き出す事にしました。ただ、Node.jsのバージョンですが、不定期でAdobeIllustratorのバージョンが上がるとNode.jsのバージョンも変わるので利用しているモジュールが動かなくなる恐れやAdobeIllustrator自体がクラッシュする事があるので使用には注意が必要かと思います。(Adobe-CEPのGitHubのリポジトリ上のドキュメントに詳しく記載されていますので使用する際にはチェックしてください。) - 回転角度を取得する方法
ドキュメントにはオブジェクトを回転させるためのrotate()関数は存在するのに回転した値を取得する方法がはじめわかりませんでした。
Adobeのフォーラムで調べていたら、オブジェクトに付随されているtagsという情報に追加されるということがわかったので、そこから取得する方法にしました。(tagsはkey-valueのペアで構成されています。)
角度の値ですが、BBAccumRotationというkeyでvalueにラディアン値で入っています。 - 変更していないデータでPNG画像書き出すと差分が発生する
GitLFSでバージョン管理を行っているのですが、AdobeIllustratorで何も変更せずにPNG画像を書き出すと、Git上では差分が発生してしまう状態がありました。これはたぶん、PNGの最適化等を行うことでこの差分は発生しなくなるかと思います。 - 三項演算子がネストしているとエラーになる
たとえば、let a = 30 let b = a > 10 ? a < 50 ? 'a' : 'b' : 'c'
みたいな感じの三項演算子を用いたコードが存在するとExtendScript上ではエラーになってしまい動作することができなくなってしまいます。
今回のツールでは、三項演算子のネストしたものが無かったので、ESLintのno-nested-ternaryを使用しエラーにすることで、今後の開発者が間違って記述しないようにしました。
さいごに
今回、AdobeIllustratorのエクステンションを作成のためにwebpack+babelを使用してモダンなJavaScriptで開発できるように行いました。ですが、ExtendScriptを実行させると不明なエラーが発生して止まったりして、安定して動作させるのが大変でした。ですが、webpackを用いていたため、既に存在するプラグインを使用したり、一般的なUtilityなライブラリを使用したりして必要最小限での開発をすることができました。
今後もより良いものを作っていきたいと思います。