便利ページ：JavascriptでQRコードスキャン

久しぶりの、「[便利ページ：Javascriptでちょっとした便利な機能を作ってみた](https://qiita.com/poruruba/items/6c67c7eef34ed302feb3 )」 のシリーズものです。

今回は、QRコードスキャンです。
QRコード生成はすでに実装してあったのですが、スキャンする方はありませんでした。

HTML5では、PCに接続されたカメラを扱うことができますので、ブラウザだけでスキャンできます。また、AndroidやiPhoneでのChromeでも動作するので、これでPCだけでなくスマホでも動きます。

いつもの通りGitHubにも上げてあります。
　https://github.com/poruruba/utilities

参考までに、以下にデモとしてアクセスできるようにしてあります。
　https://poruruba.github.io/utilities/

(2020/2/2 修正)
・画像サイズを300px固定ではなく、カメラ画像のサイズに合わせるようにしました。

# QRコードスキャンのためのライブラリ

以下を使わせていただきました。ありがとうございます。

cozmo/jsQR
　https://github.com/cozmo/jsQR 

HTMLで以下のようにスクリプトをロードしておきます。
　`<script src="dist/js/jsQR.js"></script>`

# ソースコード抜粋

肝心のJavascriptのソースコードです。重要部分のみ抜粋しています。

```js:start.js
        qrcode_scan: function(){
            this.qrcode_video = $('#qrcode_camera')[0];
            this.qrcode_canvas = $('#qrcode_canvas')[0];

            if( this.qrcode_running ){
                this.qrcode_forcestop();
                return;
            }

            this.qrcode_running = true;
            this.qrcode_btn = 'QRスキャン停止';

            this.qrcode_timer = setTimeout(() =>{
                this.qrcode_forcestop();
            }, QRCODE_CANCEL_TIMER);

            return navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" }, audio: false })
            .then(stream =>{
                this.qrcode_scaned_data = "";
                this.qrcode_video.srcObject = stream;
                this.qrcode_draw();
            })
            .catch(error =>{
                alert(error);
            });
        },
        qrcode_draw: function(){
//            console.log(this.qrcode_video.videoWidth, this.qrcode_video.videoHeight);
            if( this.qrcode_context == null ){
                if( this.qrcode_video.videoWidth == 0 || this.qrcode_video.videoHeight == 0 ){
                    if( this.qrcode_running )
                        requestAnimationFrame(this.qrcode_draw);
                    return;
                }
                this.qrcode_canvas.width = this.qrcode_video.videoWidth;
                this.qrcode_canvas.height = this.qrcode_video.videoHeight;
                this.qrcode_context = this.qrcode_canvas.getContext('2d');
            }
            this.qrcode_context.drawImage(this.qrcode_video, 0, 0, this.qrcode_canvas.width, this.qrcode_canvas.height);
            const imageData = this.qrcode_context.getImageData(0, 0, this.qrcode_canvas.width, this.qrcode_canvas.height);

            const code = jsQR(imageData.data, this.qrcode_canvas.width, this.qrcode_canvas.height);
            if( code && code.data != "" ){
                this.qrcode_scaned_data = code.data;
                console.log(code);
                
                this.qrcode_forcestop();

                this.qrcode_context.strokeStyle = "blue";
                this.qrcode_context.lineWidth = 3;
                
                var pos = code.location;
                this.qrcode_context.beginPath();
                this.qrcode_context.moveTo(pos.topLeftCorner.x, pos.topLeftCorner.y);
                this.qrcode_context.lineTo(pos.topRightCorner.x, pos.topRightCorner.y);
                this.qrcode_context.lineTo(pos.bottomRightCorner.x, pos.bottomRightCorner.y);
                this.qrcode_context.lineTo(pos.bottomLeftCorner.x, pos.bottomLeftCorner.y);
                this.qrcode_context.lineTo(pos.topLeftCorner.x, pos.topLeftCorner.y);
                this.qrcode_context.stroke();
            }else{
                if( this.qrcode_running )
                    requestAnimationFrame(this.qrcode_draw);
            }
        },
        qrcode_forcestop: function(){
            if( !this.qrcode_running )
                return;

            this.qrcode_running = false;

            if( this.qrcode_timer != null ){
                clearTimeout(this.qrcode_timer);
                this.qrcode_timer = null;
            }

            this.qrcode_video.pause();
            this.qrcode_video.srcObject = null;
            this.qrcode_btn = 'QRスキャン開始';
        },
```

ご参考までに、HTMLの方も。

```html:index.html
    <label>scaned data</label>
    <div class="input-group">
        <span class="input-group-btn">
            <button class="btn btn-default clip_btn glyphicon glyphicon-paperclip" data-clipboard-target="#qrcode_scaned_data"></button>
        </span>
        <input id="qrcode_scaned_data" type="text" class="form-control" v-model="qrcode_scaned_data" readonly>
    </div><br>
    <button class="btn btn-primary" v-on:click="qrcode_scan()">{{qrcode_btn}}</button><br>
    <div>
        <img v-show="!qrcode_running && qrcode_scaned_data==''" id="qrcode_start" class="img-responsive" src="./img/qrcode_start.png"><br>
        <video v-show="qrcode_running" id="qrcode_camera" class="img-responsive" autoplay></video>
        <canvas v-show="!qrcode_running && qrcode_scaned_data!=''" id="qrcode_canvas" class="img-responsive"></canvas>
    </div>
```

# 解説

・qrcode_scan()
ボタン押下をトリガに、QRコードスキャンを開始します。
まずは、「navigator.mediaDevices.getUserMedia」を呼び出して、ユーザに対してカメラ利用の許可を聞いた後にカメラを起動させます。
起動した後、「this.qrcode_video.srcObject = stream;」でカメラ画像をWebページに表示させ、「qrcode_draw()」の呼び出しで、カメラ画像からQRコードを探します。
setTimeout()がありますが、一定時間QRコードスキャンで見つからなかったときに、QRコードスキャンを停止するためのものです。
ちなみに、ブラウザからカメラを利用するため、HTMLはHTTPSでホスティングしている必要があります。

・qrcode_draw()
カメラ画像からQRコードをスキャンします。
いったん、別のcanvasのcontextにコピーしたのち、QRコードスキャンのライブラリを呼び出します。
QRコードがあった場合には、QRコードの部分を青色の四角形で囲ってスキャン終了です。
もしQRコードがなかった場合には、「requestAnimationFrame」を呼び出して次のカメラ画像を待ちます。

# 補足

CPU負荷を低減せねば。。。

以上