JPG to BPGコンバーター(Photoshop+Generator)

今回のものはAdobe Generatorプラグインです。このGeneratorプラグインと言うのは従来のプラグインとは異なり、プラグイン自身がPhotoshopのAPIに直接アクセスするものではありません。GeneratorコアがPhotoshopサーバとソケット通信を行い各種コントロールを行います。その為、コード構成は純然たるNODE+Javascriptであり、CEPが抱えるネイティブライブラリが利用できない事やクラスタを構成できない等の制限が存在しません。
そこで、今回は外部ライブラリを利用する構成をテストすると言うことで、少し前に話題になったBPGファイルへの変換を行うプラグインを作ってみます。

まず、http://bellard.org/bpg/よりライブラリを導入します。Windowsの場合バイナリインストーラーが用意されています。MacはHomebrewを利用すると楽ちんでしょう。
GeneratorおよびNODEの導入は前回の記事をご覧ください。

さて、今回はNPMの導入から。

https://github.com/shovon/bpg-stream
よりzipをダウンロードし展開します。

bpg_module.png

上記のようにnode_modulesフォルダに入れておきます。
そしてindex.jsを一部改変します。

      function (filepath, callback) {
        var bpgpath = this.filepath + ‘.’ + ‘bpg’;     
        debugReader(‘Will temporarily write the BPG file to %s’, bpgpath);
        var bpgenc = childProcess.spawn(
          ‘bpgenc’, [ filepath, ‘-o’, bpgpath]
        );
        bpgenc.on(‘close’, function (code) {
          debugReader(‘The encoder has exitted with code %d’, code);
          fs.unlink(filepath);
          if (code) {
            var error = new Error(
              ‘The BPG encoder exitted with a non-zero error code ‘ + code
                + ‘.’
            );
            debugReader(‘An error occurred: %s’, error.message);
            return callback(
              error
            );
          }
          callback(null, bpgpath);
        });
      }.bind(this),

178行目以降の部分が実際にBPGのプロセスを実行する部分です。ネイティブプロセスを呼び出すための引数が最小限ですので、圧縮がデフォルトのままで実行されます。品質を調整するため、以下のようにqパラメータに数値を与えます。

‘bpgenc’, [ filepath, ‘-o’, bpgpath, ‘-q’, ’16’]

このqスイッチは値が大きくなるほど圧縮率が上がり、小さくなるほど品質が上がります。

そして、bpg-streamは依存性の関連でasyncモジュールが必要ですので、これも同様に…

https://github.com/caolan/async

よりダウンロードします。しかしながら、今回のものはプロジェクト自体がわたしのgithubに置かれています。この中には必要なモジュール類もパッケージされている上、上記の改変が既に適用されています。

https://github.com/ten-A/generator-BPG

わたしのgitを利用する場合、こちらのリンクよりzipをダウンロードしてPhotoshopフォルダに作られたpluginsフォルダに解凍したものを投入するだけでインストールは終了します。パッケージ自体を使いたくない場合でもmain.jsとpackage.jsonはダウンロードして下さい。

