33
35

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JavaScript で QRコードのデコード:jsQR で簡素な処理(日本語の扱いでハマった部分についても記載)

Last updated at Posted at 2021-08-01

この記事は JavaScript を用いた QRコードのデコードに関する内容です。

Qiita の記事でも、いくつかこの話題に関する記事がありますが、「シンプルな最低限の実装となるとどうなるか」という話を調べて試してみたくなり、今回の内容を試しました。

ライブラリ(jsQR)を試す

Web上の記事を検索してみていると、JavaScript用のライブラリはいくつかあるようですが、よく見かけたものの 1つに jsQR というものがありました。

cozmo/jsQR: A pure javascript QR code reading library. This library takes in raw images and will locate, extract and parse any QR code found within.
 https://github.com/cozmo/jsQR

例えば Qiita の記事でいうと、このあたりの記事などです。

公式デモサイトを使ったお試し

上記のサイトを見るとデモページがあったので、それを開いて PC やスマホで動かしてみました。

●jsQR Demo
 https://cozmo.github.io/jsQR/

具体的には、PC上で以下のサイトを利用して、適当な文字列を埋め込んだ QRコードを生成し、それを上記のページを開いたスマホのブラウザで読み込ませてみました。

●QRコード(二次元バーコード)作成【無料】
 https://cman.jp/QRcode/

QRコードに埋め込む内容で日本語を入力していた場合、デモサイトでその文字列が表示されたなかったのですが、その話はいったん置いておいて、アルファベット・半角スペースのみで作った文字列で再度試したました。こちらは問題なく成功。
(原因については、「マルチバイトを考慮した処理がされてない?」とか「ライブラリ内でマルチバイトを扱えない?」とか、そのあたりかなとか思いつつ...)
【追記】 これは、QR生成を行うサイト側の文字コードの問題でした...(詳細は後で補足)

ちなみに、デモサイトのソースコードを表示させてみると、以下のような内容でした。

<html>
<head>
  <meta charset="utf-8">
  <title>jsQR Demo</title>
  <script src="./jsQR.js"></script>
  <link href="https://fonts.googleapis.com/css?family=Ropa+Sans" rel="stylesheet">
  <style>
    body {
      font-family: 'Ropa Sans', sans-serif;
      color: #333;
      max-width: 640px;
      margin: 0 auto;
      position: relative;
    }

    #githubLink {
      position: absolute;
      right: 0;
      top: 12px;
      color: #2D99FF;
    }

    h1 {
      margin: 10px 0;
      font-size: 40px;
    }

    #loadingMessage {
      text-align: center;
      padding: 40px;
      background-color: #eee;
    }

    #canvas {
      width: 100%;
    }

    #output {
      margin-top: 20px;
      background: #eee;
      padding: 10px;
      padding-bottom: 0;
    }

    #output div {
      padding-bottom: 10px;
      word-wrap: break-word;
    }

    #noQRFound {
      text-align: center;
    }
  </style>
</head>
<body>
  <h1>jsQR Demo</h1>
  <a id="githubLink" href="https://github.com/cozmo/jsQR">View documentation on Github</a>
  <p>Pure JavaScript QR code decoding library.</p>
  <div id="loadingMessage">🎥 Unable to access video stream (please make sure you have a webcam enabled)</div>
  <canvas id="canvas" hidden></canvas>
  <div id="output" hidden>
    <div id="outputMessage">No QR code detected.</div>
    <div hidden><b>Data:</b> <span id="outputData"></span></div>
  </div>
  <script>
    var video = document.createElement("video");
    var canvasElement = document.getElementById("canvas");
    var canvas = canvasElement.getContext("2d");
    var loadingMessage = document.getElementById("loadingMessage");
    var outputContainer = document.getElementById("output");
    var outputMessage = document.getElementById("outputMessage");
    var outputData = document.getElementById("outputData");

    function drawLine(begin, end, color) {
      canvas.beginPath();
      canvas.moveTo(begin.x, begin.y);
      canvas.lineTo(end.x, end.y);
      canvas.lineWidth = 4;
      canvas.strokeStyle = color;
      canvas.stroke();
    }

    // Use facingMode: environment to attemt to get the front camera on phones
    navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } }).then(function(stream) {
      video.srcObject = stream;
      video.setAttribute("playsinline", true); // required to tell iOS safari we don't want fullscreen
      video.play();
      requestAnimationFrame(tick);
    });

    function tick() {
      loadingMessage.innerText = "⌛ Loading video..."
      if (video.readyState === video.HAVE_ENOUGH_DATA) {
        loadingMessage.hidden = true;
        canvasElement.hidden = false;
        outputContainer.hidden = false;

        canvasElement.height = video.videoHeight;
        canvasElement.width = video.videoWidth;
        canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height);
        var imageData = canvas.getImageData(0, 0, canvasElement.width, canvasElement.height);
        var code = jsQR(imageData.data, imageData.width, imageData.height, {
          inversionAttempts: "dontInvert",
        });
        if (code) {
          drawLine(code.location.topLeftCorner, code.location.topRightCorner, "#FF3B58");
          drawLine(code.location.topRightCorner, code.location.bottomRightCorner, "#FF3B58");
          drawLine(code.location.bottomRightCorner, code.location.bottomLeftCorner, "#FF3B58");
          drawLine(code.location.bottomLeftCorner, code.location.topLeftCorner, "#FF3B58");
          outputMessage.hidden = true;
          outputData.parentElement.hidden = false;
          outputData.innerText = code.data;
        } else {
          outputMessage.hidden = false;
          outputData.parentElement.hidden = true;
        }
      }
      requestAnimationFrame(tick);
    }
  </script>
