Skip to content

Instantly share code, notes, and snippets.

@Rokt33r
Created Nov 28, 2015
Embed
What would you like to do?
Use NPM

Use NPM

ElectronはNodeとほとんど差がないので、NPMで入れたパッケージも使える。 今回は簡単な画像編集ができるアプリを作ってみる。

1. 設置

npm init

まず、Moduleを入れる前にpackage.jsonからつくる。 特に設定はいらないので、全部基本設定で十分。

npm i -S jquery jimp tinycolor2

jimptinycolor2は純粋に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
  })
})

2. Component 配置

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>

簡単に写真を編集するボタンとイメージたぐを配置した。

3. 機能実装

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を使う必要がある。

では、実行してみよう。

4. Web worker?

実行してみると一応働くと思うが、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ボタンが働くと思う。

5. Native module

しかし、これは一番最適なゴールではない。 本質的に遅すぎることが問題なので、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-rebuildelectron-prebuiltを必要とするので、改めて設置する。

次にRebuildさせる。

./node_modules/.bin/electron-rebuild

メッセージは何も出ないけど、これができたらおわりである。 実行してみるとめっちゃ時間がかかった3番めのFilterがかなり早くなっていることがわかると思う。

Tip

ちなみに、package.jsonscripts.postinstallelectron-rebuild入れたら、次から設置するNative moduleは勝手にRebuildされるようにしたらかなり楽になる。

{
  "scripts": {
   "start": "electron index.js",
   "postinstall": "electron-rebuild"
  },
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment