ごんれのラボ

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

Adobe XDのアートボードをPNGに書き出すPluginを作った

概要

2018年10月よりAdobe XDのPlugin開発が誰でもできるようになった。
そこで、公式のサンプルを読み解きつつ、ドキュメント内のアートボードをPNGに書き出すPluginを作ってみた。

仕様

  • 用途に応じて、メニューから「選択しているアートボードを書き出す」「すべてのアートボードを書き出す」を選べる
  • 書き出しサイズは以下のサイズから選べる
    • 0.5x(50%)
    • 1x(100%)
    • 1.5x(150%)
    • 2x(200%)
    • 3x(300%)
    • 4x(400%)
    • 5x(500%)
  • 書き出したPNGはダイアログで選択したフォルダ内に 20181126101110 というような名前のフォルダを作成し、その中に格納される
    • 実行後に表示するアラートに保存先が表示される
  • PNGはアートボード名で書き出される

使い方

output

インストール方法

  1. こちらからxdxファイルをダウンロード
  2. ダウンロードしたxdxファイルをダブルクリック
  3. Pluginをインストールするか確認する旨のダイアログが表示されるので、インストールを選択
  4. インストール完了した旨が表示される
  5. メニュー>プラグイン に「export-artboards-to-png」が表示され、利用可能になる

アンインストール方法

  1. メニュー>プラグイン から「プラグインを管理...」を選択
  2. サイドバーから「インストールされたプラグイン」を選択
  3. 「export-artboards-to-png」にマウスカーソルを合わせると、名前の右側に「…」が表示されるので、クリック
  4. アンインストールを選択
  5. Pluginがアンインストールされる

困ったこと

ドキュメント上のすべてのNodeを取得する方法がわからない

コマンドに渡される第二引数の documentRoot がドキュメント上のすべてのNodeだった。

ドキュメントより抜粋。

Typically, you access scenegraph nodes via the selection argument that is passed to your plugin command, or by traversing the entire document tree using the documentRoot argument that is passed to your plugin command.

option タグで selected をサポートしていない

ダイアログ表示時にデフォルトで書き出しサイズを設定しておきたかったんだけど、ドキュメントにサポートしていない旨書いてあって諦めた。
他に方法があるかもしれないけど…。 ドキュメントはこちら

ソースコード

GitHubで公開しているので、詳しくはそちらを参照してほしい。
export-artboards-to-png

main.js だけ載せておく。

const application = require('application');
const fs = require('uxp').storage.localFileSystem;
const { Artboard } = require('scenegraph');
const {alert, confirm, prompt, error, warning} = require("./lib/dialogs.js");

let selectedRatio;

async function exportSelectedArtboards(selection, root) {
    let nodes = selection.items.filter(node => node instanceof Artboard);
    if (nodes.length == 0) {
        error('エラー', 'アートボードを1つ以上選択して実行ください');
        return;
    } else {
        exportArtboards(nodes);
    }
}

async function exportAllArtboards(selection, root) {
    let nodes = root.children.filter(node => node instanceof Artboard);
    if (nodes.length == 0) {
        error('エラー', 'アートボードが存在しません');
        return;
    } else {
        exportArtboards(nodes);
    }
}

async function exportArtboards(nodes) {
    const outputFilePaths = await exportNodes(nodes);
    if (undefined == outputFilePaths) {
        return;
    }
    const joinedFilePath = outputFilePaths.join('\n');
    alert('処理が完了しました', `書き出したPNGファイルは下記に格納されています\n\n${joinedFilePath}`);
}

async function exportNodes(nodes) {
    const dialog = await makeSelectScaleDialog();
    await dialog.showModal();
    dialog.remove();

    if (selectedRatio.selectedIndex < 0) {
        return;
    }

    const excutedTimeStr = getNowYMDHMS();
    const selectedFolder = await fs.getFolder();
    if (null == selectedFolder) {
        return undefined;
    }
    const folder = await selectedFolder.createFolder(excutedTimeStr);

    let renditions = await makeRenditionOptions(nodes, folder);
    application.createRenditions(renditions)
        .then(results => {
    })
    .catch(error => {
        console.log(error);
    })

    // 書き出したファイルのパスを配列に格納
    const outputFilePaths = renditions.map(rendition => {
        return rendition.outputFile.nativePath;
    });

    return outputFilePaths;
}

async function makeRenditionOptions(nodes, folder) {
    let renditions = [];
    await Promise.all(nodes.map(async (node, i) => {
        const file = await folder.createFile(node.name + '.png', {overwrite: true});
        renditions.push({
            node: node,
            outputFile: file,
            type: 'png',
            scale: selectedRatio.options[Math.max(0, selectedRatio.selectedIndex)].value
        });
    }));
    return renditions;
}

function getNowYMDHMS() {
    const dt = new Date();
    const y = dt.getFullYear();
    const m = ('00' + (dt.getMonth()+1)).slice(-2);
    const d = ('00' + dt.getDate()).slice(-2);
    const h = ('00' + dt.getHours()).slice(-2);
    const mm = ('00' + dt.getMinutes()).slice(-2);
    const s = ('00' + dt.getSeconds()).slice(-2);
    const result = y + m + d + h + mm + s;
    return result;
}

function makeSelectScaleDialog() {
    const labelWidth = 75;

    const dialog =
        h("dialog",
          h("form", { method:"dialog", style: { width: 380 }},
            h("h1", "Select the export scale"),
            h("label", { class: "row" },
              h("span", { style: { width: labelWidth } }, "Scale"),
              selectedRatio = h("select", {  },
                                h("option", { selected: true, value: 0.5 }, "0.5x"),
                                h("option", { value: 1 }, "1x"),
                                h("option", { value: 1.5 }, "1.5x"),
                                h("option", { value: 2 }, "2x"),
                                h("option", { value: 3 }, "3x"),
                                h("option", { value: 4 }, "4x"),
                                h("option", { value: 5 }, "5x")
              )
            ),
            h("footer",
              h("button", { uxpVariant: "primary", onclick(e) { selectedRatio.selectedIndex = -1; dialog.close(); } }, "Cancel"),
              h("button", { uxpVariant: "cta", type: "submit", onclick(e){ dialog.close(); e.preventDefault; } }, "OK")
            )
          )
        )
    document.body.appendChild(dialog);
    return dialog;
}

/**
* Shorthand for creating Elements.
* @param {*} tag The tag name of the element.
* @param {*} [props] Optional props.
* @param {*} children Child elements or strings
*/
function h(tag, props, ...children) {
    let element = document.createElement(tag);
    if (props) {
        if (props.nodeType || typeof props !== "object") {
            children.unshift(props);
        }
        else {
            for (let name in props) {
                let value = props[name];
                if (name == "style") {
                    Object.assign(element.style, value);
                }
                else {
                    element.setAttribute(name, value);
                    element[name] = value;
                }
            }
        }
    }
    for (let child of children) {
        element.appendChild(typeof child === "object" ? child : document.createTextNode(child));
    }
    return element;
}

module.exports = {
    commands: {
        exportSelectedArtboards,
        exportAllArtboards
    }
};

次はなに作るの?

なにかしら通信が発生するものを作りたい。

感想

ES6で書けるの、最高。
InDesignのCEPもES6に対応してくれ!
頼む!!!!!!!!!

参考

公式ドキュメント
はじめてのAdobe XDプラグイン開発!定番のHello Worldを表示させてみよう