怎么使用 qrcode.js 生成带 Logo 的二维码

作者:白露

时间:2026-11-03

浏览:539

详细介绍如何使用 qrcode.js 配合 HTML5 Canvas 生成带中心 Logo 的二维码,解析二维码容错机制原理,提供完整的代码实现、安全尺寸与坐标计算方法及常见排错指南。

文章目录
技术教程 JavaScript / HTML5 前端可视化 阅读时间:约 8 分钟
怎么使用 qrcode.js 生成带 Logo 的二维码

在开发网页分享、移动端扫码登录或电子支付功能时,我们常需要把平台或商户的品牌 Logo 嵌入二维码正中央,以提高视觉辨识度。然而,主流开源库 qrcode.js 本身并没有提供直接传入 Logo 图片的配置项。如果单纯在 DOM 结构中用一个 <img> 标签以绝对定位强行盖在二维码上方,不仅极易出现未按比例缩放而遮蔽关键码点、导出保存时丢失 Logo 的问题,还会因为容错等级不足导致扫码设备无法识别。解决这一问题的规范做法是:将 qrcode.js 的容错率设置为最高的 H 级别(30% 纠错容量),利用 HTML5 Canvas 绘制底层二维码,随后在 Canvas 2D 上下文中计算精确的中心安全坐标,绘制白色隔离底衬并叠加 Logo 图像,最终输出为一张完整的、可识别的图像。

一、核心原理:为什么二维码中心能放置 Logo

二维码(Quick Response Code)之所以允许被遮挡一部分仍然能够被扫码枪和手机相机快速识别,归功于其底层采用的 里德-所罗门纠错编码(Reed-Solomon Error Correction) 算法。二维码在编码原始数据时,会根据设定的容错等级计算出一定比例的冗余纠错码字,并将这些纠错码交错分散保存在整个矩阵的各个码块中。

根据国际标准,二维码定义了四个容错等级:

  • Level L(Low):约 7% 的字码损坏时仍可被恢复。数据容量最大,容错能力最弱。
  • Level M(Medium):约 15% 的字码损坏时可恢复。这是许多二维码库(包括 qrcode.js)的默认配置。
  • Level Q(Quartile):约 25% 的字码损坏时可恢复。适合中度遮挡或恶劣工业环境。
  • Level H(High):约 30% 的字码损坏时可恢复。容错能力最强,但由于冗余数据占比较高,生成的点阵密度会相应变大。

当我们在二维码正中央覆盖一张 Logo 时,从解码器的角度来看,被 Logo 覆盖的区域等同于“损坏的码块”。如果覆盖面积加上为了防粘连而留出的白色保护边框达到了 10%~15%,若使用默认的 Level M,二维码的纠错配额就会被 Logo 消耗殆尽。一旦外部环境出现反光、镜头畸变或轻微褶皱,扫码识别就会立刻失败。因此,生成带 Logo 的二维码,第一道刚性前提就是强制指定容错等级为 QRCode.CorrectLevel.H,确保在中心被遮损 10% 左右后,周边仍有约 20% 的充裕容错余量应对实际环境。

怎么使用 qrcode.js 生成带 Logo 的二维码
图 1:二维码矩阵分区结构与不同容错等级对中心覆盖物的抗损能力对比

二、开发准备与技术选型

在开始编码之前,需要明确运行环境并准备好相关的静态资源:

1. 引入 qrcode.js 库

在传统的静态 HTML 页面中,可以通过 CDN 或者将编译好的 qrcode.min.js 放置在项目本地静态目录中直接引入:

<!-- 引入标准版 qrcode.js -->
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>

如果是在 Vue、React 或打包工具(如 Webpack、Vite)环境中使用,也可以通过 npm 安装包装库(如 qrcodejs2),或者直接使用原生 Canvas API 配合基础算法。

2. Logo 图像规范

  • 长宽比例:务必使用 1:1 的正方形图片。若使用矩形图标,会导致二维码在横向或纵向上遮挡不对称,极易破坏寻象区附近的校验码。
  • 图像格式:推荐使用带透明背景的 PNG 格式或 SVG 转换后的清晰位图。避免使用有杂色背景的 JPG 格式。
  • 素材尺寸:Logo 文件的原始像素建议在 150×150 像素到 300×300 像素之间。过小的图片合成到高清大图上会发虚,过大的图片则会增加移动端解码时的内存占用。

三、核心实现步骤与完整代码

