tag:crieit.net,2005:https://crieit.net/tags/QR%E3%82%B3%E3%83%BC%E3%83%89/feed
「QRコード」の記事 - Crieit
Crieitでタグ「QRコード」に投稿された最近の記事
2024-02-08T01:37:05+09:00
https://crieit.net/tags/QR%E3%82%B3%E3%83%BC%E3%83%89/feed
tag:crieit.net,2005:PublicArticle/18734
2024-02-08T01:37:05+09:00
2024-02-08T01:37:05+09:00
https://crieit.net/posts/D-D-QR
D&DしたQRコード画像をブラウザ上でデコードする
<p>ブラウザ上にドラッグ&ドロップ (D&D) されたQRコードの画像を、読み込んでデコードするUIを作りたい。</p>
<p>Pure JavaScript で動作する、QR コードデコーダー(パーサー) <a target="_blank" rel="nofollow noopener" href="https://github.com/cozmo/jsQR">cozmo/jsQR</a> が有名なので、これを使ってみよう。</p>
<p>jsQR は、 png や jpg といった画像コンテナの解凍機能も、 HTML や DOM, Web 周りに特化した機能も一切無いため、読み込ませた画像は何らかの方法で <code>Uint8ClampedArray</code> な RAW 画像に変換して渡す必要がある。</p>
<p>ちょっと遠回りにはなるが、</p>
<ol>
<li>D&D されたファイルの <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/API/File_API">File API</a> を読み取る</li>
<li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/FileReader">FileReader API</a> を使って、ファイルを DataURI として読み込ませ、 img タグに表示させる</li>
<li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas">OffscreenCanvas</a> と <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvasRenderingContext2D">OffscreenCanvasRenderingContext2D</a> を経由して <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/API/ImageData">ImageData</a> を作成する</li>
<li>jsQR に処理を投げる</li>
</ol>
<p>といった処理になるだろうか。</p>
<p>早速実装してみよう。</p>
<h2 id="実装"><a href="#%E5%AE%9F%E8%A3%85">実装</a></h2>
<pre><code class="html"><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/[email protected]/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>
</code></pre>
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="GReBXOM" data-user="advanceboy" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a target="_blank" rel="nofollow noopener" href="https://codepen.io/advanceboy/pen/GReBXOM">
decode D&Ded QR images</a> by advanceboy (<a target="_blank" rel="nofollow noopener" href="https://codepen.io/advanceboy">@advanceboy</a>)
on <a target="_blank" rel="nofollow noopener" href="https://codepen.io">CodePen</a>.</span>
</p>
<p>いったん img タグで画像を表示させるワンクッションを置いているおかげで、 png, jpg のみならず svg 等の画像もデコードできるようになっている。</p>
<p>Edge や Chrome ブラウザであれば、表示されている画像をそのまま D&D してきてのデコードもできるぞ。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2024/02/decode-drag-and-droped-qr-image-01.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2024/02/decode-drag-and-droped-qr-image-01-1024x564.png" alt="" /></a></p>
<p>jsQR の処理は、 JavaScript のスレッドで行われるので、処理が重くなるとブラウザが固まってしまう。<br />
このため、理想的には Web Worker に処理を渡してしまうほうがよさそうではある。</p>
<p>ただ、1000x1000ピクセル程度の画像であれば、近年のデバイスなら一瞬でデコードできるので、実際のところはメインスレッド側で処理してしまって問題ないだろう。</p>
advanceboy