URLや文字列をQRコードにしたい、逆にQRの中身を確認したい——こういう地味な作業、ついWebのQR生成サイトに貼ってしまいがちです。でも、入力した文字列(URL・連絡先・Wi-Fiパスワードなど)を外部サーバーに送るのは、内容によってはちょっと気になります。
この記事では、ライブラリを使ってQRコードの「生成」と「読み取り」をブラウザの中だけで完結する方法をまとめます。どちらもサーバー不要・アップロードなしで動きます。日本語が化ける落とし穴や、読み取りが失敗しやすいポイントも残しておきます。
ゴール:文字列 → QR画像(PNG/SVG)を生成、QR画像 → 中身のテキスト、どちらもブラウザ内で完結できるようになること。
生成:qrcode.js で文字列をQRにする
QRコード生成は自前で実装すると誤り訂正やマスクパターンが面倒なので、軽量ライブラリに任せます。ここでは定番の qrcode-generator(qrcode.js) を使います。数十KBで依存なし、canvasにもSVGにも描けるのが便利です。
基本の流れはこれだけ。
// qrcode(typeNumber, errorCorrectionLevel)
// typeNumber = 0 で内容量に合わせて自動サイズ
const qr = qrcode(0, "M"); // 誤り訂正レベル L / M / Q / H
qr.addData("https://example.com");
qr.make();
const count = qr.getModuleCount(); // 一辺のマス数
あとは qr.isDark(row, col) で各マスが黒かどうかを取り、canvasに塗っていきます。
function render(qr, scale, margin, fg, bg) {
const count = qr.getModuleCount();
const px = (count + margin * 2) * scale;
const cv = document.getElementById("cv");
cv.width = cv.height = px;
const ctx = cv.getContext("2d");
ctx.fillStyle = bg; ctx.fillRect(0, 0, px, px); // 背景(余白ごと)
ctx.fillStyle = fg;
for (let r = 0; r < count; r++) {
for (let c = 0; c < count; c++) {
if (qr.isDark(r, c)) {
ctx.fillRect((c + margin) * scale, (r + margin) * scale, scale, scale);
}
}
}
}
margin(周囲の余白=クワイエットゾーン)は最低4マスが仕様上の推奨です。ここを削るとスキャン成功率が落ちるので、削りすぎ注意。
落とし穴①:日本語が化ける(UTF-8で入れる)
qrcode.js は文字列をバイト列に変換するとき、デフォルトが日本語を想定していないことがあります。URLや英数字だけなら問題になりませんが、日本語やマルチバイト記号を入れると文字化けした状態でエンコードされてしまいます。
対策は、バイト変換を UTF-8 で行うよう上書きしてしまうこと。
// 文字列を常にUTF-8バイトとしてエンコードする
qrcode.stringToBytes = function (s) {
const b = new TextEncoder().encode(s), a = [];
for (let i = 0; i < b.length; i++) a.push(b[i]);
return a;
};
TextEncoder は必ずUTF-8を返すので、これで日本語・絵文字も正しくQR化できます。読み取り側(スマホのカメラなど)もUTF-8前提で解釈するものが多いので、素直にUTF-8で統一するのが安全です。
PNG / SVG で書き出す
canvasに描いてあれば、PNG保存は toBlob 一発です。
document.getElementById("cv").toBlob((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = "qrcode.png";
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}, "image/png");
印刷やロゴ用途できれいに拡大したいなら、canvas(ラスタ)ではなくSVG(ベクタ)で出すのがおすすめです。マスを <rect> として並べるだけ。
function toSVG(qr, scale, margin, fg, bg) {
const count = qr.getModuleCount();
const dim = count + margin * 2;
const size = dim * scale;
let rects = "";
for (let r = 0; r < count; r++) {
for (let c = 0; c < count; c++) {
if (qr.isDark(r, c)) {
rects += `<rect x="${(c + margin) * scale}" y="${(r + margin) * scale}" width="${scale}" height="${scale}"/>`;
}
}
}
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" shape-rendering="crispEdges">`
+ `<rect width="${size}" height="${size}" fill="${bg}"/>`
+ `<g fill="${fg}">${rects}</g></svg>`;
}
shape-rendering="crispEdges" を付けると、拡大してもマスの境界がぼやけません。ベクタなので何倍にしても劣化しないのが強みです。
読み取り:まず BarcodeDetector、無ければ jsQR
読み取りは2段構えにすると堅牢です。
-
BarcodeDetector(ブラウザ標準API)が使えるなら、それを使う(ネイティブ実装で速い・ライブラリ不要) - 無ければ jsQR にフォールバック(WASM不要の軽量ライブラリ)
BarcodeDetector はChrome系では使えますが、SafariやFirefoxでは未対応のことがあるので、フォールバックを用意しておくと安心です。
共通の下準備として、画像をcanvasに描いてピクセルデータ(getImageData)を取り出します。
function toImageData(img, maxDim = 1600) {
// 大きすぎる画像は縮小(速度と精度のバランス)
const scale = Math.min(1, maxDim / Math.max(img.width, img.height));
const w = Math.max(1, Math.round(img.width * scale));
const h = Math.max(1, Math.round(img.height * scale));
const cv = document.createElement("canvas");
cv.width = w; cv.height = h;
const ctx = cv.getContext("2d");
ctx.drawImage(img, 0, 0, w, h);
return ctx.getImageData(0, 0, w, h);
}
読み取り本体。
async function decodeQR(img) {
// ① BarcodeDetector が使えるなら優先
if ("BarcodeDetector" in window) {
try {
const detector = new BarcodeDetector({ formats: ["qr_code"] });
const codes = await detector.detect(img); // <img>/canvas/Blobを渡せる
if (codes.length) return codes[0].rawValue;
} catch (_) { /* 失敗したら jsQR に落とす */ }
}
// ② フォールバック:jsQR
const data = toImageData(img);
const code = jsQR(data.data, data.width, data.height, {
inversionAttempts: "attemptBoth", // 白黒反転したQRも試す
});
return code && code.data ? code.data : null;
}
inversionAttempts: "attemptBoth" が地味に効きます。背景が黒・コードが白のような反転デザインのQRは、これを指定しないと読めないことがあります。
落とし穴②:読み取りが失敗するとき
読み取れないときの原因はだいたいこのあたりです。
- QRが小さく写りすぎ。 写真全体の中でQRが小さいと厳しい。QRが大きくはっきり写るよう切り抜いてから渡す。
-
縮小しすぎ。 高速化のために縮小するのは良いが、やりすぎるとマスが潰れる。
maxDimは1000〜1600くらいが無難。 -
反転・低コントラスト。
attemptBothを付ける。それでもダメなら前処理で二値化する手もある。 -
URLだけ特別扱いしたい。 読み取れた文字列が
https?://で始まるなら、そのままリンクとして開けるようにしておくと親切。ただし中身は信用しない(後述)。
function showResult(text) {
document.getElementById("val").textContent = text;
const link = document.getElementById("openLink");
if (/^https?:\/\//i.test(text)) {
link.href = text; // URLならリンク化
link.rel = "noopener noreferrer"; // 開く先に window.opener を渡さない
link.style.display = "inline-block";
} else {
link.style.display = "none";
}
}
ドラッグ&ドロップ / 貼り付けに対応する
QR読み取りは「スクショを貼るだけ」で使えると一気に便利になります。ドロップとクリップボード貼り付けの両対応はこれだけ。
const dz = document.getElementById("drop");
["dragenter", "dragover"].forEach((e) =>
dz.addEventListener(e, (ev) => { ev.preventDefault(); dz.classList.add("drag"); }));
["dragleave", "drop"].forEach((e) =>
dz.addEventListener(e, (ev) => { ev.preventDefault(); dz.classList.remove("drag"); }));
dz.addEventListener("drop", (ev) => {
if (ev.dataTransfer?.files) pick(ev.dataTransfer.files);
});
// クリップボードから画像を貼り付け(Ctrl+V)
document.addEventListener("paste", (ev) => {
const items = ev.clipboardData?.items;
if (!items) return;
for (const it of items) {
if (it.type.indexOf("image") === 0) { pick([it.getAsFile()]); break; }
}
});
セキュリティの注意:読み取ったURLを信用しない
QRの中身は任意の文字列です。読み取ったURLが安全とは限らないので、
-
自動で遷移しない。
location.href = 読み取り結果のような自動ジャンプはしない。ユーザーがリンクを見て自分でクリックする形にする。 -
リンクには
rel="noopener noreferrer"を付ける。 - URL以外(
javascript:スキームなど)はリンク化しない(上の例ではhttps?://のみリンク化している)。
「読み取れた文字列を画面に出すだけ」に留め、遷移の判断は人に委ねるのが安全です。
まとめ
-
生成は
qrcode.jsでaddData → make → isDarkをcanvasに描くだけ。日本語はstringToBytesをUTF-8で上書きして化けを防ぐ - 余白(クワイエットゾーン)は4マス以上。印刷用途は**SVG+
crispEdges**で劣化なし -
読み取りは
BarcodeDetector(標準API)を優先し、無ければ jsQR にフォールバック - jsQRは
inversionAttempts: "attemptBoth"で反転QRにも強くなる。大きすぎる画像はmaxDimで縮小 - 読み取ったURLは信用しない:自動遷移しない・
rel="noopener noreferrer"・スキームを絞る
生成も読み取りも、実はブラウザ内で完結できます。入力内容をどこにも送らずに済むので、URL短縮やWi-Fi情報など「中身をあまり外に出したくないQR」で特に効きます。参考になれば嬉しいです。