当サイトのQRコード作成ツールで、日本語を正しく読み取れない不具合がありました。原因はライブラリ「qrcode-generator」の既定の文字変換です。設定の直し方(1行)と、直したあとに確かめる方法を、実測つきで紹介します。
英数字のURLなら何も起きないので、気づきにくい不具合です。

使っているもの
| ライブラリ | 役割 | 読み込み元 |
|---|---|---|
| qrcode-generator 1.4.4 | QRコードを作る | cdnjs |
| jsQR 1.4.0 | できたQRコードを読み戻して確かめる | jsDelivr(cdnjsには無い) |
どちらもnpmなしで、scriptタグ1本で使えます。この記事の実測は、誤り訂正レベルM・Byteモード・セル4pxの条件です。
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js"></script>
最小のコード
qrcode-generatorの基本は「作る → 文字を入れる → 確定する → 描く」の4行です。描画先の要素を1つ用意しておきます。
<div id="out"></div>
<script>
var qr = qrcode(0, 'M'); // 0 = サイズ自動、'M' = 誤り訂正レベル
qr.addData('https://tozji.com/'); // モード省略時は Byte
qr.make();
document.getElementById('out').innerHTML = qr.createImgTag(4, 16);
</script>
第1引数の0は「必要な大きさを自動で決める」、第2引数は誤り訂正レベル(L・M・Q・H)です。addDataの第2引数でモード(Numeric・Alphanumeric・Byte・Kanji)を指定でき、省略するとByteです。createImgTagのほかに、createSvgTagやcreateDataURLもあります。ここまでは公式の説明どおりで、英数字のURLなら何も問題は起きません。
症状:日本語が正しく読み取れない
addData('こんにちは')で作ったQRコードは、見た目はふつうにできあがります。エラーも出ません。ところが読み取ると、入れた文字が出てきません。jsQRで確かめると、「こんにちは」「日本語」「東京都」は空文字、「あいうえお」は「BDFHJ」と読まれました。文字列と読み取り側によって、空になることも、別の文字になることもあります。当サイトのツールでは、公開後にレビューで指摘されて気づきました。
原因:既定の文字変換が、多バイト文字を想定していない
qrcode-generatorは、文字列をバイトの並びに変換してからQRコードに詰めます。この変換を担当しているのがqrcode.stringToBytesという関数で、公式のREADMEにはこう書かれています。
Encodes a string into an array of number(byte) using any charset. This function is used by internal. Overwrite this function to encode using a multibyte charset.
つまり既定の変換は1文字を1バイトとして扱うので、UTF-8で3バイトになる日本語はここで壊れます。「多バイト文字を使うならこの関数を差し替えてください」というのが、ライブラリの設計です。
実際に確かめると、既定のqrcode.stringToBytes('こんにちは')は5バイトを返します。先頭は83(16進で0x53)。「こ」の文字コードは0x3053なので、下位の1バイトだけが残って上位が捨てられている形です。「あいうえお」(0x3042・0x3044・0x3046・0x3048・0x304A)なら下位バイトは0x42・0x44・0x46・0x48・0x4A、つまりASCIIの「BDFHJ」。さっきの読み取り結果と一致します。「こんにちは」から作られた5バイトは、UTF-8として正しく解釈できない並びだったため、jsQRでは空文字になりました。
直し方:1行で、UTF-8の変換に差し替える
ライブラリの中に、UTF-8用の変換関数がすでに入っています。qrcode.stringToBytesFuncs['UTF-8']です。この関数をqrcode.stringToBytesに代入します。
// ライブラリを読み込んだあと、qrcode() を呼ぶ前に1回だけ
qrcode.stringToBytes = qrcode.stringToBytesFuncs['UTF-8'];
var qr = qrcode(0, 'M');
qr.addData('こんにちは');
qr.make();
これで「こんにちは」がそのまま読み取れるようになります。住所、日本語のURL、絵文字も同じです。
はまりどころ2つ
その1:代入する相手を間違えると、効かない
差し替える先は、qrcode(...)で作ったオブジェクトではなく、qrcodeという関数そのものです。作ったオブジェクトの側にstringToBytesを代入しても、内部の変換には使われません。qr.stringToBytesへの代入も試しましたが、読み取り結果は変わりませんでした。
// 効かない
var qr = qrcode(0, 'M');
qr.stringToBytes = qrcode.stringToBytesFuncs['UTF-8'];
// 効く
qrcode.stringToBytes = qrcode.stringToBytesFuncs['UTF-8'];
var qr = qrcode(0, 'M');
その2:日本語は、同じ文字数でもQRコードが大きくなる
UTF-8では、一般的なひらがな・カタカナ・漢字は1文字3バイトです(拡張漢字や絵文字など4バイトの文字もあります)。差し替え後のstringToBytes('こんにちは')は15バイトを返します。英数字の3倍の情報量になるので、同じ文字数でもQRコードのマス目が増えます。手元で測ると(誤り訂正M・Byteモード)、「あいうえお」(5文字)は一辺25マス、「abcde」(5文字)は21マスで、英字なら15文字「abcdefghijklmno」を入れたときと同じ大きさでした。
容量オーバーと読み取り失敗では、対処が異なります。
- 容量オーバー(
make()がcode length overflowで止まる): 文字数と誤り訂正レベルの組み合わせがライブラリの上限を超えています。文字数を減らすか、誤り訂正レベルを下げます。 - ロゴを重ねた後に読み取れない: 隠れたマスが誤り訂正の許容を超えています。ロゴの大きさ、余白、コントラスト、誤り訂正レベルを確認します。ロゴを小さくしても文字の容量は増えません。
当サイトのツールは、ロゴを付けると誤り訂正をHに固定します。ロゴを外し、誤り訂正レベルをHより下げれば、入る量を増やせます。ツールの案内は、容量オーバーと読み取り失敗で別の文言にしています。
確かめ方:作ったQRコードを、その場で読み戻す
生成したQRコードをjsQRで読み戻し、入力した文字列と一致するか確認します。canvasに描いて、jsQRに渡します。
var text = 'こんにちは';
var qr = qrcode(0, 'M');
qr.addData(text);
qr.make();
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var n = qr.getModuleCount();
var cell = 4;
canvas.width = canvas.height = n * cell;
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
qr.renderTo2dContext(ctx, cell);
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
var result = jsQR(img.data, img.width, img.height);
console.log(result && result.data === text ? '一致' : '不一致: ' + (result ? result.data : '読み取れない'));
当サイトのQRコード作成ツールには、この往復をボタン1つで行う「テスト」機能を付けています。日本語・住所・絵文字・日本語のURLで、読み戻しが一致することを確認済みです。スマホのカメラでの読み取りは、アプリごとに文字コードの扱いが違うことがあるので、渡す前に実機でも一度確かめてください。

まとめ
qrcode-generatorで日本語が正しく読み取れないのは、既定のstringToBytesが多バイト文字を想定しておらず、下位1バイトだけを詰めるからです。qrcode.stringToBytes = qrcode.stringToBytesFuncs['UTF-8'];の1行を、ライブラリ読み込み後・qrcode()の前に入れれば直ります。代入先は関数そのもの。直したら、jsQRで読み戻して入力と一致するか確かめる。
この組み合わせで動いているのがQRコード作成ツールです。読み取り側の仕組みはQRコード読み取りツールで使っています。
参考: qrcode-generator(GitHub・JavaScript README)/cdnjs: qrcode-generator/jsQR(GitHub)


