ElectronはNodeとほとんど差がないので、NPMで入れたパッケージも使える。 今回は簡単な画像編集ができるアプリを作ってみる。
npm init
まず、Moduleを入れる前にpackage.json
からつくる。
特に設定はいらないので、全部基本設定で十分。
npm i -S jquery jimp tinycolor2
jimp
とtinycolor2
は純粋にJavascriptだけで作られたImage編集moduleである。
次にアプリの起動部も入れておく
const electron = require('electron')
const app = electron.app
const BrowserWindow = electron.BrowserWindow
var mainWindow = null
app.on('window-all-closed', function () {
app.quit()
})
app.on('ready', function () {
mainWindow = new BrowserWindow({width: 800, height: 600})
mainWindow.loadURL('file://' + __dirname + '/main-browser/index.html')
mainWindow.on('closed', function () {
mainWindow = null
})
})
index.html
<!DOCTYPE html>
<html>
<head>
<title>Picmod</title>
<style>
body {
margin: 0;
}
#canvas {
width: 100%;
}
</style>
</head>
<body>
<div id='mainWindow'>
<div id='pallet'>
<button id='open'>Open</button>
<button id='reset'>Reset</button>
<button id='filter1'>Filter1</button>
<button id='filter2'>Filter2</button>
<button id='filter3'>Filter3</button>
</div>
<img id='canvas'>
</div>
<script>
require('./index.js')
</script>
</body>
</html>
簡単に写真を編集するボタンとイメージたぐを配置した。
index.js
を次のように作成する。
const fs = require('fs')
const path = require('path')
const $ = require('jquery')
const Jimp = require('jimp')
const electron = require('electron')
const remote = electron.remote
const data = {
originalPath: null,
tempPath: null
}
function getPicturePath () {
return remote.app.getPath('pictures')
}
function getTempPath (filename) {
var dataPath = remote.app.getPath('appData')
if (filename != null) return path.join(dataPath, filename)
return dataPath
}
function openImage () {
var selectedPaths = remote.dialog.showOpenDialog({
defaultPath: getPicturePath(),
properties: ['openFile'],
filters: [{
name: 'Images',
extensions: ['jpg', 'png', 'gif']
}]
})
if (selectedPaths == null) return null
return selectedPaths[0]
}
function copyFile (originalPath, targetPath, cb) {
var rstream = fs.createReadStream(originalPath)
var wstream = fs.createWriteStream(targetPath)
rstream.pipe(wstream)
rstream.on('end', cb)
}
$('#open').on('click', function () {
if (data.tempPath != null) {
fs.unlinkSync(data.tempPath)
data.originalPath = null
data.tempPath = null
}
var originalPath = openImage()
if (originalPath != null) {
data.originalPath = originalPath
var filename = path.basename(originalPath)
data.tempPath = getTempPath(filename)
copyFile(data.originalPath, data.tempPath, function () {
$('#canvas').attr('src', 'file://' + data.tempPath + '?' + new Date().getTime())
})
}
})
$('#reset').on('click', function () {
copyFile(data.originalPath, data.tempPath, function () {
$('#canvas').attr('src', 'file://' + data.tempPath + '?' + new Date().getTime())
})
})
$('#filter1').on('click', function () {
if (data.tempPath == null) return false
Jimp.read(data.tempPath, function (err, image) {
if (err) {
console.error(err)
}
image.posterize(5).write(data.tempPath, function () {
$('#canvas').attr('src', 'file://' + data.tempPath + '?' + new Date().getTime())
})
})
})
$('#filter2').on('click', function () {
if (data.tempPath == null) return false
Jimp.read(data.tempPath, function (err, image) {
if (err) {
console.error(err)
}
image.sepia().write(data.tempPath, function () {
$('#canvas').attr('src', 'file://' + data.tempPath + '?' + new Date().getTime())
})
})
})
$('#filter3').on('click', function () {
if (data.tempPath == null) return false
Jimp.read(data.tempPath, function (err, image) {
if (err) {
console.error(err)
}
image
.color([
{ apply: 'hue', params: [ -90 ] }
])
.write(data.tempPath, function () {
$('#canvas').attr('src', 'file://' + data.tempPath + '?' + new Date().getTime())
})
})
})
全体的な機能はOpen buttonから写真を開いて、写真に幾つかの効果をいれてみることである。Jqueryは普通につかえるし、Jimpの使い方自体はあまり気にしなくていい。
今回重要であるのは、わざわざremote
からdialog
をよびだしていることである。
一般的にWeb appでファイルを受け取るときは<input type='file'>
を使う。
しかし、今回の場合はdialog
を呼び出したほうがいい。なぜなら、Electronも一応Chromeであって、input
タグからファイルを選択しても正確な経路が取れない。
(保安的な理由でC:\fakepath\~
のように変な経路を返す)
なので、正確な経路を取るためにはdialog
を使う必要がある。
では、実行してみよう。
実行してみると一応働くと思うが、3番目のフィルターがかなり重いと思う。さらい、致命的な問題はJimpが純粋なJSでかかれているため、画面(Renderer process)が完全に凍ってしまう。
今回はその問題を解決するために、他のProcessから重い作業を起動させて、Renderer processが止まらないようにする。
ここでWeb workerを使えばいいと思うが、ElectronはWeb workerが使えない。
正直に言うといらない。殆どのNodeのmoduleがElectronでも使えるので、child_process
を使えば簡単に解決できる。
では、やってみよう。
まず、アプリが凍っているのかを確認するため簡単にlogを吐き出すボタンをindex.html
に追加する。
<button onclick='console.log("still alive!")'>Check</button>
では、child_process#fork
でフィルター3の部分を実行できるようにコードを分離してみよう
。
index.js
var worker = null
const ChildProcess = require('child_process')
$('#filter3').on('click', function () {
if (data.tempPath == null) return false
if (worker) return false
console.log(data.tempPath)
worker = ChildProcess.fork(path.join(__dirname, 'filter3.js'), [data.tempPath])
worker.on('exit', function () {
console.log('done')
$('#canvas').attr('src', 'file://' + data.tempPath + '?' + new Date().getTime())
worker = null
})
})
filter3.js
const Jimp = require('jimp')
Jimp.read(process.argv[2], function (err, image) {
if (err) {
console.error(err)
}
image
.color([
{ apply: 'hue', params: [ -90 ] }
])
.write(process.argv[2], function () {
process.exit()
})
})
実行してみると、フィルター3が処理中であっても、Checkボタンが働くと思う。
しかし、これは一番最適なゴールではない。 本質的に遅すぎることが問題なので、Native moduleを入れる必要がある。
npm i -S lwip
LwipもJimpもほぼおなじものであるが、Native moduleである。
index.js
の3番めのFilterを次のように書き直す。
$('#filter3').on('click', function () {
if (data.tempPath == null) return false
lwip.open(data.tempPath, function (err, image) {
if (err) {
throw err
}
image
.hue(90, function (err, image) {
image.writeFile(data.tempPath, function (err) {
if (err) throw err
$('#canvas').attr('src', 'file://' + data.tempPath + '?' + new Date().getTime())
})
})
})
})
一応コードはこれで終わる。
しかし、実行をしてみるとできないと思う。
理由はNative moduleは今使っているNodeに合わせてRebuildしなきゃならない。
ElectronもNodeが実装されているが、今設置したNPMはそのNodeから実行されていない。
つまり、バイナリーが設置されたらRebuildする必要がある。
その役割は、electron-rebuild
というものがする。
npm i -D electron-prebuilt electron-rebuild
electron-rebuild
はelectron-prebuilt
を必要とするので、改めて設置する。
次にRebuildさせる。
./node_modules/.bin/electron-rebuild
メッセージは何も出ないけど、これができたらおわりである。 実行してみるとめっちゃ時間がかかった3番めのFilterがかなり早くなっていることがわかると思う。
ちなみに、package.json
のscripts.postinstall
にelectron-rebuild
入れたら、次から設置するNative moduleは勝手にRebuildされるようにしたらかなり楽になる。
{
"scripts": {
"start": "electron index.js",
"postinstall": "electron-rebuild"
},
}