要在最终生成的二维码中合成 Logo,标准的流水线分为四个关键操作阶段:

  1. 创建临时宿主容器:通过一个离屏或临时的 DOM 容器接收 qrcode.js 生成的二维码。
  2. 提取并绘制基础二维码:qrcode.js 生成完毕后,会向容器内部插入一个 <canvas>(在某些旧版或特殊情况下会回退到 <img>)。我们需要获取此 Canvas 图像作为底图。
  3. 异步加载 Logo 图像:通过 JavaScript 异步加载图片对象,并监听 onload 回调,确保图片完全解码后再进行绘制。
  4. 计算坐标并绘制底衬与 Logo:在 Canvas 的中心位置先绘制一个带有圆角的白色底衬框(比 Logo 略大一圈),再将 Logo 绘制在底衬中央,最后将整张图呈现给用户或提供保存下载。
怎么使用 qrcode.js 生成带 Logo 的二维码
图 2:带 Logo 二维码生成的全生命周期操作步骤分解

完整可执行代码示例

下面是一个经过模块化封装的完整实现文件。该代码将生成逻辑包装为 generateQRCodeWithLogo 函数,支持设置二维码内容、尺寸、Logo 路径、Logo 相对比例以及圆角边框样式:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>qrcode.js 生成带 Logo 二维码示例</title>
  <script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
  <style>
    body {
      font-family: sans-serif;
      display: flex;
      flex-direction: column;
      align-items: center;
      padding: 40px;
      background: #f5f7fa;
    }
    #qrcode-result {
      margin-top: 20px;
      box-shadow: 0 4px 16px rgba(0,0,0,0.1);
      border-radius: 8px;
      background: #fff;
      padding: 12px;
    }
    .action-btn {
      margin-top: 16px;
      padding: 8px 20px;
      background: #142537;
      color: #fff;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
  </style>
</head>
<body>

  <h2>qrcode.js 带 Logo 二维码生成演示</h2>
  <div id="qrcode-result"></div>
  <button class="action-btn" id="btn-download">下载二维码图片</button>

  <script>
    /**
     * 生成带 Logo 的二维码
     * @param {Object} options 配置项
     * @param {string} options.text 二维码内容
     * @param {number} options.size 二维码尺寸(像素)
     * @param {string} options.logoUrl Logo 图片地址(支持本地、Base64 或允许跨域的 URL)
     * @param {number} options.logoRatio Logo 尺寸占二维码总宽度的比例(推荐 0.2,最大不超过 0.25)
     * @param {HTMLElement} options.container 输出的目标 DOM 容器
     */
    function generateQRCodeWithLogo(options) {
      const {
        text,
        size = 280,
        logoUrl,
        logoRatio = 0.2,
        container
      } = options;

      // 1. 创建离屏隐藏容器,供 qrcode.js 进行初次渲染
      const tempHolder = document.createElement('div');

      // 2. 实例化 QRCode,注意必须指定 correctLevel 为 H (High)
      const qrInstance = new QRCode(tempHolder, {
        text: text,
        width: size,
        height: size,
        colorDark: "#000000",
        colorLight: "#ffffff",
        correctLevel: QRCode.CorrectLevel.H // 【关键】30% 容错率
      });

      // 3. qrcode.js 可能在渲染时使用内部计时器,使用延时以保证 DOM 内已生成 Canvas
      setTimeout(() => {
        const qrCanvas = tempHolder.querySelector('canvas');
        if (!qrCanvas) {
          console.error('未能获取到 qrcode.js 生成的 canvas 节点');
          return;
        }

        // 4. 创建最终展示的 Canvas
        const finalCanvas = document.createElement('canvas');
        finalCanvas.width = size;
        finalCanvas.height = size;
        const ctx = finalCanvas.getContext('2d');

        // 将底层二维码绘制到最终画布上
        ctx.drawImage(qrCanvas, 0, 0, size, size);

        // 如果未指定 Logo,则直接输出普通高容错二维码
        if (!logoUrl) {
          container.innerHTML = '';
          container.appendChild(finalCanvas);
          return;
        }

        // 5. 异步加载 Logo 图像
        const logoImg = new Image();
        logoImg.crossOrigin = 'Anonymous'; // 处理跨域以防画布污染
        logoImg.src = logoUrl;

        logoImg.onload = function() {
          // 计算 Logo 绘制尺寸与边距
          const logoSize = Math.floor(size * logoRatio);
          const logoX = Math.floor((size - logoSize) / 2);
          const logoY = Math.floor((size - logoSize) / 2);

          // 计算白色衬底参数(在 Logo 四周增加安全间隙)
          const padding = Math.max(3, Math.floor(logoSize * 0.08));
          const bgSize = logoSize + padding * 2;
          const bgX = logoX - padding;
          const bgY = logoY - padding;
          const borderRadius = Math.floor(bgSize * 0.15); // 优雅的微圆角

          // 绘制白色圆角背景垫层(隔离二维码像素,提升扫码识别率)
          ctx.save();
          drawRoundedRect(ctx, bgX, bgY, bgSize, bgSize, borderRadius);
          ctx.fillStyle = '#FFFFFF';
          ctx.fill();
          ctx.strokeStyle = '#E2E8F0';
          ctx.lineWidth = 1;
          ctx.stroke();
          ctx.restore();

          // 绘制 Logo 本身(带图片圆角裁切)
          ctx.save();
          drawRoundedRect(ctx, logoX, logoY, logoSize, logoSize, Math.max(2, borderRadius - 2));
          ctx.clip(); // 裁剪出圆角区域
          ctx.drawImage(logoImg, logoX, logoY, logoSize, logoSize);
          ctx.restore();

          // 输出到页面容器
          container.innerHTML = '';
          container.appendChild(finalCanvas);

          // 绑定下载按钮
          document.getElementById('btn-download').onclick = function() {
            const dataUrl = finalCanvas.toDataURL('image/png');
            const link = document.createElement('a');
            link.download = 'qrcode_with_logo.png';
            link.href = dataUrl;
            link.click();
          };
        };

        logoImg.onerror = function() {
          console.warn('Logo 图片加载失败,已降级输出纯二维码');
          container.innerHTML = '';
          container.appendChild(finalCanvas);
        };
      }, 50);
    }

    /**
     * 辅助函数:绘制圆角矩形路径
     */
    function drawRoundedRect(ctx, x, y, width, height, radius) {
      ctx.beginPath();
      ctx.moveTo(x + radius, y);
      ctx.lineTo(x + width - radius, y);
      ctx.arcTo(x + width, y, x + width, y + radius, radius);
      ctx.lineTo(x + width, y + height - radius);
      ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
      ctx.lineTo(x + radius, y + height);
      ctx.arcTo(x, y + height, x, y + height - radius, radius);
      ctx.lineTo(x, y + radius);
      ctx.arcTo(x, y, x + radius, y, radius);
      ctx.closePath();
    }

    // 页面加载后立即调用
    window.addEventListener('DOMContentLoaded', () => {
      // 演示用 SVG Base64 Logo(保证无外链依赖直接可用)
      const sampleLogo = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" rx="20" fill="%23142537"/><path d="M50 20 L80 35 L80 65 C80 80 65 90 50 95 C35 90 20 80 20 65 L20 35 Z" fill="%2377B9AE"/><path d="M42 52 L48 58 L62 44" fill="none" stroke="%23FFFFFF" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/></svg>';

      generateQRCodeWithLogo({
        text: 'https://example.com/login?token=secure123',
        size: 300,
        logoUrl: sampleLogo,
        logoRatio: 0.22,
        container: document.getElementById('qrcode-result')
      });
    });
  </script>
</body>
</html>

四、关键细节:尺寸计算与安全衬底规范

在带 Logo 的二维码开发中,许多开发者容易忽略坐标的微积分细节以及防粘连隔离机制,导致生成的二维码“好看但不好扫”。以下两项参数设置直接决定了识别率的成败:

1. Logo 尺寸的安全黄金比例

计算 Logo 宽度比例(logoRatio)时,切忌凭感觉随意拉伸。数学上,覆盖面积与单边尺寸呈平方比关系:

  • 若 Logo 边长占二维码边长的 20%(0.2),覆盖面积仅占 0.2 × 0.2 = 4%;
  • 若 Logo 边长占二维码边长的 25%(0.25),覆盖面积上升到 0.25 × 0.25 = 6.25%;
  • 若 Logo 边长占二维码边长的 35%(0.35),覆盖面积激增至 0.35 × 0.35 = 12.25%,加上衬底将达到 16% 以上,迫近纠错极限。

实操建议:将单边比例控制在 18% ~ 22% 之间,最大严禁超过 25%。这个范围既能保证人类肉眼清晰辨识出品牌图标,又把遮挡率牢牢控制在 H 级容错的舒适区内。

2. 为什么必须绘制白色隔离底衬

二维码依靠黑白像素点阵的交替变化来表示二进制“0”与“1”。如果 Logo 拥有不规则边缘或深色背景,直接贴在二维码上时,Logo 的深色边缘会与背景二维码的黑色码块融为一体,导致扫码算法发生粘连误判,错误地将 Logo 边缘解析成畸变的数据模块。在 Logo 下方先绘制一个外扩 3~6 像素的纯白色衬底,能够形成一道天然的“静区屏障”,确保二维码内部边界分明。

怎么使用 qrcode.js 生成带 Logo 的二维码
图 3:Canvas 坐标系中各层绘制起点与 Logo 安全区域的数学计算逻辑

五、常见问题与排错指南

在实际项目集成过程中,经常有开发者反馈“代码写了但图片出不来”或者“生成的二维码无法识别”。以下列出最常见的问题根源与针对性排查策略:

1. 图片跨域导致 Canvas 污染(Tainted Canvas)

故障现象:二维码和 Logo 在页面中显示正常,但当调用 canvas.toDataURL() 或尝试提供图片下载功能时,浏览器抛出致命报错:Uncaught DOMException: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.

解决办法:当 Logo 存储在独立的文件服务器或第三方 CDN 上时,必须为 Image 实例显式指定允许匿名跨域:

const logoImg = new Image();
logoImg.crossOrigin = 'Anonymous'; // 必须在设置 src 之前指定
logoImg.src = 'https://res.example.com/logo.png';

同时,存放 Logo 的静态资源服务器必须在响应头中包含 Access-Control-Allow-Origin: *,否则图片即使能够正常加载,也会将 Canvas 标记为“受污染”。

2. 异步时序错位导致 Logo 丢失

故障现象:导出的二维码中只有黑白码块,中间空空如也,完全没有绘制 Logo。

排查思路:这是前端初学者最易犯的异步时序错误。qrcode.js 生成 DOM 需要微小的计算时间,而 Image 对象的网络请求和解码更是异步事件。如果在 logoImg.src = url 下一行直接调用 ctx.drawImage(logoImg, ...),此时图片尚未完成下载解码,绘图操作会默默失效且不报错。所有 Logo 相关的绘制指令,必须严格包裹在 logoImg.onload = function() { ... } 回调函数中。

3. 手机与扫描器识别困难或完全不识别

排查矩阵:

  • 检查容错等级:确认 correctLevel 是否误写为了 QRCode.CorrectLevel.M 或 L。带 Logo 必须是 H。
  • 检查中心比例:Logo 单边长度是否超过了二维码总宽度的 25%。如果过大,请立即调小。
  • 检查是否漏掉了白色衬底:深色 Logo 边缘若与二维码黑色码块直接接触,扫描器无法分辨边界,必须加上纯白色 Padding 底框。
  • 检查寻象图形是否受损:三个顶角的“回”字形方块是扫描器校正透视与旋转的生命线,绝对不允许有任何衬底或投影侵入其中。

4. 高分屏(Retina)下图片模糊锯齿

优化方案:在 iPhone、Mac 等高 PPI 屏幕上,默认 1 个 CSS 像素对应 2 或 3 个物理像素。为了保证二维码和 Logo 放大时不失真,可以在实例化和绘制时将 Canvas 尺寸乘以 window.devicePixelRatio(例如放大 2 倍或 3 倍),而在 CSS 样式上将其宽度限制为逻辑像素宽度:

const dpr = window.devicePixelRatio || 2;
finalCanvas.width = size * dpr;
finalCanvas.height = size * dpr;
finalCanvas.style.width = size + 'px';
finalCanvas.style.height = size + 'px';
ctx.scale(dpr, dpr);
怎么使用 qrcode.js 生成带 Logo 的二维码
图 4:带 Logo 二维码全链路技术实施与上线交付核对矩阵

六、总结

使用 qrcode.js 生成带 Logo 的二维码,本质上是一个“高容错底层生成 + Canvas 2D 像素层叠加”的工程组合。虽然该库没有直接提供 Logo API,但通过结合 HTML5 Canvas,我们不仅能精准实现 Logo 的居中绘制与圆角美化,还可以自由扩展品牌文字、外边框及自定义背景等视觉特性。

在实际落地时,只要牢牢把握住三大铁律:一是容错率必须锁定为 Level H,为中央遮挡留下充裕的纠错空间;二是 Logo 边长严格控制在 20% 左右并保留白色隔离衬底,防止深色码块粘连;三是妥善处理图片异步加载与跨域标头,便能稳定高效地生成既符合品牌视觉规范、又具备极高扫码辨识度的优质二维码。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章