Electronでローカル画像ファイルを表示する

# 概要
Electronのアプリケーションを開発中に、埋め込み画像を表示しようとして「Not allowed to load local resource」というエラーに遭遇したので、その対策をメモしておきます。

パッケージ化すると対策を打たなくても画像は表示されるかもしれません。
検証したら後日追記すると思います。

結論から言うと、fileプロトコルをやめて、`<img src="data:image/png;base64,<画像をbase64化した文字列>"/>` を使って解決しました。

# 相対パスでの指定の問題（別の問題が発生するため、後述の対策を実施）
Electronアプリは、Reactを使っていてcreate-react-appで作ったアプリで、それをElectron化したものです。

index.htmlの横に、logo.pngを配置、<img>タグで、`<img src="../build/logo.png"/>`として表示させようとしていました。

画像は、Webアプリとしては表示されますが、Electron上では`src="file:///build/logo.png"`となり、絶対パスで探すようになります。

この対策として、以下のサイトを参考に、`protocol.interceptFileProtocol` というAPIで、"file:///%RELATIVE_PATH%" (RELATIVE_PATHはマジックワード）で始まっていれば、相対パスとしてハンドルするよう実装しました。

- [Electronで絶対パスの基点をアプリケーションがあるディレクトリにする](https://qiita.com/tuttieee/items/4123784f1eb68a1d3925)  
  https://qiita.com/tuttieee/items/4123784f1eb68a1d3925

imgタグの指定は、`<img src="%RELATIVE_PATH%../build/logo.png" />`とし、ソースはこんな感じです。

```javascript
protocol.interceptFileProtocol('file', (req, callback) => {
    const requestedUrl = req.url.substr(7);

    if (path.startsWith("/%RELATIVE_PATh%") {
      callback(path.normalize(path.join(__dirname, requestedUrl.substring("/%RELATIVE_PATh%".length)));
    } else {
      callback(requestedUrl);
    }
```

この対策は、Macでは動作しましたが、Windowsでは「Not allowed to load local resource」というエラーに遭遇し、動作しませんでした。

# Not allowed to load local resource 対策（解決済み）
Googleでいくつか調べましたが、WebsecurityをOffにする、create-react-appをeject（不可逆の操作で、create-react-appの良さが失われる）したり、webpackのコンフィグを変えるなどといったもので、適用しても効果がありませんでした。

- 一番有効そうだった解決策（WebsecurityをOffにする）　  
 https://github.com/electron/electron/issues/5107#issuecomment-299971806
- Webpackの設定変更　   
  https://github.com/electron/electron/issues/5107

結局は、以下に引用した https://stackoverflow.com/questions/50272451/electron-js-images-from-local-file-system に書かれているように、fileプロトコルを使っている限りセキュリティをOffにするなどの解決策しかなく、問題は解決しませんでした。

Electron by default allows local resources to be accessed by render processes only when their html files are loaded from local sources with the file:// protocol for security reasons.

If you are loading the html from any http:// or https:// protocol even from a local server like webpack-dev-server, access to local resources is disabled.

If you loading html pages from a local server only during development and switching to local html files in production, you can disable websecurity during development, taking care to enable it in production.


# 最終的な解決策
``<img src="data:image/png;base64,<画像をbase64化した文字列>"/>` というやり方で解決しました。

解決した後のフォルダ構成は、以下の通りです。

```
  build/ <= publicとsrcのビルド結果が格納される。
  public/index.html
         logo.png
  src/ ...
       libs/ImageLoader.js
       main.js <= ElectronのMainプロセスの起動ファイル
```

解決のためのステップは以下の通りです。
1. main.jsで、imagesフォルダ以下のpng/jpgファイルをbase64化し、globalの変数に詰める。
2. ImageLoader.jsクラスを作成し、srcに渡す文字列を生成する。
3. jsxで、imgタグに、step2で生成した文字列を渡す。

ソースは以下の通り。

* 画像ファイルを捜査、globalマップに詰めるところまで。

```javascript:main.js
const {app, BrowserWindow, ipcMain} = require('electron')
const path = require('path')
const url = require('url')
const fs = require('fs')
const info = {
  win: null,
  images: {}
}

global.info = info

let win
function createWindow () {
  win = new BrowserWindow({width: 800, height: 600, resizable:true})
  info.win = win
  
  // images以下のフォルダを捜査
  const files = fs.readdirSync(path.join(__dirname, '/../build/images'))
  files.forEach((file)=>{
    const ext = path.extname(file)
    if(ext === '.png' || ext === '.jpg') {
      const binary = fs.readFileSync(path.join(__dirname, '/../build/images/' + file))
      // base64化
      const base64data = new Buffer(binary).toString('base64')
      // マップに詰める
      info.images[file] = {
        type: ext.substring(1),
        name: file,
        data: base64data
      }
    }
  })
  // 通常のElectronのメイン処理
  win.loadURL(url.format({
    pathname: path.join(__dirname, '/../build/index.html'),
    protocol: 'file:',
    slashes: true
  }))
  // 以下省略・・・
```

* ImageLoader.js（グローバルマップから、data:image/png,base64,... を生成）

```javascript:ImageLoader.js
const electron = window.require('electron')
//globalを取得するには、electron.remoteで取得する
const remote = electron.remote

class ImageLoader {
  static loadSrc = (imagePath) => {
    const info = remote.getGlobal('info')
    const image = info.images[imagePath]
    for(let key in info.images) {
      console.log(key + " " + info.images[key].type + " " + info.images[key].name)
    }
    if(image === undefined)
      return "image not found"
    // data:image/<png|jpg>;base64,<base64 encoded data>の生成
    return `data:image/${image.type};base64,${image.data}`
  }
}

export default ImageLoader
```

* 画像表示させるところ

```javascript:App.jsx
import React from 'react'
import ImageLoader from '../libs/ImageLoader'
/* アプリケーション画面の定義 */
const App = () => (
  <div>
    <img src={ImageLoader.loadSrc('logo.png')} height="64px" width="64px"/>
  // 以下省略・・・
```

# さいごに
対策の備忘録として記載しています。
他の方法もあるかもしれませんので、ご存知でしたらお知らせいただければ幸いです。