D&DしたQRコード画像をブラウザ上でデコードする

Pocket

ブラウザ上にドラッグ&ドロップ (D&D) されたQRコードの画像を、読み込んでデコードするUIを作りたい。

Pure JavaScript で動作する、QR コードデコーダー(パーサー) cozmo/jsQR が有名なので、これを使ってみよう。

jsQR は、 png や jpg といった画像コンテナの解凍機能も、 HTML や DOM, Web 周りに特化した機能も一切無いため、読み込ませた画像は何らかの方法で Uint8ClampedArray な RAW 画像に変換して渡す必要がある。

ちょっと遠回りにはなるが、

  1. D&D されたファイルの File API を読み取る
  2. FileReader API を使って、ファイルを DataURI として読み込ませ、 img タグに表示させる
  3. OffscreenCanvasOffscreenCanvasRenderingContext2D を経由して ImageData を作成する
  4. jsQR に処理を投げる

といった処理になるだろうか。

早速実装してみよう。

実装

<div id="divDrop" style="margin: 10px; padding: 10px; background-color: lightgray; border: 4px dashed gray; border-radius: 12px;">
    <div>Drag and drop an image file here</div>
    <div>or <input id="iptFile" type="file" accept="image/*"></div>
    <div id="divPreviewContainer"></div>
</div>
<div style="margin: 10px; padding: 10px;">
    <input type="text" id="iptResult" style="width: 600px">
    <div id="divErrorOut" style="display: none; margin: 4px; padding: 4px; background-color: pink; border: 1px solid red; border-radius: 4px;"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
<script type="text/javascript">
    // @ts-check
    (() => {
        "use strict";
        const divDrop = /** @type {HTMLDivElement} */(document.getElementById("divDrop"));
        const divPreviewContainer = /** @type {HTMLDivElement} */(document.getElementById("divPreviewContainer"));
        const divErrorOut = /** @type {HTMLDivElement} */(document.getElementById("divErrorOut"));
        const iptFile = /** @type {HTMLInputElement} */(document.getElementById("iptFile"));
        const iptResult = /** @type {HTMLInputElement} */(document.getElementById("iptResult"));

        /** @type { (file: File) => Promise<any> } */
        async function decodeQrCode(file) {
            divErrorOut.style.display = "none";
            try {
                // read ile
                const fileReader = new FileReader();
                const fileReadAsync = new Promise((resolve, reject) => {
                    fileReader.onload = ev => resolve(ev.target?.result);
                    fileReader.onerror = ev => reject(ev);
                });
                fileReader.readAsDataURL(file);
                /** @type {string} */
                const dataUrl = await fileReadAsync;

                // load as image
                divPreviewContainer.innerHTML = "";
                const imgPreview = document.createElement("img");
                const imgLoadAsync = new Promise(resolve => imgPreview.onload = ev => resolve(ev))
                imgPreview.setAttribute("src", dataUrl);
                divPreviewContainer.append(imgPreview);
                await imgLoadAsync;
                const { naturalWidth, naturalHeight } = imgPreview;

                // convert image to raw binary
                var canvas = new OffscreenCanvas(naturalWidth, naturalHeight);
                var ctx = canvas.getContext("2d");
                if (!ctx) throw "failure to getContext";
                ctx.drawImage(imgPreview, 0, 0);
                const imageData = ctx.getImageData(0, 0, naturalWidth, naturalHeight);

                // decode with jsQR
                const code = jsQR(imageData.data, naturalWidth, naturalHeight);
                if (code) {
                    iptResult.value = code.data;
                } else {
                    iptResult.value = "";
                    throw "decode QR error";
                }
            } catch (err) {
                divErrorOut.style.display = "block";
                divErrorOut.textContent = `${err}`;
            }
        }

        // Handling D&D
        divDrop.addEventListener("dragover", ev => {
            ev.preventDefault();
        });
        divDrop.addEventListener("drop", ev => {
            ev.preventDefault();
            let imageFiles;
            if (ev.dataTransfer && 0 < (imageFiles = [...ev.dataTransfer.files].filter(f => f.type.startsWith("image/"))).length) {
                const dt = new DataTransfer();
                imageFiles.forEach(f => dt.items.add(f));
                iptFile.files = dt.files;
                decodeQrCode(imageFiles[0]);
            }
        });
        iptFile.addEventListener("change", ev => iptFile.files && 0 < iptFile.files.length ? decodeQrCode(iptFile.files[0]) : undefined)
    })();
</script>

See the Pen
decode D&Ded QR images
by advanceboy (@advanceboy)
on CodePen.

いったん img タグで画像を表示させるワンクッションを置いているおかげで、 png, jpg のみならず svg 等の画像もデコードできるようになっている。

Edge や Chrome ブラウザであれば、表示されている画像をそのまま D&D してきてのデコードもできるぞ。

jsQR の処理は、 JavaScript のスレッドで行われるので、処理が重くなるとブラウザが固まってしまう。
このため、理想的には Web Worker に処理を渡してしまうほうがよさそうではある。

ただ、1000x1000ピクセル程度の画像であれば、近年のデバイスなら一瞬でデコードできるので、実際のところはメインスレッド側で処理してしまって問題ないだろう。

D&DしたQRコード画像をブラウザ上でデコードする」への1件のフィードバック

  1. ピンバック: JavaScript で TOTP を計算する | Aqua Ware つぶやきブログ

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください