Mac用ゆっくり系ボイス再生アプリMYukkuriVoiceでユーザー辞書機能に対応した

https://github.com/taku-o/myukkurivoice
MYukkuriVoiceは、Mac用のゆっくりボイスを再生したり、録音したりする機能のあるアプリです。

ユーザー辞書機能を作ろうとして、
同系統のアプリ、SofTalk棒読みちゃんでは、自力で文章を字句解析して置き換えていたようなので、
自分もまた自力で作ろうとしていたが、
AquesTalkのライブラリで、Mac環境用の辞書作成機能が提供されたので、想定より楽に作れてしまった。


動画作成用のアプリなので、宣伝は動画で行うべきである(主張)。
https://www.nicovideo.jp/watch/sm34217643
https://www.nicovideo.jp/watch/sm34217643

続・WAVファイル。JavaScriptでwavファイルを分解する

前回、wavファイルのフォーマットを調べたので、
wavファイルをパースできるようになった。JavaScriptで。
http://d.hatena.ne.jp/taku-o/20181120/1542726865

import * as fs from 'fs';
const cTable = require('console.table');

fs.readFile('sample.wav', 'binary', (err, content) => {
  if (err) {
    console.error(err); return;
  }

  let offset = 0;
  let d = null;
  let data:any = null;
  const tables = [];
  let buffer = Buffer.from(content, 'binary');

  // 1-4 Chunk ID "RIFF"
  d = buffer.readUIntBE(offset, 4);
  data = Buffer.from(d.toString(16), 'hex').toString();
  offset += 4;
  tables.push({ position: '1-4', header: 'Chunk ID "RIFF"', data: data, });

  // 5-8 Chunk Size
  d = buffer.readUIntLE(offset, 4);
  data = d;
  offset += 4;
  tables.push({ position: '5-8', header: 'Chunk Size', data: data, });

  // 9-12  Format "WAVE"
  d = buffer.readUIntBE(offset, 4);
  data = Buffer.from(d.toString(16), 'hex').toString();
  offset += 4;
  tables.push({ position: '9-12', header: 'Format "WAVE"', data: data, });

  // 13-16 Subchunk1 ID "fmt "
  d = buffer.readUIntBE(offset, 4);
  data = Buffer.from(d.toString(16), 'hex').toString();
  offset += 4;
  tables.push({ position: '13-16', header: 'Subchunk1 ID "fmt "', data: data, });

  // 17-20 Subchunk1 Size "16"
  d = buffer.readUIntLE(offset, 4);
  data = d;
  offset += 4;
  tables.push({ position: '17-20', header: 'Subchunk1 Size', data: data, });

  // 21-22 Audio Format "1"
  d = buffer.readUIntLE(offset, 2);
  data = d;
  offset += 2;
  tables.push({ position: '21-22', header: 'Audio Format "1" PCM', data: data, });

  // 23-24 Num Channels
  d = buffer.readUIntLE(offset, 2);
  data = d;
  offset += 2;
  tables.push({ position: '23-24', header: 'Num Channels', data: data, });

  // 25-28 Sample Rate
  d = buffer.readUIntLE(offset, 4);
  data = d;
  offset += 4;
  tables.push({ position: '25-28', header: 'Sample Rate', data: data, });

  // 29-32 Byte Rate
  d = buffer.readUIntLE(offset, 4);
  data = d;
  offset += 4;
  tables.push({ position: '29-32', header: 'Byte Rate', data: data, });

  // 33-34 Block Align
  d = buffer.readUIntLE(offset, 2);
  data = d;
  offset += 2;
  tables.push({ position: '33-34', header: 'Block Align', data: data, });

  // 35-36 Bits Per Sample
  d = buffer.readUIntLE(offset, 2);
  data = d;
  offset += 2;
  tables.push({ position: '35-36', header: 'Bits Per Sample', data: data, });

  // 37-40 Subchunk2 ID "data"
  d = buffer.readUIntBE(offset, 4);
  data = Buffer.from(d.toString(16), 'hex').toString();
  offset += 4;
  tables.push({ position: '37-40', header: 'Subchunk2 ID "data"', data: data, });

  // 41-44 Subchunk2 Size
  d = buffer.readUIntLE(offset, 4);
  data = d;
  offset += 4;
  tables.push({ position: '41-44', header: 'Subchunk2 Size', data: data, });

  // 45-   Subchunk2 data

  console.table(tables);
});
  • 結果