(function () {
    “use strict”;

    var PLUGIN_ID = require(“./package.json”).name,
        MENU_ID = “BPG”,
        MENU_LABEL = “$$$/JavaScripts/Generator/BPG/Menu=BPG”;

    var _document = null;

    var _generator = null,
        _currentDocumentId = null,
        _config = null;

    var fs = require(‘fs’),
        path = require(‘path’),
        Encoder = require(‘bpg-stream’);

    /*********** INIT ***********/

    function init(generator, config) {
        _generator = generator;
        _config = config;
        console.log(“initializing generator JPG to BPG Converter with config %j”, _config);
        _generator.addMenuItem(MENU_ID, MENU_LABEL, true, false).then(
            function () {
                console.log(“Menu created”, MENU_ID);
            },
            function () {
                console.error(“Menu creation failed”, MENU_ID);
            }
        );
        _generator.onPhotoshopEvent(“generatorMenuChanged”, handleGeneratorMenuClicked);
        function initLater() {
            requestEntireDocument();
        }
        process.nextTick(initLater);
    }

    /*********** EVENTS ***********/

    function handleGeneratorMenuClicked(event) {
        // Ignore changes to other menus
        var menu = event.generatorMenuChanged;
        if (!menu || menu.name !== MENU_ID) {
            return;
        }

        requestEntireDocument();

        var startingMenuState = _generator.getMenuState(menu.name);
        console.log(“Menu event %s, starting state %s”, stringify(event), stringify(startingMenuState));
    }

    /*********** CALLS ***********/

    function requestEntireDocument(documentId) {
        if (!documentId) {
            console.log(“Determining the current document ID”);
        }
        _generator.getDocumentInfo(documentId).then(
            function (document) {
                //console.log(document.file);
                var svName = document.file.replace(/\.jpg|\.JPG|\.JPEG|\.jpeg/, “.bpg”);
                fs.createReadStream(document.file)
                .pipe(new Encoder())
                .pipe(fs.createWriteStream(svName)).on(“close”, function(){
                console.log(“BPG created…”);
                });
            },
            function (err) {
                console.error(“[Tutorial] Error in getDocumentInfo:”, err);
            }
        ).done();
    }

    function setCurrentDocumentId(id) {
        if (_currentDocumentId === id) {
            return;
        }
        console.log(“Current document ID:”, id);
        _currentDocumentId = id;
    }

    function stringify(object) {
        try {
            return JSON.stringify(object, null, ”    “);
        } catch (e) {
            console.error(e);
        }
        return String(object);
    }

    exports.init = init;

    // Unit test function exports
    exports._setConfig = function (config) {
        _config = config;
    };

}());

こちらがメインのJavascriptファイルです。続いて付属のJSONファイル。

{
  “name”: “generator-BPG”,
  “version”: “1.0.0”,
  “description”: “Generator BPG test”,
  “main”: “main.js”,
  “generator-core-version”: “~3”,
  “repository”: {
    “type”: “git”,
    “url”: “https://github.com/ten-A/generator-BPG”
  },
  “license”: “MIT License”,
  “readmeFilename”: “README.md”,
  “scripts”: {
    “test”: “grunt test”
  },
  “dependencies”: {
  },
  “devDependencies”: {
  }
}

ではJavascriptファイルを少し細かく見ていきます。
初期化関連冒頭はメニュー登録用の設定です。PLUGIN_IDはJSONから読み出されます。ややこしいです。

    var PLUGIN_ID = require(“./package.json”).name,
        MENU_ID = “BPG”,
        MENU_LABEL = “$$$/JavaScripts/Generator/BPG/Menu=BPG”;

基本的にJSONはGeneratorコアがプラグインの読み出しを行う為のメタデータを記述しているものです。しかしながら、こういったIDなどの一意なものは整合性を担保するためにこういった読み出しを行っているものと思われます。
今回のものはファイルを取り扱うのとBPG関連のNPMを利用しますので、以下のようにインポートします。

    var fs = require(‘fs’),
        path = require(‘path’),
        Encoder = require(‘bpg-stream’);

続いて初期化ファンクションです。

    function init(generator, config) {
        _generator = generator;
        _config = config;
        console.log(“initializing generator JPG to BPG Converter with config %j”, _config);
        _generator.addMenuItem(MENU_ID, MENU_LABEL, true, false).then(
            function () {
                console.log(“Menu created”, MENU_ID);
            },
            function () {
                console.error(“Menu creation failed”, MENU_ID);
            }
        );
        _generator.onPhotoshopEvent(“generatorMenuChanged”, handleGeneratorMenuClicked);
        function initLater() {
            requestEntireDocument();
        }
        process.nextTick(initLater);
    }

本来ならコアから順に追っかけるのが理解につながるのですが、面倒なので簡単に。各プラグインの読み込み部分です。
addMenuItemファンクションでメニュー登録するのですが、以下にプロトタイプを挙げておきます。

    Generator.prototype.addMenuItem = function (name, displayName, enabled, checked) {
        var menuItems = [], m;

        // Store menu state
        this._menuState[MENU_STATE_KEY_PREFIX + name] = {
            name: name,
            displayName: displayName,
            enabled: enabled,
            checked: checked
        };

        // Rebuild the whole menu
        for (m in this._menuState) {
            if (m.indexOf(MENU_STATE_KEY_PREFIX) === 0) {
                menuItems.push(this._menuState[m]);
            }
        }
        return this.evaluateJSXFile(“./jsx/buildMenu.jsx”, {items : menuItems});
    };

