ブラウザ上にドラッグ&ドロップ (D&D) されたQRコードの画像を、読み込んでデコードするUIを作りたい。
Pure JavaScript で動作する、QR コードデコーダー(パーサー) cozmo/jsQR が有名なので、これを使ってみよう。
jsQR は、 png や jpg といった画像コンテナの解凍機能も、 HTML や DOM, Web 周りに特化した機能も一切無いため、読み込ませた画像は何らかの方法で Uint8ClampedArray
な RAW 画像に変換して渡す必要がある。
ちょっと遠回りにはなるが、
- D&D されたファイルの File API を読み取る
- FileReader API を使って、ファイルを DataURI として読み込ませ、 img タグに表示させる
- OffscreenCanvas と OffscreenCanvasRenderingContext2D を経由して ImageData を作成する
- 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ピクセル程度の画像であれば、近年のデバイスなら一瞬でデコードできるので、実際のところはメインスレッド側で処理してしまって問題ないだろう。
ピンバック: JavaScript で TOTP を計算する | Aqua Ware つぶやきブログ