position  header                data  
--------  --------------------  ------
1-4       Chunk ID "RIFF"       RIFF  
5-8       Chunk Size            131108
9-12      Format "WAVE"         WAVE  
13-16     Subchunk1 ID "fmt "   fmt   
17-20     Subchunk1 Size        16    
21-22     Audio Format "1" PCM  1     
23-24     Num Channels          1     
25-28     Sample Rate           44100 
29-32     Byte Rate             88200 
33-34     Block Align           2     
35-36     Bits Per Sample       16    
37-40     Subchunk2 ID "data"   data  
41-44     Subchunk2 Size        131072

パースした感想

良い勉強の題材でありました。

WAVファイルのフォーマットの確認

アプリで出力したWAVファイルが正しくないのではないか、という問題に遭遇して、
その際、バイナリファイルといろいろな資料を読みながら、
WAVファイルがどのような仕様になっているか調べたので、その時の調査メモ。

用意したもの

バイナリエディタと電卓。
大きな数値の項目はリトルエンディアンで、文字の項目はASCII。
16進数を10進数で読む。
なので、用意する電卓は↑の変換が出来るものでないと読めないですのぅ。

バイナリファイルを読む

実際に作ったWAVファイルをバイナリエディタで読んでみつつ、
どの位置にどのような値が入っているか確認。

WAVEファイル その1
bytes 定義 入っていた値 解読 説明、または検算
1-4 Chunk ID 52 49 46 46 RIFF "RIFF"固定
5-8 Chunk Size FC 72 00 00 29436 ファイルサイズ - 8bytes
ファイルサイズからChunk ID Chunk Size除いた分か
29444 - 8 = 29436
9-12 Format 57 41 56 45 WAVE "WAVE"固定
13-16 Subchunk1 ID 66 6D 74 fmt "fmt"固定
17-20 Subchunk1 Size 20 10 00 00 16 "16"固定。Subchunk1の長さ(21-36)なので
21-22 Audio Format 01 00 1 "1"固定。(PCMなら)
23-24 Num Channels 01 00 1 チャネル数
25-28 Sample Rate 40 1F 00 00 8000 サンプリングレート
29-32 Byte Rate 80 3E 00 00 16000 ブロックサイズ
SampleRate * NumChannels * BitsPerSample/8
(8000 * 1 * 16) / 8 = 16000
33-34 Block Align 02 00 2 1サンプルあたりのビット数
NumChannels * BitsPerSample / 8
(1 * 16) / 8 = 2
35-36 Bits Per Sample 10 00 16 8 bits = 8, 16 bits = 16
37-40 Subchunk2 ID 64 61 74 61 data "data"固定
41-44 Subchunk2 Size D8 72 00 00 29400 このチャンクのデータのサイズ
ここからファイルの最後までが波形データだから
ファイルサイズ - ここまでのサイズ(44)になるはず
29444 - 44 = 29400
45- data ここからファイルの最後まで波形データが入っている
WAVEファイル その2
bytes 定義 入っていた値 解読 説明、または検算
1-4 Chunk ID 52 49 46 46 RIFF "RIFF"固定
5-8 Chunk Size 24 00 02 00 131108 ファイルサイズ - 8bytes
131116 - 8 = 131108
9-12 Format 57 41 56 45 WAVE "WAVE"固定
13-16 Subchunk1 ID 66 6D 74 fmt "fmt"固定
17-20 Subchunk1 Size 20 10 00 00 16 "16"固定。
21-22 Audio Format 01 00 1 "1"固定。(PCMなら)
23-24 Num Channels 01 00 1 チャネル数
25-28 Sample Rate 44 AC 00 00 44100 サンプリングレート
29-32 Byte Rate 88 58 01 00 88200 ブロックサイズ
SampleRate * NumChannels * BitsPerSample/8
(44100 * 1 * 16) / 8 = 88200
33-34 Block Align 02 00 2 1サンプルあたりのビット数
NumChannels * BitsPerSample / 8
(1 * 16) / 8 = 2
35-36 Bits Per Sample 10 00 16 8 bits = 8, 16 bits = 16
37-40 Subchunk2 ID 64 61 74 61 data "data"固定
41-44 Subchunk2 Size 00 00 02 00 131072 このチャンクのデータのサイズ
131116 - 44 = 131072
45- data ここから波形データ

WAVEのバイナリ読んだ感想

読みやすいフォーマット。
アプリからPCMのWAVEファイルを出力すると仮定した時、
ファイルサイズの箇所以外は割と固定値になる。

