見出し画像

Adobe XDプラグインでの画像書き出し・読み込みの実装について

Adobe XD Plugins Advent Calendar 2018 の17日目の記事です。
個人開発したXDプラグイン「Image minify」の実装で得られた知見について書き残します。

目次
1. Image minifyについて
2. レイヤーの表示内容を画像として書き出す
3. 複数レイヤーの書き出し処理を非同期実行する
4. GroupとArtboardの子レイヤーへの実行
5. 設定データの保存と読み込み
6. 今後の課題
注意
この記事で扱うXD API仕様は2018/12/17現在のものです。この記事とあわせて公式ドキュメントで最新情報を確認することをオススメします。


1. Image minifyについて

つくったのはこんな感じのプラグインです。
選択したレイヤーに含む画像のサイズと画質を調整し最適化することで、PDFファイルにエクスポートした際のファイル容量を削減します。


2. レイヤーの表示内容を画像として書き出す

以下がImage minifyの主な処理の流れです。

選択レイヤーから画像を取得
 ↓
適切な解像度と画質で画像を書き出す
 ↓
書き出した画像をレイヤーに再配置

これをシンプルに実装してみます。

const ImageFill = require("scenegraph").ImageFill;
const fs = require("uxp").storage.localFileSystem;
const application = require("application");

async function mainCommand(selection) {

  // 選択した中から単一のレイヤーを取り出す
  let node = selection.items[0];
  if(node.fill && node.fill instanceof ImageFill){
    try{

      // 画像書き出し先に一時フォルダを指定
      const folder = await fs.getTemporaryFolder();

      // .guidを利用してレイヤー固有のファイル名にする
      const file = await folder.createFile(node.guid + '.png', {overwrite: true});
      let renditionSettings = [{
        node: node,
        outputFile: file,
        type: application.RenditionType.JPG,
        scale: 1,
        quality: 80
      }];

      // 画像ファイルの書き出し
      const results = await application.createRenditions(renditionSettings);
      if(results){
        // 書き出した画像をレイヤーに再読込
        node.fill = new ImageFill(file);
      }

    }catch(error){
      console.log(error);
    }
  }
}

解像度の高い画像レイヤーを選択して実行すると、画像の解像度が低減することがわかります。

参考
・保存先にTemporaryFolder(一時フォルダ)を指定することで、ファイルの保存ダイアログを開くことなくファイルの保存ができます。
・画像ファイルの書き出しについては公式ドキュメント How to export a rendition を参考にしました。


3. 複数レイヤーの書き出し処理を非同期実行する

同時に2点以上の画像書き出しに対応するために、先ほどの処理を複数回実行可能なように書き換えます。

function mainCommand(selection) {
  // 選択したすべてのレイヤーに対して処理を実行
  for(let i = 0; i < selection.items.length; i++){
    const node = selection.items[i];
    resizeImage(node);
  }
}

async function resizeImage(node){
  if(node.fill && node.fill instanceof ImageFill){
    try{

      /* 省略 */

    }catch(error){
      console.log(error);
    }
  }
}

これを実行すると、こんな感じのエラーになります。

[Error: Plugin xxxxxx is not permitted to make changes from the background. Return a Promise to continue execution asynchronously.]

非同期でレイヤーなどに変更を加える場合は、メイン関数がPromiseを返さなければいけないとのこと。今までPromiseやasync/awaitでの非同期処理をまともに実装したことが無かったのでここで結構つまずきましたが、結果非同期にループを順次実行するような処理に書き換えて解決しました。

async function mainCommand(selection) {
  // 選択したすべてのレイヤーに対して非同期に処理を順次実行
  for(let i = 0; i < selection.items.length; i++){
    const node = selection.items[i];
    await resizeImage(node);
  }
}

function resizeImage(node){
  // 非同期な関数としてPromiseを返すように実装
  return new Promise(async function(resolve){
    if(node.fill && node.fill instanceof ImageFill){
      try{

      /* 省略 */

      }catch(error){
        console.log(error);
      }
    }
    resolve();
  });
}

複数画像への同時実行に成功しました!

