0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザだけでQRコードを生成・読み取りする(qrcode.js × BarcodeDetector/jsQR)

0
Posted at

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段構えにすると堅牢です。

  1. BarcodeDetector(ブラウザ標準API)が使えるなら、それを使う(ネイティブ実装で速い・ライブラリ不要)
  2. 無ければ 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」で特に効きます。参考になれば嬉しいです。


0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?