この様にオブジェクトを整えてExtendscriptファイルパスとと共にevaluateJSXFileファンクションに渡されます。

    Generator.prototype.evaluateJSXFile = function (path, params) {
        var self = this,
            deferred = self._sendJSXFile(path, params);

        deferred.promise.progress(function (message) {
            if (message.type === “javascript”) {
                deferred.resolve(message.value);
            }
        });

        return deferred.promise;
    };

はい、こんな感じです。 更に追いかけます。

    Generator.prototype._sendJSXFile = function (path, params) {
        var resolve = require(“path”).resolve,
            readFile = require(“fs”).readFile;
        
        var self = this,
            deferred = Q.defer();
        
        var paramsString = “null”;
        if (params) {
            try {
                paramsString = JSON.stringify(params);
            } catch (jsonError) {
                deferred.reject(jsonError);
            }
        }
        
        if (deferred.promise.isPending()) {
            readFile(resolve(__dirname, path), {encoding: “utf8”}, function (err, data) {
                var id;

                if (err) {
                    deferred.reject(err);
                } else {
                    data = “var params = ” + paramsString + “;\n” + data;
                    id = self._photoshop.sendCommand(data);
                    self._messageDeferreds[id] = deferred;

                    deferred.promise.finally(function () {
                        delete self._messageDeferreds[id];
                    });
                }
            });
        }
        
        return deferred;
    };

こちらでJSXファイルを読み込み、引数をstringifyしてペイロードを組み立てます。

    PhotoshopClient.prototype.sendCommand = function (javascript) {
        var id = this._lastMessageID = (this._lastMessageID + 1) % MAX_MESSAGE_ID,
            codeBuffer = new Buffer(javascript, “utf8”),
            payloadBuffer = new Buffer(codeBuffer.length + PAYLOAD_HEADER_LENGTH);
        
        payloadBuffer.writeUInt32BE(PROTOCOL_VERSION, PAYLOAD_PROTOCOL_OFFSET);
        payloadBuffer.writeUInt32BE(id, PAYLOAD_ID_OFFSET);
        payloadBuffer.writeUInt32BE(MESSAGE_TYPE_JAVASCRIPT, PAYLOAD_TYPE_OFFSET);
        codeBuffer.copy(payloadBuffer, PAYLOAD_HEADER_LENGTH);

        this._sendMessage(payloadBuffer);
        
        return id;
    };

ここで先のテキストペイロードをバッファに収容し

    PhotoshopClient.prototype._sendMessage = function (payloadBuffer) {
        var cipheredPayloadBuffer = this._crypto ? this._crypto.cipher(payloadBuffer) : payloadBuffer;
        var headerBuffer = new Buffer(MESSAGE_PAYLOAD_OFFSET);

        // message length includes status and payload, but not the UInt32 specifying message length
        var messageLength = cipheredPayloadBuffer.length + MESSAGE_STATUS_LENGTH;
        headerBuffer.writeUInt32BE(messageLength, MESSAGE_LENGTH_OFFSET);
        headerBuffer.writeInt32BE(STATUS_NO_COMM_ERROR, MESSAGE_STATUS_OFFSET);
        
        this._writeWhenFree(headerBuffer);
        this._writeWhenFree(cipheredPayloadBuffer);
    };

    PhotoshopClient.prototype._writeWhenFree  = function (buffer) {
        var self = this;
        self._writeQueue.push(buffer);
        process.nextTick(function () { self._doPendingWrites(); });
    };

crypto通してwriteキューに投入っする、って動作をします。
脱線していますが続けましょう。
次はイベントです。

    function handleGeneratorMenuClicked(event) {
        // Ignore changes to other menus
        var menu = event.generatorMenuChanged;
        if (!menu || menu.name !== MENU_ID) {
            return;
        }

        requestEntireDocument();

        var startingMenuState = _generator.getMenuState(menu.name);
        console.log(“Menu event %s, starting state %s”, stringify(event), stringify(startingMenuState));
    }

