フロントとバックエンドで揃えるQRコード生成:vue-qrcodeとphp-qrcode

概要

Webアプリで「QRコードを表示する」要件は、実は2つの異なる場所で発生することが多いです。

  • 画面上で即座に見せたい(チケット画面、会員証、受付用の動的表示)→ フロントエンド
  • PDF・メール・帳票に焼き込みたい/サーバに保存したい(控え、入館証の印刷、通知メール添付)→ バックエンド

この2つを別々に実装すると、「画面で読めたQRが、PDFに出したら別物になって読めない」「スキャナによって片方だけ失敗する」といった地味なトラブルが起きます。原因はたいてい、フロントとバックエンドでQRの生成パラメータが揃っていないことです。

この記事では、フロントに vue-qrcode、バックエンドに php-qrcode(chillerlan/php-qrcode)を使い、両者の出力を「揃える」観点でハンズオンします。

環境

  • Vue 3 + Vite
  • @chenfengyuan/vue-qrcode(内部で qrcode を利用)
  • PHP 8.2+
  • chillerlan/php-qrcode
# フロント
npm install @chenfengyuan/vue-qrcode qrcode

# バックエンド
composer require chillerlan/php-qrcode

> chillerlan/php-qrcode はメジャーバージョンでAPIが変わります。いま composer require すると v6 系(最低 PHP 8.2)が入ります。本記事は現行の v6 系の表記を基本にし、v4 / v5 からの移行点は末尾に補足します。導入時は composer show chillerlan/php-qrcode でバージョンを確認してください。

QRコードは「同じ文字列=同じ見た目」ではない

よくあるのが次のパターンです。

  1. フロントは vue-qrcode のデフォルト設定で表示している
  2. バックエンドは php-qrcode で、別のデフォルトで画像を生成している
  3. 同じ文字列を渡しているのに、生成されるQRのバージョン(セルの密度)や余白が異なる
  4. 低品質なプリンタや古いスキャナで、片方だけ読み取り失敗する

QRコードの出力は、以下のパラメータで変わります。

  • 誤り訂正レベル(ECC Level): L / M / Q / H
  • エンコードモード: 数字 / 英数字 / バイト(UTF-8文字列はバイトモード)
  • マージン(Quiet Zone): 周囲の余白セル数
  • バージョン(型番): データ量とECCレベルから決まるセルの大きさ

両ライブラリはそれぞれ独自のデフォルト値を持つため、明示指定しない限り揃うとは限りません。対策はシンプルで、生成パラメータを一元管理し、双方で同じ値を明示的に指定することです。

揃えるべき3つのパラメータ

パラメータ 推奨の揃え方
誤り訂正レベル 用途で統一(印刷が絡むなら M または Q を推奨)
マージン 両者で同じセル数(例: 4)
エンコード UTF-8文字列はバイトモード(両者ともデフォルトでバイト)

誤り訂正レベルの目安:

  • L: 約7%復元。画面表示でデータ量を抑えたいとき
  • M: 約15%復元。汎用的なデフォルト
  • Q: 約25%復元。印刷・屋外・ロゴ埋め込み向け
  • H: 約30%復元。過酷な環境向け(セルが密になる)

実装例

フロントエンド(Vue 3 + vue-qrcode)

// main.js
import { createApp } from 'vue'
import VueQrcode from '@chenfengyuan/vue-qrcode'
import App from './App.vue'

const app = createApp(App)
app.component(VueQrcode.name, VueQrcode)
app.mount('#app')
<!-- TicketQr.vue -->
<script setup>
import { computed } from 'vue'

const props = defineProps({
  payload: { type: String, required: true },
})

const qrOptions = computed(() => ({
  width: 240,                // 表示ピクセル幅
  margin: 4,                 // Quiet Zone(セル数)
  errorCorrectionLevel: 'M', // L / M / Q / H
}))
</script>

<template>
  <vue-qrcode :value="payload" :options="qrOptions" />
</template>

ポイントは margin と errorCorrectionLevel をデフォルト任せにせず明示することです。width は表示サイズであり、QRの構造(バージョン)そのものには影響しません。構造を決めるのは文字列長とECCレベルです。

バックエンド(PHP + php-qrcode / v6系)

<?php
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use chillerlan\QRCode\Common\EccLevel;
use chillerlan\QRCode\Output\QRGdImagePNG;

final class QrService
{
    public function pngDataUri(string $payload): string
    {
        $options = new QROptions([
            'eccLevel'        => EccLevel::M,         // フロントと揃える
            'quietzoneSize'   => 4,                    // margin: 4 と揃える
            'outputInterface' => QRGdImagePNG::class,  // GDでPNG出力(出力はクラスで指定)
            'scale'           => 6,                     // 1セルあたりのpx(印刷解像度に効く)
        ]);

        // v5 以降、render() はデフォルトで base64 data URI を返す
        return (new QRCode($options))->render($payload); // "data:image/png;base64,..."
    }
}

v6 の QROptions では、出力形式は outputType 定数ではなく outputInterface に出力クラス(ここでは QRGdImagePNG::class)を渡して指定します。また v6 の QRCode コンストラクタは iterable を受け取れるため、オプションを配列のまま直接渡すこともできます(内部で QROptions に変換されます)。

// QROptions を介さず、配列をそのまま渡す書き方(v6)
$dataUri = (new QRCode([
    'eccLevel'        => EccLevel::M,
    'quietzoneSize'   => 4,
    'outputInterface' => QRGdImagePNG::class,
    'scale'           => 6,
]))->render($payload);

render() は既定で data:image/png;base64,... 形式の data URI を返します。生のバイナリ(PNGそのもの)が欲しい場合は 'outputBase64' => false を指定します。HTMLに <img src> で埋め込むなら data URI のままが扱いやすく、ファイル保存やレスポンスに直接流す場合は outputBase64=false が便利です。

設定を一箇所にまとめる

フロントとバックエンドでマジックナンバーが散らばると、片方だけ変えて事故ります。「QR仕様」を共有の設定として扱いましょう。

// config/qr.php など
return [
    'ecc'    => 'M',  // L / M / Q / H
    'margin' => 4,
    'scale'  => 6,
];

フロント側もビルド時の環境変数や共通JSONから同じ値を読み込ませておくと、「QRの仕様はここだけ見ればいい」状態にできます。

補足:v4 / v5 からの移行

古い記事やコードでは別の書き方を見かけます。バージョンによってAPIが変わっている点に注意してください。

  • v4 系: eccLevel に QRCode::ECC_M、出力形式に QRCode::OUTPUT_IMAGE_PNG を指定していました。これらの定数は v5 以降で廃止されており、現行では動きません。
  • v5 系: 出力クラスが刷新され、ECCレベルは EccLevel::M、出力形式は QROutputInterface::GDIMAGE_PNG のような定数で指定するようになりました。あわせて render() のデフォルト出力が base64 data URI 化しました。
  • v6 系: outputType 定数は廃止され、outputInterface に出力クラス(QRGdImagePNG::class など)を渡す形になりました。コンストラクタが iterable を受け取るようになり、最低要件も PHP 8.2 に上がっています。

確認方法

  1. 同一文字列で両者を生成し、並べてセル密度(バージョン)が同じか目視する
  2. フロントの描画QRとバックエンド生成のPNGを同じスマホアプリで読み取り、同一のデコード結果になるか確認する
  3. 印刷して(白黒レーザー等)読み取れるか確認する。画面では読めても印刷で読めないケースがある
  4. 長めのペイロード(URL+トークンなど)で、折り返し・バージョン上がりの挙動を確認する

注意点

  • ロゴ埋め込みをするならECCレベルを上げる(Q以上推奨)。中央にロゴを乗せる=一部セルを潰すので、復元能力が必要
  • ペイロードは短く保つ。長いURLはバージョンが上がりセルが細かくなり、印刷・小型表示で読み取り難度が上がる。短縮URLやIDのみを入れて詳細はサーバで引く設計が安全
  • 機微情報を直接QRに入れない。QRは暗号化ではなく「読み取り可能な文字列」。生のトークンや個人情報ではなく、ワンタイム性のある識別子を入れる
  • PNGを都度生成するとコストになる場合がある。静的なものはキャッシュやオブジェクトストレージへの保存を検討
  • バージョン差でAPIが変わるライブラリなので、アップデート時は生成結果を必ず再確認する

まとめ

  • QRは「同じ文字列=同じ見た目」ではない。ECCレベル・マージン・エンコードで変わる
  • フロント(vue-qrcode)とバックエンド(php-qrcode)はデフォルト任せにせず、同じパラメータを明示指定して揃える
  • 仕様値(ECC・margin・scale)は一箇所に集約し、両環境から同じ値を参照する
  • 画面で読めても印刷で読めないことがあるので、印刷確認までやる

フロントの即時表示とバックエンドの帳票・保存を両立する場面は多いです。最初に「QR仕様」を決めて両者で揃えておくと、後からの読み取りトラブルをまとめて防げます。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。