</body>
</html>

上記の内容を HTMLファイルにして、jsQR.js を同じ階層に置けば動きそうです。
「CDN からロードできるかな?」と思って調べてみたら、以下のページにたどり着けたので、こちらを利用してみます。

●jsqr CDN by jsDelivr - A CDN for npm and GitHub
 https://www.jsdelivr.com/package/npm/jsqr

デモサイトの内容をローカルのファイルで動かす

デモサイトの内容をそのままコピーした後、スクリプトタグの部分を以下の内容にして動作させてみました(上記の CDN から、2021/8/1時点の最新バージョンでミニファイされたファイルをロード)。

<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>

PC上で上記のファイルを動作させたので、今度はスマホで QRコードを生成し、読み込ませて動作確認をしました。そして問題なく動きました。

公式ライブラリのデモのソースを見てみる

「この後に何から独自の処理を加えたい場合、どこに手を入れれば良さそうか」というのを見てみました。

試しに動かしてみた時の挙動的には、以下のような処理を行っていると想像できました。

  1. カメラから画像を取得
  2. 取得したカメラ画像に対して QRコードの検出処理(画像上の位置情報を取得)
  3. QRコードのデコードも行う(検出されていた場合)
  4. 上記2 の結果が得られていた場合、取得したカメラ画像上に検出領域を示す線を重畳
  5. QRコードのデコード結果をページ上に表示
  6. カメラ画像の次のフレームを取得する(あとは、同様の処理を繰り返す)

ここからは、実際にソースコードを見ていって確認してみます。

カメラ画像の取得

上記1 に該当する部分は、以下のようでした。

    // Use facingMode: environment to attemt to get the front camera on phones
    navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } }).then(function(stream) {
      video.srcObject = stream;
      video.setAttribute("playsinline", true); // required to tell iOS safari we don't want fullscreen
      video.play();
      requestAnimationFrame(tick);
    });

カメラ画像の取得は、WebRTC等のカメラを利用する処理でおなじみの navigator.mediaDevices.getUserMedia です。

その処理の中で、スマホで動かした時、内向き/外向きのカメラのうち、外向きのカメラを利用する設定をしているようでした。
具体的には facingMode: "environment" を指定している部分です。

MDN の MediaDevices.getUserMedia() のページにも書いてあるとおりです(内向きのカメラを使うときは user を指定)。
内向き/外向きのカメラのどちらを使うか

video.setAttribute("playsinline", true); // required to tell iOS safari we don't want fullscreen の部分は、コメント内や以下のサイトにも書かれた iOS の Safari 向けの処理のようです。

●javascript - How to create an HTML5 video element in iOS that will stay hidden? - Stack Overflow
 https://stackoverflow.com/questions/64415963/how-to-create-an-html5-video-element-in-ios-that-will-stay-hidden

なお、MDN のページで playsinline について書かれた部分は以下となっています。
インライン再生.jpg

QRコードの検出・デコード

QRコードを検出したりデコードしたりする処理は、以下になりそうです。

        canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height);
        var imageData = canvas.getImageData(0, 0, canvasElement.width, canvasElement.height);
        var code = jsQR(imageData.data, imageData.width, imageData.height, {
          inversionAttempts: "dontInvert",
        });

カメラ画像をいったん Canvas に書き込み、 canvas.getImageData でそれを取得した後に、 var code = jsQR... という部分でライブラリ内での処理に引き渡しているようです。

また、オプションで {inversionAttempts: "dontInvert"} と指定している部分は、公式のサイトに説明がありました。
オプションについて
このオプション指定について、「後方互換性のためにデフォルトを attemptBoth にしているものの、パフォーマンスの面から dontInvert を指定するのが良い(将来的には dontInvert をデフォルトにする)」といった説明が書いてありました。

QRコードが検出された場合の処理

QRコードに対する処理結果を格納した code について、デモサイトのソースを読み解いて中身を確認する方法もありますが、ここではソースコードにログ出力の処理をつけたして、その出力された中身を見てみます。

Chrome で動作させ、ログを開発者ツールのコンソールに出してみました。
codeの中身.jpg

