前編の内容
前編では、Node-REDでWebAssemblyを使う方法として、functionノードにWebAssemblyバイナリを埋め込む方法と、独自ノード内でWebAssemblyを呼び出す方法の前準備までを説明しました。
後編では、前編で作成したWebAssemblyコンパイル済みのPNGトリミングルーチンを使って独自ノードを作っていきます。
(承前) 第二段階: PNG画像のトリミングをする独自ノードの作成
Node-REDノードの作成
Node-RED Nodeの作り方のドキュメントを参考にしながら、モジュールを作っていきます。
まずnpmモジュールの雛形を作ります。
% mkdir node-red-contrib-png-crop
% cd node-red-contrib-png-crop
% npm init -y
...
%
次に、package.json
を更新します。
"name": "node-red-contrib-png-crop",
"version": "1.0.0",
"description": "Cropping PNG image",
"keywords": [],
"author": "",
"license": "ISC",
"node-red": {
"nodes": {
"png-crop": "png-crop.js"
}
},
"dependencies": {
}
}
ランタイム側のロジックはpng-crop.js
として記述します。
const pngcropwasm = require('./pngcrop');
module.exports = (RED) => {
function PngCropNode(config) {
RED.nodes.createNode(this,config);
const node = this;
node.on('input', (msg, send, done) => {
if (Buffer.isBuffer(msg.payload.img)) {
const x = msg.payload.x || 0;
const y = msg.payload.y || 0;
const width = msg.payload.width || 100;
const height = msg.payload.height || 100;
const cropped_img = pngcropwasm.croppng(msg.payload.img, x, y, width, height);
msg.payload.img = cropped_img;
send(msg);
done();
} else {
done('no image');
}
});
};
RED.nodes.registerType('png-crop', PngCropNode);
}
冒頭のrequire()
でWebAssemblyバイナリにコンパイルされたPNGトリミングライブラリをモジュールとして読み込んでいます。そして、Node-REDのメッセージハンドラで、ペイロードに含まれているイメージファイルを引数としてトリミング関数を呼び出し、結果をメッセージとして送信しています。
あとは、エディタ側の記述です。とくに設定インタフェースはないので、最低限の記述のみしてあります。
<script type="text/javascript">
RED.nodes.registerType('png-crop', {
category: 'function',
color: '#F3B567',
defaults: {
name: {value: ""}
},
inputs: 1,
outputs: 1,
icon: "font-awesome/fa-crop",
label: function() { return this.name ||"png-crop";}
});
</script>
<script type="text/html" data-template-name="png-crop">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
</script>
<script type="text/html" data-help-name="png-crop">
<p>A simple node that crops the image</p>
</script>
独自ノードのインストールとフローの作成
これをインストールして、フローを作ってみましょう。
% cd ~/.node-red/
% npm install ....../node-red-contrib-png-crop
...
% cd ...../node-red
% npm start
functionカテゴリに"png crop"というノードができています。
試しにWeb上のPNG画像をトリミングしてダッシュボードに表示するフローを作ってみます。
[{"id":"924181c4.4467c","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"fc70fd9.aa3a5","type":"inject","z":"924181c4.4467c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":100,"wires":[["aef153f1.97613"]]},{"id":"aef153f1.97613","type":"http request","z":"924181c4.4467c","name":"Ferris the crab","method":"GET","ret":"bin","paytoqs":"ignore","url":"https://rustacean.net/assets/rustacean-flat-happy.png","tls":"","persist":false,"proxy":"","authType":"","x":340,"y":100,"wires":[["b33daa13.f05df8"]]},{"id":"b33daa13.f05df8","type":"change","z":"924181c4.4467c","name":"","rules":[{"t":"move","p":"payload","pt":"msg","to":"payload.img","tot":"msg"},{"t":"set","p":"payload.x","pt":"msg","to":"480","tot":"num"},{"t":"set","p":"payload.y","pt":"msg","to":"350","tot":"str"},{"t":"set","p":"payload.height","pt":"msg","to":"300","tot":"str"},{"t":"set","p":"payload.width","pt":"msg","to":"300","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":540,"y":100,"wires":[["1f7259b8.b4e1e6"]]},{"id":"7497a53c.595bdc","type":"function","z":"924181c4.4467c","name":"Base64 encode","func":"const cropped = Buffer.from(msg.payload.img);\nmsg.payload = cropped.toString('base64');\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":340,"y":180,"wires":[["6dd7310.e04afd"]]},{"id":"6dd7310.e04afd","type":"ui_template","z":"924181c4.4467c","group":"c744f26f.11e5f","name":"","order":0,"width":0,"height":0,"format":"<div style=\"height: 300px; width: 300px\">\n<img src=\"data:image/png;base64,{{msg.payload}}\"\n alt='cropped image'\n />\n </div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":520,"y":180,"wires":[[]]},{"id":"1f7259b8.b4e1e6","type":"png-crop","z":"924181c4.4467c","name":"","x":160,"y":180,"wires":[["7497a53c.595bdc"]]},{"id":"c744f26f.11e5f","type":"ui_group","z":"","name":"Default","tab":"22fdfd7c.238b92","order":1,"disp":true,"width":"6","collapse":false},{"id":"22fdfd7c.238b92","type":"ui_tab","z":"","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]
RustマスコットのFerris the Crabの画像を持ってきて、顔の部分を切り出してui_template
ダッシュボードに表示するフローになっています。
速度は?
気になるのが変換速度になります。
5K画像を100回トリミングする簡単なスクリプトを書いて、速度を調べました。
const pngcrop = require('png-crop');
for (let i=0; i<100; i++) {
pngcrop.crop('./test.png', './testout.png',
{width: 1000, height: 1000, top: 10, left: 10},
(err) => { if (err) throw err; });
}
const pngcrop = require('./pngcrop');
const fs = require('fs');
for (let i = 0; i < 100; i++) {
const input = fs.readFileSync('./test.png', {flag:'r'});
const cropped = pngcrop.croppng(input, 10,10,1000,1000);
fs.writeFileSync('./testout.png',cropped);
}
結果は下記の通りです。プログラムの作りが違うので直接は比べられませんが、WebAssemblyベースのほうが約1.4倍高速という結果になっています。もちろん、ネイティブコードで実装すればさらに高速化できますが、ここではNode.jsで完結するところに利点を見出しています。
所要時間 | pure JS比 | |
---|---|---|
pure JS版 | 98秒 | 1 |
Rust+Wasm+JS版 | 68秒 | 0.69 |
今回はwasm-bindgen
で生成したファイルを手でコピーしてnpmモジュールに追加していますが、wasm-pack
を使うとnpmモジュールの作成まで自動的に行えます。Node-REDのノードモジュールとして使うときには、wasm-bindgen
の生成物をそのままコピーで十分かと思います。
最後に
やや無理やりな使い方ではありますが、RustとWebAssemblyの組み合わせでNode-REDノードが記述できることを示しました。
WebAssemblyによって、多様な言語で記述したプログラムがブラウザ上やNode.js上で安全に実行できるようになりました。また、WebAssembly System Interface(WASI)によってWebAssemblyはサンドボックスを備えた軽量な実行環境としても使われるようになってきています。今後は、エッジコンピューティングなどより広い応用範囲で使われる技術になっていくと期待しています。