RIFFの仕様

WAVEで使われているRIFF (Resource Interchange File Format)の仕様も見ておく。
なるほどなー。

bytes 定義 説明、または検算
1-4 Chunk ID ChunkのID
5-8 Chunk Size Chunkのサイズ - IDとSizeフィールドを除いた値
9- data チャンクのデータ。データのサイズが偶数で無いなら埋める

もし、WAVEファイルにiXMLチャンクを差し込むなら

WAVEファイルにiXMLチャンクという仕様があって、
ずっと、どうやってデータを入れれば良いのかサンプルが見つからなくて困っていたんだけど、
RIFFの仕様をみたら、分かってきた。
同じように入れれば良いのだろう。(試してない)

bytes 定義 入れる値 解読 説明、または検算
1-4 Chunk ID 69 58 4D 4C iXML "iXML"固定
5-8 Chunk Size iXMLのデータサイズ - 8
9- data iXMLのデータ


chunkのdataに入れるデータは次のようなものになるだろう。
role nameの所以外は固定になりそう?

<?xml version="1.0" encoding="UTF-8"?>
<BWFXML>
    <IXML_VERSION>2.02</IXML_VERSION>
    <TRACK_LIST>
        <TRACK_COUNT>1</TRACK_COUNT>
        <TRACK>
            <CHANNEL_INDEX>1</CHANNEL_INDEX>
            <INTERLEAVE_INDEX>1</INTERLEAVE_INDEX>
            <NAME>role name</NAME>
        </TRACK>
    </TRACK_LIST>
</BWFXML>

↓実際に入れるデータ

&lt;?xml version="1.0" encoding="UTF-8"?&gt;&lt;BWFXML&gt;&lt;IXML_VERSION&gt;2.02&lt;/IXML_VERSION&gt;&lt;TRACK_LIST&gt;&lt;TRACK_COUNT&gt;1&lt;/TRACK_COUNT&gt;&lt;TRACK&gt;&lt;CHANNEL_INDEX&gt;1&lt;/CHANNEL_INDEX&gt;&lt;INTERLEAVE_INDEX&gt;1&lt;/INTERLEAVE_INDEX&gt;&lt;NAME&gt;role name&lt;/NAME&gt;&lt;/TRACK&gt;&lt;/TRACK_LIST&gt;&lt;/BWFXML&gt;


結論。ダメだった。BWFの仕様に合わせる必要があって、
他にも入れないとイケナイデータがある。
それと、Chunkの順番が決まっているようだ。

マルチなウィンドウのElectronアプリで、メニュー制御する話

Electronのアプリで、ウィンドウ毎に、メニューを切り替えたいとか、
有効無効を切り替えたいことってあると思うんです。

今回はフォーカスが当たっている時だけ、メニューを有効にする方法の話。

メニューアイテムのenabledを切り替える

メニューの有効無効を切り替えるには、
メニューのアイテムのenabled属性を切り替えれば良し。


メニューを作るときに、id属性を設定しておいて。

import {Menu} from 'electron';

const menuList = [
  {
    ... (略) ...
  },
  {
    label: '辞書',
    submenu: [
      {
        id: 'dict-close', // ←←← idを設定
        enabled: false,
        label: '辞書を閉じる (&#8984;W)',
        click() { if (myApp.dictWindow) { myApp.dictWindow.close(); } },
      },
      {
        id: 'dict-tutorial', // ←←← idを設定
        enabled: false,
        label: '辞書チュートリアル',
        click() { if (myApp.dictWindow) { myApp.dictWindow.webContents.send('menu', 'tutorial'); } },
      },
      {type: 'separator'},
      {
        id: 'dict-add', // ←←← idを設定
        enabled: false,
        label: '定義データ追加 (&#8984;N)',
        click() { if (myApp.dictWindow) { myApp.dictWindow.webContents.send('menu', 'add'); } },
      },
    ]
  },
  {
    ... (略) ...
  },
];
const menuTemplate = Menu.buildFromTemplate(menuList);
Menu.setApplicationMenu(menuTemplate);

そのidでメニューを取り出して、enabled属性を切り替える。

import {Menu} from 'electron';

const dictMenuItems = [
  'dict-close',
  'dict-tutorial',
  'dict-add',
  'dict-delete',
  'dict-save',
  'dict-cancel',
  'dict-export',
  'dict-reset',
];

const menu = Menu.getApplicationMenu();
for (let m of dictMenuItems) {
  const item = menu.getMenuItemById(m);
  item.enabled = true; // ←←← enabledを切り替え
}