参考
async / await の理解と非同期な繰返し処理の実装について、以下の記事が助けになりました。
Async Functions


4. GroupとArtboardの子レイヤーへの実行

複数画像への実行に成功したので、今度は選択レイヤーにGroupやArtboardを含んでいる場合に子レイヤーを辿って実行するようにしたいです。

先ほどの resizeImage() 関数に変更を加え、GroupかArtboardの場合はchildrenを取得し、自関数を再帰的に呼び出すように変更します。

function resizeImage(node){
  return new Promise(async function(resolve){
    if(node instanceof Artboard || node instanceof Group){
      // 選択レイヤーがArtboardかGroupの場合の処理を追加
      for(let i = 0; i < node.children.length; i++){
        let child = node.children.at(i);
        if(child){
          await resizeImage(child);
        }
      }
    }else if(node.fill && node.fill instanceof ImageFill){
      /* 省略 */
    }
    resolve();
  });
}

Artboardを選択して実行すると、内包する画像レイヤーに処理が適用されます。

Groupを選択して実行すると、こんな感じのエラーになります。


Plugin Error: Plugin made a change outside the current edit context

この「edit context」についてAPIリファレンスに説明があります。

If the user has drilled down into a container node, that container is the current edit context and only its immediate children are in scope for selection/editing.
(ユーザがコンテナノードにドリルダウンした場合、そのコンテナは現在のedit contextであり、直接の子のみが選択/編集の対象となります。)

https://adobexdplatform.com/plugin-docs/reference/core/edit-context.html 

さらっと書かれているので今まで見落としていましたが、Selectionに含むGroupの子に対して変更を加えることはできないようです。仕方ないのでGroupへの処理の実行は今回は諦めました。


5. 設定データの保存と読み込み

主な処理の実装はこれで完了ですが、折角なので設定ダイアログで解像度の比率やJPG画質を変更できるようにしてみます。

こうした設定値は次回起動時にも同じ値を使用したいので、設定変更後に保存し、プラグイン処理を始める前に読み込むようにします。
設定の保存、読み込み処理は以下のように実装しています。

// 設定の初期値
let setting = {
  scale: 1,
  quality: 80
}

// 設定ファイルのファイル名
const settingFileName = 'setting.json';

// 設定の保存処理
async function saveSetting(){
  return new Promise(async resolve => {
    try{

      // 設定ファイルはDataFolderに格納
      const folder = await fs.getDataFolder();
      const file = await folder.createEntry(settingFileName, {overwrite: true});

      // settingオブジェクトをJSON形式に変換して保存
      file.write(JSON.stringify(setting));

    }catch(error){
      console.log(error);
    }
    resolve();
  });
}

// 設定の読み込み処理
async function loadSetting(){
  return new Promise(async resolve => {
    try{

      // 設定ファイルはDataFolderから読み込み
      const folder = await fs.getDataFolder();
      const file = await folder.getEntry(settingFileName);
      const contents = await file.read();

      // JSONのパース
      const contentObj = JSON.parse(contents);
      if(contentObj){

        // パースしたオブジェクトをsettingに格納
        Object.assign(setting,contentObj);
      }
    }catch(error){
      console.log(error);
    }
    resolve();
  });
}
参考
・ダイアログの実装方法は割愛しましたが、公式ドキュメントのModal dialogsを参考にしています。
・dataFolderへのファイルの保存、読み込みについては公式ドキュメントStorage APIsを参考にしています。


6. 今後の課題

プラグインの機能・品質としての課題はまだまだあります。特に以下はファイル容量への影響が大きいためなんとかしたいところですが、現状のXD APIでは画像のバイナリデータを直接確認できないため、実現が難しそうな気がしています。

・同一内容の画像を同一ファイルとして書き出し・読み込みをして容量を削減したい
・透過領域を含まないPNG画像もJPGファイルとして圧縮したい

API仕様のアップデートを待ちながら緩々開発しようと思います。

※ Image minifyのコードは https://github.com/shingo2000/xd-plugin-image-minify にアップしています。

この記事が気に入ったらサポートをしてみませんか?