electronでQRコードリーダーを作った

# 概要
PC上でQRコードをスクショして読み取るアプリがなかったのでelectronで作りました。

QRをsnipping toolなどでキャプチャ（コピーだけでいい）→アプリ起動→クリップボードを参照→QRの内容を表示
といった流れです。

QRの読み取りはjsqrというライブラリを使います。
electronのチュートリアルは割愛します。
# 動作
![usage.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2986051/231cd102-dd05-0963-c927-e5713f56ea14.gif)
# 環境
node v16.13.1
electron 21.3.1
jsqr 1.4.0
# ipc通信とpreloader
本記事では、ipc通信とpreload.jsによるAPIアクセスを利用しています。
[Electron ipc (プロセス間通信)　ipcMain, ipcRenderer　使い方](https://ichiri.biz/tech/electron-ipcmain-ipcrenderer/)
こちらの記事がすごくわかりやすかったのでおすすめです。
# コード全文
```js:main.js
const { app, BrowserWindow ,ipcMain , clipboard ,shell  } = require('electron');
const path = require('path');
const jsqr = require('jsqr');


const createWindow = () => {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })
  
  mainWindow.loadFile('index.html');
  mainWindow.setPosition(1000, 0);
  // mainWindow.webContents.openDevTools()

  mainWindow.webContents.setWindowOpenHandler(details =>{
    shell.openExternal(details.url);
    return { action: "deny" };
  });
  
  ipcMain.on('reload', () => {
    mainWindow.reload();
  })
  
}

app.whenReady().then(() => {
  createWindow();
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0)
      createWindow();
  })
})


app.on('window-all-closed', () => {
  if (process.platform !== 'darwin')
    app.quit();
})

ipcMain.handle('getImg', async (event, ...args) => {
  return clipboard.readImage().toPNG();
})
ipcMain.handle('getQrValue', async (event, data,width, height) => {
  let code = jsqr(data, width, height);
  if(code){
    return code.data;
  }else{
    return 0;
  }
})
```
```js:preload.js
const {contextBridge ,ipcRenderer} = require('electron');

contextBridge.exposeInMainWorld(
  'myApi', {
    getImg: async () => await ipcRenderer.invoke('getImg'),
    getQrValue: async (...args) => await ipcRenderer.invoke('getQrValue', ...args),
    reload: () => ipcRenderer.send('reload'),
})
```
```js:renderer.js
window.addEventListener('DOMContentLoaded', async() => {
  document.querySelector("button").addEventListener('click', ()=>{
    window.myApi.reload();
  })
  await setImg();
  setQrValue();
})
const handleErr = () => {
  const errorMsgEle = document.querySelector(".errorMsg");
  errorMsgEle.innerHTML = "エラー:画像をコピーしてください"
}
const setImg = async() => {
  const imgUrl = await window.myApi.getImg();
  const blob = new Blob([imgUrl], { type: "image/png" });
  const canvas = document.querySelector("canvas");
  const bitmap = await createImageBitmap(blob).catch(handleErr);
  canvas.width = bitmap.width;
  canvas.height = bitmap.height;
  canvas.getContext('2d')?.drawImage(bitmap, 0, 0);
  canvas.style = "border:3px dashed black";

  
}
  
const setQrValue = async() => {
  const canvas = document.querySelector("canvas");
  const ctx = canvas.getContext("2d");
  const img = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const qrValue = await window.myApi.getQrValue(img.data,img.width, img.height);
  const qrValueEle = document.querySelector(".qrValue a");
  if(qrValue != 0){
    qrValueEle.href = qrValue;
    qrValueEle.innerHTML = qrValue;
  }else{
    qrValueEle.innerHTML = "エラー:QRコードは1画像につき1枚まで";
  }
}
```
```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com; connect-src *; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src * blob:; media-src *">
    <title>QRreader</title>
  </head>
  <body>
    <div><button>リロード</button> 画像をクリップボードにコピーしてリロード</div>
    
    <div class="errorMsg" style="margin-top:1em;"></div>
    <p class="qrValue"><a target="_blank"></a></p>
    <canvas></canvas>
    <script src="./renderer.js"></script>
  </body>
</html>
```
# 解説
### main.js
```js
const { app, BrowserWindow ,ipcMain , clipboard ,shell  } = require('electron');
const path = require('path');
const jsqr = require('jsqr');
```
いつもの```app,BrowserWindow```の他に、ipc通信の```ipcMain```、clipboardから画像を取得する```clipboard```、electronのリンクを既定ブラウザで開くための```shell```を定義する。
```js
mainWindow.webContents.setWindowOpenHandler(details =>{
    shell.openExternal(details.url);
    return { action: "deny" };
  });
```
リンクがふまれたときの挙動を定義する。
今回はブラウザで開いたほうが都合が良いので、```shell.openExternal()```を使っています。
```return { action: "deny" };```はelectron側でもウィンドウを作ろうとするのを拒否するための処理です。
```js
ipcMain.on('reload', () => {
    mainWindow.reload();
  })
```
```js
ipcMain.handle('getImg', async (event, ...args) => {
  return clipboard.readImage().toPNG();
})
ipcMain.handle('getQrValue', async (event, data,width, height) => {
  let code = jsqr(data, width, height);
  if(code){
    return code.data;
  }else{
    return 0;
  }
})

```
ipc通信のハンドル登録をする。
レンダラー側からAPIを通じて呼ばれる側の処理を書いています。
上から、全体のリロード、クリップボードの画像をpngに変換して返す、qrの内容を返すといった内容です。
### preload.js
```js
const {contextBridge ,ipcRenderer} = require('electron');
```
ipc通信の```ipcRenderer```、API接続のための```contextBridge```を定義する。
```js
contextBridge.exposeInMainWorld(
  'myApi', {
    getImg: async () => await ipcRenderer.invoke('getImg'),
    getQrValue: async (...args) => await ipcRenderer.invoke('getQrValue', ...args),
    reload: () => ipcRenderer.send('reload'),
})
```
APIを登録する。
```invoke()```は非同期なのでasyncを付けます。
### renderer.js
```js
window.addEventListener('DOMContentLoaded', async() => {
  document.querySelector("button").addEventListener('click', ()=>{
    window.myApi.reload();
  })
  await setImg();
  setQrValue();
})
```
後述する```index.html```のボタンが押されたらAPI経由でリロードするので、イベントを登録しておく。
```setImg()```はrenderer側からクリップボードの画像をメインプロセスに要求し、その画像をcanvasに表示します。
```setQrValue()```はcanvasの画像の情報をメインプロセスに渡して、QRの内容を表示します。
```setImg()```が完了してから```setQrValue()```を呼びたいので```await```しておきます。(もしかしたら要らないかもしれない)
```js
const setImg = async() => {
  const imgUrl = await window.myApi.getImg();
  const blob = new Blob([imgUrl], { type: "image/png" });
  const canvas = document.querySelector("canvas");
  const bitmap = await createImageBitmap(blob).catch(handleErr);
  canvas.width = bitmap.width;
  canvas.height = bitmap.height;
  canvas.getContext('2d')?.drawImage(bitmap, 0, 0);
  canvas.style = "border:3px dashed black";  
}
```
API経由で```getImg()```を呼んでblobに変換、```createImageBitmap()```及び```drawImage()```でcanvasに画像を描画する。
```js
const handleErr = () => {
  const errorMsgEle = document.querySelector(".errorMsg");
  errorMsgEle.innerHTML = "エラー:画像をコピーしてください"
}
```
```createImageBitmap()```するときに、クリップボードに画像がないとerrorを吐くので、そのハンドリングをする。
```js
const setQrValue = async() => {
  const canvas = document.querySelector("canvas");
  const ctx = canvas.getContext("2d");
  const img = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const qrValue = await window.myApi.getQrValue(img.data,img.width, img.height);
  const qrValueEle = document.querySelector(".qrValue a");
  if(qrValue != 0){
    qrValueEle.href = qrValue;
    qrValueEle.innerHTML = qrValue;
  }else{
    qrValueEle.innerHTML = "エラー:QRコードは1画像につき1枚まで";
  }
}
```
canvasの画像の情報をメインプロセスの```getQrValue()```に渡して、QRの内容を表示する。
画像にQRがない or 2枚以上QRがある　と0が返ってくるので分岐しておきます。
### index.html
```html
    <div><button>リロード</button> 画像をクリップボードにコピーしてリロード</div>
```
リロードボタンと説明文
```html
    <div class="errorMsg" style="margin-top:1em;"></div>
```
エラーはいたときメッセージを格納するdiv
```html
    <p class="qrValue"><a target="_blank"></a></p>
```
qrの内容(今回の場合だとURL)を表示する。
aタグにしてクリックしたらブラウザで開くようにしました。```target="_blank"```じゃないとelectron内で開こうとするので注意してください。
# まとめ
zoomとかでQRを画面共有されたとき、いちいちスマホを探す手間をこのアプリで省ける　と思って作成しました。
electronは前に使ったことがありましたが、ipc通信のあたりとかめっちゃ変わっててわりと苦労しました。それでもelectronは簡単にデスクトップアプリ作れるのはいいですね。
QRjsを初めて使ったけど、すごく簡単にQRを処理できるのでjsでQR読みたいならこれ一択だなって思いました（そんな機会があるかはさておき）。2枚以上あると認識できないけど、画像処理ライブラリとかで分割できるかもしれないです。
クリップボードだけじゃなくて、ローカルに保存されてる画像とか、URLでアクセスできる画像とかも対応したほうがいいかもしれないですね。
# 参考にしたサイト
[Electron ipc (プロセス間通信)　ipcMain, ipcRenderer　使い方](https://ichiri.biz/tech/electron-ipcmain-ipcrenderer/)
[Electron入門 ~ Webの技術でつくるデスクトップアプリ](https://zenn.dev/sprout2000/books/6f6a0bf2fd301c/viewer/13263)
[ElectronでのIPCの例（send, sendSync, invoke, etc.）](https://blog.ikappio.com/electron-example-ipc-send-sendsync-invoke-etc/)
[jsQRであっさりQRコードリーダ/メーカ](https://zenn.dev/sdkfz181tiger/articles/096dfb74d485db)
[Electron の Webview で新規タブを開く場合もページ遷移させる](http://var.blog.jp/archives/86316642.html)
[[Electron] リンクをクリックすると標準Webブラウザで開く
](https://blog.katsubemakito.net/nodejs/electron/click-to-defaultbrowser)
# link
[QRreader Github](https://github.com/zannenkai/QRreader)