フォーカスされたらメニューを有効に、フォーカスが外れたらメニューを無効に

BrowserWindowがフォーカスされている時だけメニューを有効にするには、
focus、blur、それとcloseのイベントを拾う。
closeも拾って置かないと、フォーカスされた状態でウィンドウ閉じられると、
ウィンドウ閉じてるのにメニューが有効なままになる。

this.dictWindow.on('close', () => {
  disableDictMenu(); // ←←← イベントを拾って、メニューのenabledを切り替える
});
this.dictWindow.on('focus', () => {
  enableDictMenu(); // ←←← イベントを拾って、メニューのenabledを切り替える
});
this.dictWindow.on('blur', () => {
  disableDictMenu(); // ←←← イベントを拾って、メニューのenabledを切り替える
});


ウィンドウまわりのコードも含めると。

... (略) ...

function showDictWindow(): void {
  const acceptFirstMouse = this.appCfg.acceptFirstMouse;
  this.dictWindow = new BrowserWindow({
    width: width,
    height: height,
    show: false, // show at did-finish-load event
  });
  this.dictWindow.loadURL(`file://${__dirname}/contents-dict.html`);

  // window event
  this.dictWindow.webContents.on('did-finish-load', () => {
    myApp.dictWindow.show(); myApp.dictWindow.focus();
  });
  this.dictWindow.on('close', () => {
    disableDictMenu();
  });
  this.dictWindow.on('focus', () => {
    enableDictMenu();
  });
  this.dictWindow.on('blur', () => {
    disableDictMenu();
  });
}

メニューのacceleratorが邪魔な話

メニューアイテムにacceleratorを設定していると、
そのショートカットキーは、全てのウィンドウで効いてしまう。

import {Menu} from 'electron';

const menuList = [
  {
    ... (略) ...
  },
  {
    label: '辞書',
    submenu: [
      {
        role: 'quit',
        accelerator: 'Command+Q', // ←←← acceleratorってこれ
      },
    ]
  },
  {
    ... (略) ...
  },
];
const menuTemplate = Menu.buildFromTemplate(menuList);
Menu.setApplicationMenu(menuTemplate);

これが邪魔なら、なんらかの対処をいれましょう。

acceleratorを無効にするか、
有効にしたい時以外はacceleratorを切る。

終わり

オワリダヨ

Electronアプリで、アプリアイコンにドロップされたファイルを処理する

Finder上のアプリアイコンにファイルをドロップされた時、
Dock上のアプリアイコンにファイルをドロップされた時、
Electron製のアプリで、そのファイルを受け取って処理する機能の実装方法です。

(Macの話だったり、私製アプリのコードの断片が入ってたり、
Typescriptだったり、パラメータが雑だったりするけど、うまく読み替えて欲しい。)


アプリのデータの持ち方にもよりますけど、
・アプリが起動中にファイルをドロップする
・アプリが起動していない時にファイルをドロップする
で、処理方法を変える必要があると思う。

Info.plistでアプリがファイルを受け取れるように設定する

  • Mac環境の話
  • ファイルをアプリアイコンに落とした時に反応するように、アプリのInfo.plistを更新します。
  • 次のようなplistファイルを作って、アプリのInfo.plistに組み込みます。
  • この設定を入れないと、ファイルをドロップしても、アプリが反応しない。
  • extend.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>CFBundleDocumentTypes</key>
    <array>
      <dict>
        <key>CFBundleTypeName</key>
        <string>All Files</string>
        <key>LSHandlerRank</key>
        <string>Owner</string>
        <key>LSItemContentTypes</key>
        <array>
          <string>public.text</string>
          <string>public.data</string>
          <string>public.content</string>
        </array>
      </dict>
    </array>
  </dict>
</plist>
  • このextend.plistを、electron-packagerコマンド実行時に組み込んで、Info.plistにパラメータを追加します。
  • extend-infoで指定します。
electron-packager . YourAppName --platform=darwin --arch=x64 --extend-info=extend.plist



ファイルドロップのイベントを受け取る

  • "will-finish-launching"のタイミングで、"open-file"イベントをハンドリングする処理を登録します。(ここより前で拾うとマズイのかな?試さないけど)
  • main processのjavascript内に実装します。
  • preventDefault()した後に、ファイルをハンドリングする処理を入れます。
import {app} from 'electron';

