Quagga.js でバーコード読取してみた

# はじめに

自分は以前に Cordova を使って「蔵書台帳」アプリを作りました。このとき
書籍の ISBN のバーコードを読取するのに、Cordova のプラグインを使っていました。
改めて調べてみると、ブラウザ上の JavaScript コードで直接、カメラ画像からバーコードの読取できるんですね。

JavaScript でバーコード読取できるライブラリは幾つもありましたが、これがよさそうでした。

[GitHub - serratus/quaggaJS: An advanced barcode-scanner written in JavaScript](https://github.com/serratus/quaggaJS)
[GitHub - ericblade/quagga2: An advanced barcode-scanner written in Javascript and TypeScript]( https://github.com/ericblade/quagga2)

紹介している記事も多く、幾つか試してみました。

# Quagga.js を使ってみた

* Quagga2 1.7.4

## インストールする

今回は CDN を使いました。

```html:
    <script src="https://unpkg.com/@ericblade/quagga2@1.7.4/dist/quagga.min.js"></script>
```

## Quagga.js を使ってみた

Quagga.js を使ってみます。

以下の記事を参考にしました。
[QuaggaJSを使ったバーコードリーダ実装 | @sinのブログ](https://ameblo.jp/white-rabbit-0925/entry-12579659224.html)

カメラ画像を表示するエリアを用意します。併せて読取の結果を表示する項目も用意します。

```html:app.html
    <div id="container"></div>
    <p id="process"></p>
    <p id="result"></p>
```
```css:app.css
    #container {
        width: 100%;
        height: auto;
    }
```

JavaScript のコードで Quagga.js の機能を利用します。

```javascript:app.js
    window.onload = function(){
        Quagga.init({
            inputStream: {
                type: "LiveStream",
                target: document.querySelector('#container')
            },
            constraints: {
                facingMode: "environment",
            },
            decoder: {
                readers: [ "ean_reader" ]
            } 
        }, 
        function(err) {
            if (err) {
                console.log(err);
                return;
            }
            console.log("Initialization finished. Ready to start");
            Quagga.start();
        });

        Quagga.onProcessed(function(result){
            var ctx = Quagga.canvas.ctx.overlay;
            var canvas = Quagga.canvas.dom.overlay;

            ctx.clearRect(0, 0, parseInt(canvas.width), parseInt(canvas.height));

            if (result) {
                if (result.box) {
                    console.log(JSON.stringify(result.box));
                    Quagga.ImageDebug.drawPath(result.box, {x: 0, y: 1}, ctx, {color: 'blue', lineWidth: 2});
                }
            }
        });

        Quagga.onDetected(function(result){
            document.querySelector('#result').textContent = result.codeResult.code;
        });      
    };
```

```Quagga.init()``` で初期設定します。このコールバック関数で
```Quagga.start()``` して読取を開始します。
カメラ画像にバーコードが発見された経過は ```Quagga.onProcessed()``` で処理を記述します。
バーコードが認識できた結果は ```Quagga.onDetected()``` で処理を記述します。
```Quagga.ImageDebug.drawPath()``` を使うと、カメラ画像にバーコードを発見できた場所を四角で囲んだりできます。

ところが上記のコードでは、
```#container``` が、画面横幅一杯に CSS で指定しているのに、画面をはみ出して表示されたりします。
また、カメラ画像と離れたところに、バーコードの場所の四角が表示されます。
実行時の HTML を確認すると、```#container``` に対して Quagga.js が ```video``` と ```canvas``` を追加しています。
これのサイズと位置を調整すればよさそうです。以下の記述を CSS に追加します。

```css:app.css
    #container > video, #container > canvas {
        width: 100%;
    }
    #container > canvas {
        position: absolute;
        left: 0px; top: 0px;
    }
```

# カメラ画像の特定の場所のバーコードだけ読取したい

バーコード読取できるようになりましたが、問題ありました。
書籍の ISBN のバーコードは 2 段で 1 組です。また、別の書籍のバーコードがカメラ画像に入ってくることがあります。
このとき上記のコードでは、画像にある複数のバーコードがまとめて読取されます。
画像にガイドになる四角を表示して、そこに収まったバーコードだけ読取するようにできないでしょうか。

## JavaScript のコードでカメラ画像にブラウザ上に表示する

まず、JavaScript のコードでカメラ画像にブラウザ上に表示します。

以下の記事を参考にしました。
[【JavaScript】Webカメラの映像をブラウザに表示する方法 | アールエフェクト](    https://reffect.co.jp/html/javascript-webcamera)

カメラ画像を表示するエリアを用意します。```video``` タグを使います。

```app.html
    <video id="video"></video>
    <p id="result"></p>
```
```app.css
    #video {
        width: 100%;
        height: auto;
    }
```

JavaScript のコードでカメラ画像を表示します。

```app.js
    var video = document.querySelector('#video');
    navigator.mediaDevices.getUserMedia({
        video: {
            facingMode: 'environment'
        },
        audio: false
    })
    .then(function(stream){
        video.srcObject = stream;
        video.play();
    })
    .catch(function(e){
        document.querySelector('#result').textContent = JSON.stringify(e);
    });
```

```navigator.mediaDevices.getUserMedia()``` を使用します。

上記のコードは ```video``` タグにカメラ画像を表示していますが、その内容を ```canvas``` タグにコピーして表示するようにします。

以下の記事を参考にしました。
[Webカメラの映像をcanvasに表示させる - Qiita](https://qiita.com/chelcat3/items/02c77b55d080d770530a)

カメラ画像を表示するエリアを ```video``` タグから ```canvas``` タグに変えます。

```app.html
    <canvas id="canvas"></canvas>
    <p id="result"></p>
```
```app.css
    #canvas {
        width: 100%;
        height: auto;
    }
```

JavaScript のコードでカメラ画像を表示します。

```javascript:app.js
    var video = document.createElement('video');
    var canvas = document.querySelector('#canvas');
    navigator.mediaDevices.getUserMedia({
        video: {
            facingMode: 'environment'
        },
        audio: false
    })
    .then(function(stream){
        video.srcObject = stream;
        video.play();
        setInterval(function(){
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;
            var ctx = canvas.getContext('2d');
            ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, canvas.width, canvas.height);
        }, 200);
    })
    .catch(function(e){
        document.querySelector('#result').textContent = JSON.stringify(e);
    });
```

```video``` タグは JavaScript コードで生成します。
```canvas``` の表示するサイズは CSS で指定しますが、画像データのサイズはコードで ```video``` のサイズと同じに指定します。
```setInterval()``` を使って 0.2 秒ごとに ```video``` の内容を ```canvas``` にコピーします。

## カメラ画像の特定の場所に四角を描く

カメラ画像を ```canvas``` に表示するようにしたので、ここに四角を描きます。
画面の中央に高さ 100 ピクセルの四角を描くことにします。

```javascript:app.js
        (前略)
        ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, canvas.width, canvas.height);

        var box = {
            x: 50,
            h: 100
        };
        box.y = (canvas.height - box.h) / 2;
        box.w = (canvas.width - box.x * 2);

        ctx.beginPath();
        ctx.strokeStyle = 'red';
        ctx.lineWidth = 2;
        ctx.rect(
            box.x, box.y, box.w, box.h
        );
        ctx.stroke();
```

さらに、上記の四角の場所の内容を別の ```canvas``` にコピーします。

```javascript:app.js
    (前略)
    var canvas = document.querySelector('#canvas');
    var buf = document.createElement('canvas');
    document.body.append(buf);
```
```javascript:
        (前略)
        ctx.stroke();

        buf.width = box.w;
        buf.height = box.h;
        buf.getContext('2d').drawImage(canvas, box.x, box.y, box.w, box.h, 0, 0, box.w, box.h);
```
## カメラ画像の特定の場所のバーコードだけ読取する

上記のコードで切取できた画像データを Quagga.js に渡してバーコード読取します。

以下の記事を参考にしました。
[Javascriptでカメラ映像からquaggaJSを使用してJANコードを読む(バーコードを読み取る)](https://mam-mam.net/javascript/js_quagga.html)
[QuaggaJSを使ってブラウザでバーコードスキャン](https://nanbu.marune205.net/2021/12/barcode-scan-quaggajs.html)

```javascript:app.js
        (前略)
        buf.getContext('2d').drawImage(canvas, box.x, box.y, box.w, box.h, 0, 0, box.w, box.h);

        buf.toBlob(function(blob){
            var reader = new FileReader();
            reader.readAsDataURL(blob);
            reader.onload = function(){
                Quagga.decodeSingle({
                    decoder: {
                        readers: [ "ean_reader" ],
                    },
                    src: reader.result
                },
                function(result){
                    if (result && result.codeResult) {
                        document.querySelector('#result').textContent = result.codeResult.code;
                    }
                });
            };
        });
```

```Quagga.init()``` して ```Quagga.start()``` する代わりに ```Quagga.decodeSingle()``` を使います。
```canvas``` の内容を ```toBlob()``` で変換して、これを ```FileReader``` の ```readAsDataURL()``` で読込して ```Quagga.decodeSingle()``` に渡します。
バーコードが認識できた結果は ```Quagga.decodeSingle()``` のコールバック関数で処理を記述します。

このコードでは、```Quagga.decodeSingle()``` が 0.2 秒ごとに呼出されます。
ところが ```decodeSingle()``` して読取できてコールバック関数が呼出されるまでに、0.2 秒以上掛かるかも知れません。
ある ```decodeSingle()``` の呼出が完了する前に次の ```decodeSingle()``` が呼出されたら、まずいですね。
フラグ ```isDetecting``` を用意して、```decodeSingle()``` が完了するまでは新たに ```decodeSingle()``` を呼出しないようにします。

```javascript:app.js
    var isDetecting = false;

    setInterval(function(){
        (中略)
        buf.getContext('2d').drawImage(canvas, box.x, box.y, box.w, box.h, 0, 0, box.w, box.h);

        if (isDetecting) return;
        isDetecting = true;

        buf.toBlob(function(blob){
            var reader = new FileReader();
            reader.readAsDataURL(blob);
            reader.onload = function(){
                Quagga.decodeSingle({
                    decoder: {
                        readers: [ "ean_reader" ],
                    },
                    src: reader.result
                },
                function(result){
                    if (result && result.codeResult) {
                        document.querySelector('#result').textContent = result.codeResult.code;
                    }
                    isRecognizing = false;
                });
            };
        });
    }, 200);
```