【kintoneカスタマイズ】 QRコードリーダーを kintone に実装してみた

![Static Badge](https://img.shields.io/badge/MacOS-13.5.2-brightgreen)　![Static Badge](https://img.shields.io/badge/Last%20Modified-2020%2F10%2F25-brightgreen)

## はじめに
この記事ではQRリーダーを kintone カスタマイズで実現する過程を紹介しています。

## 対象者
kintone アプリ上にカメラ映像を出力したい方/QRリーダーを実装したい方

## 何を作ったのか
kintone カスタマイズでQRコードを使った受付システムを構築しました。
今回はその中でも kintone 上で QRリーダーを実装する手順についてです。
[jsQR](https://github.com/cozmo/jsQR) というJavaScriptのライブラリを使ってQR読み取りを実装しました。

全体としては、フェイスカメラのついているノートPCのカメラを使用して、取り込んだ映像をフレームごとcanvasに描画し、フレーム内にあるQRコードをデコードする流れです。

## 開発に必要なもの
- kintone
- QRコード (お手持ちのスマホで何かしらのQRを表示できればOK)

## 解説
### アプリ構成
| フィールド名 | フィールド形式 | フィールドコード | 備考 |
| :---: | :---: | :---: | :--- |
| - | スペース | preview | canvas 要素を追加するためのフィールド |
| 内容 | 文字列(1列) | 内容 | デコードした中身を入れるフィールド |



### 手順
#### canvasにフェイスカメラの映像を描画する
ではまず、カメラをJavaScriptから起動して、canvas上に描画してみましょう。

```index.js
(async () => {
  'use strict';

  //カメラの設定
  const constraints = {
    video: {
      width: 1280,
      height: 720,
      aspectRatio: 1.78, //アスペクト比
      facingMode: 'user', //フェイスカメラの向き
    },
  };
  const video = document.createElement('video');
  const canvas = document.createElement('canvas');
  canvas.width = constraints.video.width;
  canvas.height = constraints.video.height;
  //canvas.style.transform = 'scale(-1, 1)';  // 画面を左右反転する

  const context = canvas.getContext('2d');

  kintone.events.on('app.record.detail.show', async (event) => {
    //kintoneのスペースフィールドに出すならスペースフィールドを設置して要素を追加する
    kintone.app.record.getSpaceElement('canvas').appendChild(canvas);
  
    const stream = await navigator.mediaDevices.getUserMedia(constraints);
    video.srcObject = stream;
    video.play();
    const loop = async () => {
      const repeat = requestAnimationFrame(loop);
      //メディアの準備状態が十分でなければリターン
      if (video.readyState !== video.HAVE_ENOUGH_DATA) return;
      
      context.drawImage(video, 0, 0, constraints.video.width, constraints.video.height);
    };
    requestAnimationFrame(loop);
  });
})();
```

中身を解説すると、`video`のデータ、つまり映像を1秒間に60回canvasに描画しています。
`requestAnimationFrame`とは、画面のリフレッシュレート(筆者の環境では60Hz)で繰り返し処理を行う関数です。
なので、今回は`loop`という関数が1秒間に60回繰り返されます。
でも、canvasは描画するためにある要素であり、一枚の紙に絵を描くようなものなのです。
ではなぜ映像が出ているのかというと、カメラの映像をフレームに切り分けて、1秒間に60回`context.drawImage()`することで映像になっているのです。
ご存じでない方のためにお伝えすると、動画(映像)は写真を次から次に連続で再生することで映像になっているため、一枚の写真を描画する処理を高速で行なっているのです。
そのため、あたかも映像のように見えていたんです。

#### jsQRと組み合わせる
ここで重要な点としては、jsQRに読ませるデータは **[ImageDataオブジェクト](https://developer.mozilla.org/ja/docs/Web/API/ImageData)**という点です。
カメラ映像を canvas に描画した際に、 canvas が持っているImageDataオブジェクトには`Uint8ClampedArray`というRGBAの画素値を配列にしたものが含まれています。この情報にQRコードが含まれていたらjsQRで解読できるんです。

それでは、先ほどの`loop`の中の処理に下記の処理を追加してみます。

```index.js
// 読み込んだフレームをデコード(ライブラリ "jsQR" を使ってデコード)
const imageData = context.getImageData(0, 0, constraints.video.width, constraints.video.height);
const code = jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: 'dontInvert' });
if (!code || code.data === '') return;
console.log(code.data);
```

![いわちゃんisCool.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2949201/93830105-a031-bafd-4942-448493422f14.gif)

追加したコードでは、`context.getImageData()`を使ってcontext内のImageDataオブジェクトを取得し、`jsQR()`に渡すことでQRコードを解読できます。

しかし、動画でもわかるように `console.log(code.data)`が何度も実行されていることがわかるため、不要な処理が実行されないように制御する必要があります。
ここからはどう実装したいかによって変わってきますが、今回は「デコードできたら処理を止めて(モーダルウィンドウ)、モーダルから戻ったらスキャンを再開する」と言う流れを実装していきます。

##### モーダルライブラリ: sweetalert2
今回はモーダルウィンドウに [SweetAlert2](https://sweetalert2.github.io/) を使用します。([Cybozy CDN](https://cybozu.dev/ja/kintone/sdk/library/cybozu-cdn/)からも配信されています)
理由は簡単にモーダルを出して、クリックの値などを簡単に受け取れるためです。
特に今回の受付のQRコード化のような実装にはピッタリのツールです。

まずは、 kintone にライブラリを設定してください。
そして、下記のようなモーダルを準備します。
```.js
Swal.fire({
	icon: 'success',
	title: 'スキャンしました'
})
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2949201/73e0d6cc-193c-6cc0-f33d-af1bcea8e9f0.png" width="400">


#### 一度だけ読み取る処理
`loop`関数に下記のコードを追加します。

```.js
cancelAnimationFrame(repeat);
await Swal.fire({
	icon: 'success',
	title: 'スキャンしました',
});
requestAnimationFrame(loop);
```

#### kintone と組み合わせる
デコードできた QR コードを文字列１行フィールドに入れて保存してみます。
今回は、kintone のイベントは `['app.record.create.show', 'app.record.edit.show']` でスキャンしたら保存するという流れを想定しています。

sweetalert のコードを下記に変更し、OKを押したときは[内容]フィールドに反映、CANCELを押したときは反映しないコードに書き換えます。

```index.js
cancelAnimationFrame(repeat);
const nowRecord = kintone.app.record.get(); // 現在のレコードの情報を取得する
const result = await Swal.fire({
	icon: 'success',
	title: '内容に反映しますか？',
	showCancelButton: true,
}).then((result) => result.isConfirmed);
if (result) {
	nowRecord.record.内容.value = code.data; // [内容]にデコードした内容を代入
	kintone.app.record.set(nowRecord); // 変更を kintone に反映させる
}
requestAnimationFrame(loop);
```

### コード全体
```index.js
(() => {
  'use strict';

  //カメラの設定
  const constraints = {
    video: {
      width: 1280,
      height: 720,
      aspectRatio: 1.78, //アスペクト比
      facingMode: 'user', //フェイスカメラの向き
    },
  };
  const video = document.createElement('video');
  const canvas = document.createElement('canvas');
  canvas.width = constraints.video.width;
  canvas.height = constraints.video.height;
  //canvas.style.transform = 'scale(-1, 1)';  // 画面を左右反転する

  const context = canvas.getContext('2d');

  kintone.events.on(
    ['app.record.create.show', 'app.record.edit.show'],
    async (event) => {
      kintone.app.record.getSpaceElement('preview').appendChild(canvas);

      const stream = await navigator.mediaDevices.getUserMedia(constraints);
      video.srcObject = stream;
      video.play();
      const loop = async () => {
        const repeat = requestAnimationFrame(loop);
        //メディアの準備状態が十分でなければリターン
        if (video.readyState !== video.HAVE_ENOUGH_DATA) return;

        context.drawImage(video, 0, 0, constraints.video.width, constraints.video.height);
        const imageData = context.getImageData(0, 0, constraints.video.width, constraints.video.height);
        const code = jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: 'dontInvert' });

        if (!code || code.data === '') return;
        cancelAnimationFrame(repeat);
        const nowRecord = kintone.app.record.get();
        const result = await Swal.fire({
          icon: 'success',
          title: '内容に反映しますか？',
          showCancelButton: true,
        }).then((result) => result.isConfirmed);
        if (result) {
          nowRecord.record.内容.value = code.data;
          kintone.app.record.set(nowRecord);
        }
        requestAnimationFrame(loop);
      };
      requestAnimationFrame(loop);
    }
  );
})();
```

### 最後に
今回の実装では受付QRコード化の一部だったのですが、実際には読み取った後に kintone REST API を使って kintone にある受付名簿アプリのような他のアプリを更新する処理が存在します。
REST API 周りは他の方がたくさん記事を書いているので、そちらを参考に上記のコードをいじってもらえればと思います！

### 参考記事
- [jsQRを使ってブラウザQRコードリーダーを作る【技術メモ】【小ネタ】](https://qiita.com/U_sagi/items/12cc39487a863e0136a0)