// receive drop file to app icon event
let launchArgs = null;
app.on('will-finish-launching', () => {

  app.on('open-file', (event, filePath) => {
    event.preventDefault();

    if (myApp.mainWindow) {
      myApp.mainWindow.webContents.send('dropTextFile', filePath);
    } else {
      launchArgs = { filePath: filePath };
    }
  });

});

...(略)...


アプリアイコンにファイルをドロップする際、

  • アプリが起動済みである場合と、
  • アプリがまだ起動していない場合がありますよね。


アプリが起動済みで、BrowserWindowのインスタンスが既に作られているのであれば、
このようなファイルが渡されたよ、とイベントを飛ばせば事足ります。

if (myApp.mainWindow) {
  myApp.mainWindow.webContents.send('dropTextFile', filePath);
}

BrowserWindowインスタンス初期化時に、ファイルの情報を渡す

  • 起動していないアプリに、ファイルがドロップされた場合は、BrowserWindowのインスタンスはまだ作られていません。
  • ウィンドウのインスタンスが出来てから、イベントを投げて、ファイルの情報を渡してあげましょう。
  • (何らかの変数越しにパラメータを渡しても良いですけれど)
function showMainWindow(launchArgs): void {
  const myApp = this;
  let _launchArgs = launchArgs;

  mainWindow = new BrowserWindow({
    width: 800,
    height: 800,
    x: x,
    y: y,
    show: false, // show at did-finish-load event
  });
  mainWindow.loadURL(`file://${__dirname}/contents-main.html`);

  // main window event
  mainWindow.webContents.on('did-finish-load', () => {
    // receive drop file to app icon event
    if (_launchArgs && _launchArgs.filePath) {
      const filePath = _launchArgs.filePath; _launchArgs = null; // for window reload
      mainWindow.webContents.send('dropTextFile', filePath);
    }

    // show
    mainWindow.show();
    mainWindow.focus();
  });
  • "did-finish-load"のタイミングで、BrowserWindowインスタンスにイベントを投げています。
  • ドロップされたファイルの情報をクリアしておかないと、BrowserWindowを再読込した時に、また読み込まれてしまいますよ。
// main window event
mainWindow.webContents.on('did-finish-load', () => {
  // receive drop file to app icon event
  if (_launchArgs && _launchArgs.filePath) {
    const filePath = _launchArgs.filePath;
    _launchArgs = null;      // リロードされた時のために変数の中身を消しておく
    mainWindow.webContents.send('dropTextFile', filePath);
  }

  // show
  mainWindow.show();
  mainWindow.focus();
});

renderer processに渡されたファイルを読み込む

  • renderer processで渡されたファイルを読み込みます。
  • 起動済みのアプリにイベントを投げられた場合、起動前のアプリにイベントを投げられた場合で、どちらも同じインターフェイスで処理出来ると良いですね。
  • ここではipcRendererを使って処理しています。
// dropTextFile event
ipcRenderer.on('dropTextFile', (event, filePath: string) => {
  fs.readFile(filePath, 'utf-8', (err: Error, data: string) => {
    if (err) {
      MessageService.error('テキストファイルを読み込めませんでした。', err);
      return;
    }

    const win = remote.getCurrentWindow();
    win.focus();
    $scope.yinput.source = data;
    $timeout(() => { $scope.$apply(); });
  });
});
  • アプリアイコンにファイルを落とした時は、Electronアプリにはフォーカスが当たっていません。
  • このままでは、Electronアプリの画面のデータが更新されない、などの問題が起きます。
  • focus()を実行して、ウィンドウにフォーカスを当てましょう。
var remote = require('electron').remote;

const win = remote.getCurrentWindow();
win.focus();

makeSingleInstance

  • アプリが複数立ち上がっていると、うまく動かない場合があると思います。
  • (そういう記述を見た。いちいち検証しないよ。私のアプリではシングルインスタンスを有効にしないしね。)
  • makeSingleInstance()を実行して、アプリを複数起動できないようにして対処します。
app.makeSingleInstance((argv: string[], workingDirectory: string) => {});
  • 最新のバージョンでは、makeSingleInstance()は廃止され、代わりにrequestSingleInstanceLock()を使うことになりました。
  • https://electronjs.org/docs/api/app
const locked = app.requestSingleInstanceLock()

おわり

おわりだよ

Electronアプリは、まずelectron-localshortcut入れて、Command+R、Command+Wを潰すのがセオリーだと思う

おまえ、セオリー語れるほど、Electronやってないだろう。

globalShortcut

Electronにはショートカットキーを制御するglobalShortcutという機能があるんですけど、
これは公式ドキュメントにも書いてあるとおり、グローバルに効いてしまう。

アプリにフォーカスが当たって無くても、効いちゃうんですね。
これはこれで便利だけど、効き過ぎちゃう。

こんなCommand+Sを封じるアプリとか作ったら、あらゆるアプリのCommand+Sを封じちゃって大変ですね。
やってはいけませんよ。

'use strict';
const electron = require('electron');
const app = electron.app;
const globalShortcut = electron.globalShortcut;

app.on('ready', function() {
  globalShortcut.register('Command+S', function() {                                                    
    // アプリが立ち上がっている間、Command+S による保存を封じるぞ
    // 嫌がらせかな?
    console.log('Command+S is pressed')
  })
});

electron-localshortcut

ということで、electron-localshortcutです。
アプリにショートカットキーをつけたいならこちらです。

npm install --save electron-localshortcut

BrowserWindowインスタンスを生成したあと、
ウィンドウ毎にショートカットキーを登録する。
昔はウィンドウ指定できなかった。今はショートカットキーを有効にするウィンドウを指定する仕様になった。

var electron = require('electron');
var app = electron.app;
var localShortcut = require("electron-localshortcut");
var mainWindow;

...(中略)...

mainWindow = new BrowserWindow({
  width: 600,
  height: 600,
  acceptFirstMouse: true,
  show: false, // show at did-finish-load event
});
mainWindow.loadURL(`file://${__dirname}/window-main.html`);

// ショートカットキー
// Command+Qでアプリを閉じる
localShortcut.register(mainWindow, 'Command+Q', function() {
  app.quit();
});

// event
mainWindow.webContents.on('did-finish-load', function() {
  mainWindow.show();
  mainWindow.focus();
});

アプリらしく

で、今回のタイトルです。
Electronのアプリを作ると、
Command+R で画面のリロード、
Command+W でウィンドウを閉じる機能が動いてしまう。
ウェブブラウザに元々ついている機能です。


アプリにもよりますけど、自分はこれ、格好悪いと思う。
とてもブラウザブラウザ(?)してる。

なので、electron-localshortcut で、これのショートカットキーを上書きして、
機能にしないようにしておくべきなのではないか。そう思うのです。

localShortcut.register(mainWindow, 'Command+R', function() {
  // do nothing
});
localShortcut.register(mainWindow, 'Command+W', function() {
  // do nothing
});

おわり

おわりです。
なんかMac前提の話してるけど、細かいことは気にしない!!!

画像をElectron側にキャッシュしておく話

Electronを業務で使っている会社とかあるらしい。
で、自分も活用してみよう。Electronでどんなアプリを作るかって考えたら、

サーバーのデータを参照するElectronアプリも作れるけれど、
でも、それだったら、Webページでも良いわけで、
Electronを使うなら、やっぱりスタンドアローンなアプリにしたいじゃないですか。

しかし、業務で扱うデータって、残念ながら、だいたいサーバー上に置いてある。
そのデータに触れずに業務に役立つアプリを作ることは難しい。

それじゃ、こういう、ローカルとサーバ上、両方にデータがあるElectronアプリにするのが良いのかな、と。

そこで画像キャッシュだ

なぜ、画像かはわからないが、今回は画像だ。
毎回データをサーバーに取りに行かずに、(作るのが重い画像とかだ)
Electronのアプリ内部でキャッシュしておこう、という話。

実装

  • まず最初にimage-cacheを入れておいて、
npm install --save image-cache
  • cacheに画像を渡す処理。cacheから画像を取り出す処理を入れる。
# renderer.js
var imageCache = require('image-cache');

...(中略)...

// tweet.user_image は 画像URL
//
// not found image cache
if (! imageCache.isCachedSync(tweet.user_image)) {
  imageCache.setCache(tweet.user_image, function(error) {
    log.error(error);
  });
  tweet.user_image_link = tweet.user_image;

// found image cache
} else {
  const img = imageCache.getCacheSync(tweet.user_image);
  tweet.user_image_link = img.data;
}
  • angular.jsだけど
  • image-cacheから取り出したcacheは、BASE64な文字になっている。そのままそれを表示して良い。
# renderer.html
<li ng-repeat="item in streams track by item.idx">
  <img ng-src="{{item.user_image_link}}" width="32" height="32">
</li>

まとめ

Electronあまり関係ない話になってしまった。