バーコードから書籍情報を取ってくるApp

個人で持っている書籍の貸し借りが多くなり、書籍を一元的に管理したい！というモチベーションから、バーコードを読んで書籍情報を取得するAppを作りました。

デザインを整えたものはこんな感じになりました。

https://twitter.com/rj_phys/status/1593191604541849602?s=20&t=ItblPayz68M5klUKqzpq3Q

＊本当は書籍をDBで管理したいのですがその話はいずれするかもしれません。知らんけど。

今回は[quagga.js](https://serratus.github.io/quaggaJS/)と[GoogleBooksAPI](https://developers.google.com/books/docs/overview)を用いたシンプルなWebAppにしました。

# アウトライン
 - 書籍情報の管理
 - quagga.js
   バーコードを読み取るために用いるAPIについて。
 - ISBNの受け渡し
   quagga.jsで読み取ったISBNを書籍情報検索に使うための処方箋について。
 - 書籍情報の検索
   ISBNを使った書籍情報検索について。

# 書籍情報の管理
（自家本を除くほとんどの）書籍には、国際標準図書番号（ISBN: International Standard Book Number）という一意のコードが割り振られています（DBのPrimary Keyのようなもの）。

従ってISBNを知ることができれば、書籍のデータを取得することが可能です。

**余談**
現在使用されているISBNは13桁で、「JAN書籍コード（3桁）」「国別番号（1桁）」「出版者記号・書名記号（8桁）」「チェック数字（1桁）」からなっています。

例えば`978-4-16-755602-0`は次のような意味を持ちます。
```
978 -> 書籍であること（978または979）
4 -> 言語（日本語）
16 -> 出版社（文春文庫）
755602 -> 書籍を指定（文庫版「大地の子 二」）
0 -> チェック数字
```

このチェック数字は、以下のようにして求めることができます。
```
1. 左から数えて奇数番目の数字を足して X とする（チェック数字は除く）
2. 左から数えて偶数番目の数字を足し、3倍して Y とする
3. Z = X + Y
4. 10から Z の1桁目を引いた数の1桁目がチェック数字
```

```js:JavaScript
const ISBN = '9784167556020';
var Z = 0;

for(let i=0; i < ISBN.length - 1; i++){
    i%2 == 0 ? Z += Number(ISBN.slice(i,i+1)) : Z += 3*Number(ISBN.slice(i,i+1));
}
const ans = (10 - Z%10)%10;
```

# quagga.js

quagga.jsはリアルタイムでバーコードを読み取りデコードすることのできるスクリプトです。

>QuaggaJS is a barcode-scanner entirely written in JavaScript supporting real- time localization and decoding of various types of barcodes such as **EAN, CODE 128, CODE 39, EAN 8, UPC-A, UPC-C, I2of5, 2of5, CODE 93** and **CODABAR**. The library is also capable of using `getUserMedia` to get direct access to the user’s camera stream. Although the code relies on heavy image-processing even recent smartphones are capable of locating and decoding barcodes in real-time.

> QuaggaJSは**EAN, CODE 128, CODE 39, EAN 8, UPC-A, UPC-C, I2of5, 2of5, CODE 93, CODABAR**といった様々なタイプのバーコードについて、リアルタイムの特定とデコードをサポートしています！重い画像処理をしているけれど、スマートフォンでも使うことができるよ！

https://serratus.github.io/quaggaJS/

:::note warn
quagga.jsは2017/06/07を最後にメンテナンスされていません。
また、localhost以外ではhttpsでないと利用できないという[指摘](https://nanbu.marune205.net/2021/12/barcode-scan-quaggajs.html?m=1)があります。
:::

## インストール
quagga.jsは`npm`や`Bower`から使うこともできますが、今回はAPIを用いて実装します。
[リポジトリ](https://github.com/serratus/quaggaJS)から`quaggaJS/dist/quagga.min.js`をダウンロードして適当な場所に置き、HTMLから読み込みます。

```html:HTML
<script src="/src/js/quagga.min.js"></script>
```

ダウンロードしてローカルに設置する代わりとして、CDNも利用可能です。
```html:HTML
<script src="https://unpkg.com/@ericblade/quagga2@1.7.4/dist/quagga.min.js"></script>
```

## quagga.jsの利用

カメラから取得した映像とISBNを表示するために次のようなHTMLを準備しました。
```html:HTML
...
<body>
    <div id="searchField">
        <div id="quagga__livestream"></div>
			<button id="quagga__cameraOn">カメラを起動</button>
		</div>
    </div>
    <p id="resultField"></p>
</body>
...
```

```js:JavaScript
const buttonCamera = document.querySelector('#quagga__cameraOn');

// buttonCameraがクリックされたら発火
buttonCamera.addEventListener('click', () => {
    // quagga.jsを使ってバーコードを読み取る関数
	isbnLoad();
});
```

`button#quagga__cameraOn`でquagga.jsを起動し、
`#quagga__livestream`にはquagga.jsから取得した映像を表示します。

取得したISBNは`#resultField`に表示させます。

quagga.jsを動かす関数を作ります。

```js:JavaScript
function isbnLoad(){
    Quagga.init({
        inputStream: {
            type: "LiveStream",
            target: document.querySelector('#quagga__livestream'),
            decodeBarCodeRate: 3,
            successTimeout: 500,
            codeRepetition: true,
            tryVertical: true,
            frameRate: 15,
            width: 640,
            height: 480,
            facingMode: "environment"
        },
        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((result) => {
        var ctx = Quagga.canvas.ctx.overlay;
        var canvas = Quagga.canvas.dom.overlay;
        ctx.clearRect(0, 0, parseInt(canvas.width), parseInt(canvas.height));
    });

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

詳細は省きますが、`Quagga.init(config, function(err){})`で初期設定を行っています。
`Quagga.onProcessed()`では、カメラを起動している間、`#quagga__livestream`に映像を描画させています。

`Quagga.onDetected()`はバーコードが読み取られた際の動作で、
```js:JavaScript
document.querySelector('#resultField').textContent = result.codeResult.code;
```
で`#resultField`のコンテンツにISBNの値を格納しています。

そして`Quagga.stop()`でプロセスが終了し、自動的にカメラがオフになります。

:::note warn
書籍のバーコードは一般に2枚あり、どちらを読み取るかわかりません。
またquagga.jsの精度が高くないこともあり、意図したデータを取得できない場合があります。
そのため、`Quagga.stop()`の前に取得したコードが正しいISBNかどうか判定するのが良いでしょう。
:::

# ISBNの受け渡し
ISBNが取得できたら、次は書籍情報の検索を行います。

しかしISBNの取得は一般に0ではない時間がかかるので、
単純に書籍情報を取得する関数と並べて書くと予期せぬエラーが起こります。

```js:JavaScript
isbnLoad(); //quagga.jsでISBNを読み取り、#resultFeild に格納

// #resultFeild のvalueを取得する
// isbnLoad()が終了する前にこれが実行され、isbnはnullになる
const isbn = document.getElementById("resultFeild");

function(isbn) => {
 // isbnから書籍情報を取得するコード
 // isbnがnullなのでエラーが起こる
}
```

これを回避するために`async/await`を使うこともできそうですが、
上手くいかなかったのでここでは別の方法を取ります。

## MutationObserverで変更の追跡

`#resultField`のデータが変更された場合に、書籍情報を検索する関数を走らせる必要があります。
そのために使うのが`MutationObserver`です。

これはその名の通り、特定のDOM Elementを監視し、変更が加えられた場合にトリガーが発火するものです。

```js:JavaScript
// 監視対象のDOM Element
const ISBN = document.querySelector("#resultField");

var mo = new MutationObserver(function(record, observer) {
    // 変更された場合に走らせるスクリプト
    // ISBNの値を取得し、
	const isbn = record[0].addedNodes[0].nodeValue;
    // コンソールに出力
    console.log(isbn);
});

// 監視する内容
var options = {
    childList: true, //子要素の変更（テキストの変更含む）
	characterData: true //テキストの変更
};

// 監視開始
mo.observe(feedISBN, options);
```

変更が加えられた場合には`MutationRecord`が返ります。
詳細は[レファレンス](https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord)を確認してください。

監視する内容は`MutationObserver.observe()`の第二引数として渡します。
こちらも詳細は[レファレンス](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe)に譲りますが、

> An object providing options that describe which DOM mutations should be reported to `mutationObserver`'s `callback`. At a minimum, one of `childList`, `attributes`, and/or `characterData` must be true when you call `observe()`. Otherwise, a `TypeError` exception will be thrown.

> 少なくとも`childList`,`attributes`,`characterData`のいずれかは`true`に設定しておいてね！

とのことです。

# 書籍情報の検索

書籍情報を取得するためのAPIは複数あるのですが、今回は登録が不要なGoogleBooksAPIを利用しました。

リクエストURLは次の通り。

```js:JavaScript
const url = `https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}`;
```

JavaScriptのfetch APIを使ってGETします。

```js:JavaScript
const uri = `https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}`;
const config = {};
const res = await fetch(uri, config);
const data = await res.json();
```

レスポンスの形式は次の通りです。
```json:json
{
  "kind": "books#volumes",
  "totalItems": 1,
  "items": [
    {
      "kind": "books#volume",
      "id": "n-MqAQAAMAAJ",
      "etag": "GrEhUqkN06M",
      "selfLink": "https://www.googleapis.com/books/v1/volumes/n-MqAQAAMAAJ",
      "volumeInfo": {
        "title": "大地の子 2",
        "authors": [
          "山崎豊子"
        ],
        "publishedDate": "1994",
        "description": "日本人戦争孤児で、中国人の教師に養育された陸一心。肉親の情と中国への思いの間で揺れる青年の苦難の旅路を、戦争や文化大革命など",
        "industryIdentifiers": [
          {
            "type": "OTHER",
            "identifier": "WISC:89074611757"
          }
        ],
        "readingModes": {
          "text": false,
          "image": false
        },
        "pageCount": 400,
        "printType": "BOOK",
        "categories": [
          "China"
        ],
        "maturityRating": "NOT_MATURE",
        "allowAnonLogging": false,
        "contentVersion": "0.1.1.0.preview.0",
        "panelizationSummary": {
          "containsEpubBubbles": false,
          "containsImageBubbles": false
        },
        "imageLinks": {
          "smallThumbnail": "http://books.google.com/books/content?id=n-MqAQAAMAAJ&printsec=frontcover&img=1&zoom=5&source=gbs_api",
          "thumbnail": "http://books.google.com/books/content?id=n-MqAQAAMAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api"
        },
        "language": "ja",
        "previewLink": "http://books.google.co.jp/books?id=n-MqAQAAMAAJ&dq=isbn:9784167556020&hl=&cd=1&source=gbs_api",
        "infoLink": "http://books.google.co.jp/books?id=n-MqAQAAMAAJ&dq=isbn:9784167556020&hl=&source=gbs_api",
        "canonicalVolumeLink": "https://books.google.com/books/about/%E5%A4%A7%E5%9C%B0%E3%81%AE%E5%AD%90_2.html?hl=&id=n-MqAQAAMAAJ"
      },
      "saleInfo": {
        "country": "JP",
        "saleability": "NOT_FOR_SALE",
        "isEbook": false
      },
      "accessInfo": {
        "country": "JP",
        "viewability": "NO_PAGES",
        "embeddable": false,
        "publicDomain": false,
        "textToSpeechPermission": "ALLOWED",
        "epub": {
          "isAvailable": false
        },
        "pdf": {
          "isAvailable": false
        },
        "webReaderLink": "http://play.google.com/books/reader?id=n-MqAQAAMAAJ&hl=&source=gbs_api",
        "accessViewStatus": "NONE",
        "quoteSharingAllowed": false
      },
      "searchInfo": {
        "textSnippet": "日本人戦争孤児で、中国人の教師に養育された陸一心。肉親の情と中国への思いの間で揺れる青年の苦難の旅路を、戦争や文化大革命など"
      }
    }
  ]
}
```

ここには書籍自体の情報に加えて、GoogleBooksの情報も記載されています。

なお、`imageLinks`に格納されているサムネイル画像は解像度が低いので利用する際は注意すべきかも知れません。
rakutenやamazonも類似のAPIを提供しています（登録が必要）。


データが取得できたらHTMLに適当なタグを準備して、データを表示します。
こんな感じでしょうか。

```html:HTML
...
<p id="title"></p>
<p id="author"></p>
<p id="published"></p>
<p id="pageCount"></p>
<p id="description"></p>
<img src="">
...
```

```js:JavaScript
const googleBook = data['items'][0]['volumeInfo'];

document.querySelector(`#title`).textContent = `タイトル：${googleBook['title']}`;
document.querySelector(`#author`).textContent = `著者：${googleBook['authors']}`;
document.querySelector('#published').textContent = `出版年：${googleBook['publishedDate']}`;
document.querySelector('#pageCount').textContent = `ページ数：${googleBook['pageCount']}`;
document.querySelector('#description').textContent = `説明：${googleBook['description']}`;
document.querySelector('#coverImg').src = googleBook['imageLinks']['thumbnail'];
```

# 最後に

今回はいくつかの記事を参考に、quagga.jsとgoogleBooks APIを使った書籍情報を取得するappを作りました。

参考に記事を貼り付けておきます。ありがとうございました！

https://zenn.dev/nyoroko/articles/6b6b58c044505e

https://nanbu.marune205.net/2021/12/barcode-scan-quaggajs.html?m=1

https://iwb.jp/javascript-quaggajs-browser-2d-barcode-scan/

https://qiita.com/tinymouse/items/6aa75c30f828fe3f0c42