こちらのファンクションはPhotoshop側からgeneratorMenuChangedイベントがトリガされた際に実行されるものです。この中でrequestEntireDocument()をコールしてますが、こちらがメインディッシュでございます。

    function requestEntireDocument(documentId) {
        if (!documentId) {
            console.log(“Determining the current document ID”);
        }
        _generator.getDocumentInfo(documentId).then(
            function (document) {
                //console.log(document.file);
                var svName = document.file.replace(/\.jpg|\.JPG|\.JPEG|\.jpeg/, “.bpg”);
                fs.createReadStream(document.file)
                .pipe(new Encoder())
                .pipe(fs.createWriteStream(svName)).on(“close”, function(){
                console.log(“BPG created…”);
                });
            },
            function (err) {
                console.error(“[Tutorial] Error in getDocumentInfo:”, err);
            }
        ).done();
    }

開いているドキュメントのファイルパスを取得してread streamを組み立てます。そのストリームをそのままBPGエンコーダーにパイプし、更にwrite streamへと渡します。NODEでのストリームの扱いが分かっていれば、非常に単純な手順で機能が実現できるのがお分かりいただけると思います。
実際には開いているドキュメントのレイヤービットマップを抽出してJFIFストリームを組み立ててパイプする等の処理を行うほうがスマートでしょう。
このGeneratorですが、コールバックのいり乱れを避けるためにプロミスを導入しています。コードを追いかける場合には通常のNODE.jsと構成が異なることにご留意下さい。また、bpg-streamではasyncのwater-fallを利用しています。どちらも、いわゆるコールバック地獄を緩和する為の実装ですが、比較すると面白いかもしれません。

では、起動してみます。まずPhotoshopを立ち上げましょう。起動したらターミナルを立ち上げGeneratorコアのディレクトリに移動します。
ここで、確認しておきましょう。インストールが正しく行われていればPhotoshopフォルダの中の構成は以下のようになります。

generator_folder.png

続いてコマンド入力。

node app -f ../plugins

以下はGenerator起動時のログです。

$ node app -f ../plugins
[debug:core 12:44:49.865 generator.js:99:17] Launching with config:
{}
[info:core 12:44:50.393 generator.js:197:25] Detected Photoshop version: 15.2.2
[debug:core 12:44:50.505 generator.js:1501:21] Loading plugin: generator-BPG
initializing generator JPG to BPG Converter with config {}
[debug:core 12:44:50.588 generator.js:1517:21] Plugin loaded: generator-BPG
Menu created BPG
 

更にメニューを選択してみましょう。

generator_menu.png
フォトショプが行うのはメニューの選択を検知してイベントを発生させる事と問い合わせられたドキュメント情報を返すのみです。あとはNODE側で全ての処理が行われます。以下はNODE側のログです。

Determining the current document ID
Menu event {
    “generatorMenuChanged”: {
        “name”: “BPG”
    },
    “timeStamp”: 1430437817.945,
    “count”: 0
}, starting state {
    “enabled”: true,
    “checked”: false
}
/Users/No41/Downloads/bpg-0.9.5-win32/bpgsample/ig.jpg
BPG created..

ご覧のように、最近の流行りに乗っかっているのか、各種イベント等のペイロードはJSONです。
このプラグインですが、結果をPSへ返していませんのでPS側ではファイルが生成されたかどうかを検知しません。
色々と細部をすっ飛ばしているのですが、Generatorがどういったものかという片鱗ぐらいは感じていただけたのではないかと思います。
さて、結果なのですが…

http://chuwa.iobb.net/bpgsample/

をご覧ください。圧縮レベルを揃えていませんので直接的には比較出ませんが、ブロックノイズが出ないぶん綺麗に見えますが、細部をよく見るとディティールが失われている事ががわかると思います。

広告

JPG to BPGコンバーター(Photoshop+Generator)」への1件のフィードバック

  1. デティールというか、女の子のそばかすがすっ飛んじゃってますねぇ
    重要な用途には使いづらいかも

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中