QRコードが検出されなかった場合は、 code が null になるようです。
一方、QRコードが検出がされた場合、検出した位置の情報( code.location の部分)やデコード結果( code.data の部分)が code の中に格納されるようです。

あらためてソースコードのほうも見てみると、そのあたりの情報を使った処理が以下の部分で見つかります。

        if (code) {
          drawLine(code.location.topLeftCorner, code.location.topRightCorner, "#FF3B58");
          drawLine(code.location.topRightCorner, code.location.bottomRightCorner, "#FF3B58");
          drawLine(code.location.bottomRightCorner, code.location.bottomLeftCorner, "#FF3B58");
          drawLine(code.location.bottomLeftCorner, code.location.topLeftCorner, "#FF3B58");
...
          outputData.innerText = code.data;
        } else {
...

日本語の扱い

冒頭で書いた、日本語を QRコードに埋め込んだ場合に code.data の中身がどうなるかをログに出して確認してみました。
日本語の扱い.jpg

ちなみに、この時利用していたQRコード(「日本語」という文字を埋め込んだもの)は、以下の画像の内容でした。
Shift_JIS_日本語.png

この場合の code.data の中身は「空の文字列」になっているようでした。

公式ページの仕様が書かれていそうな部分(Return value の部分)を見ると、以下のような記載がありました。
Return value.jpg

上記を見ると、 code.data が空の文字列になっている時も code.binaryData にはデータが入っているように思われます。
そこで、 code.binaryData をログに出力してみて、中身を見てみます。

以下は、「日本語」という文字列を QRコードに埋め込んだ場合と、「test」という文字列を QRコードに埋め込んだ場合の code.binaryData のそれぞれの中身です。

▼ 「日本語」という文字列を QRコードに埋め込んだ場合
「日本語」という文字.jpg

▼ 「test」という文字列を QRコードに埋め込んだ場合
「test」という文字.jpg

バイナリデータのサイズ的には、うまくデータが取得できていそうに見えます(「日本語」は 2バイト文字が 3文字分で、test は 1バイトの文字が 4文字分)。

以下のサイトを参考にしつつ、Google Chrome の開発者ツールのコンソールで、デコードの処理を実行していろいろ試してみました。

その結果、分かったことは QRコードの日本語の文字列が Shift_JIS で、jsQR のデコード側が UTF-8 で文字列変換をしていそうだということでした。

以下に、Google Chrome の開発者ツールのコンソールで、「TextDecoder」を用いたデコードの処理を実行した際の、ソースコードと結果を掲載してみます。

var bytes = [147, 250, 150, 123, 140, 234];
var text_decoder = new TextDecoder('shift-jis');
var str = text_decoder.decode(Uint8Array.from(bytes).buffer);
console.log(str);

上記を実行した結果は以下となりました。
デコード(日本語).jpg

QRコード作成サイトについて少し検索してみると、例えば以下のサイトは文字コード指定も行えるようでした。

●QRコード作成| Softel labs
 https://www.softel.co.jp/labs/tools/qr/

「日本語」という文字列を、UTF-8 で QRコード化してみます。
UTF-8_日本語.png

上記の画像を jsQR の公式デモで読ませた場合は、無事に日本語の文字もページ上の出力に表示されました。

そして、最初に利用していたサイト内で、こんな記載がありました。

jsQR の処理では、この話に関係なく文字列への変換で UTF-8 を指定している実装になってる、という感じなのかな。

おわりに

JavaScript による QRコードのデコードについて、jsQR というライブラリを使うことで、簡単に実行できました。
ソースコードは、公式デモの実装がシンプルなもので、それを簡素化するというようなことはする必要がない状態でした。

そして、日本語の取り扱いに関する部分は少しハマったところがありましたが、内部実装についての知見が得られたので良かったです。

余談

かなり環境を選ぶようですが、以下の記事に出ていた Shape Detection API というものが気になったりもしています。

●続・Webの技術だけで作るQRコードリーダー - Qiita
 https://qiita.com/kan_dai/items/3486880236a2fcd9b527

後で上記の記事や、以下の仕様のページなどを見ていければと思っています。

●Accelerated Shape Detection in Images
 https://wicg.github.io/shape-detection-api/

【追記】 この後にやったこと 2つ

p5.js版も完成!(動作確認は PC のみ)

その後、p5.js を使って、ソースコードの行数を減らしたバージョンができました。

●p5.js と jsQR で簡素な QRコードのデコード処理:全体で40行ほど(試行錯誤の過程の情報も記載) - Qiita
 https://qiita.com/youtoy/items/d7dd18e7127f23f482dd

QRコードのデコードと Teachable Machine での画像分類を同時に動かす

その後、Teachable Machine の画像分類(p5.js版)と jsQR による QRコードのデコードを、同時に動かす処理を試してみて、動作させることができました。

33
35
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
35

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?