怎么使用 qrcode.js 生成带 Logo 的二维码
详细介绍如何使用 qrcode.js 配合 HTML5 Canvas 生成带中心 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% 的充裕容错余量应对实际环境。
二、开发准备与技术选型
在开始编码之前,需要明确运行环境并准备好相关的静态资源:
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,标准的流水线分为四个关键操作阶段:
- 创建临时宿主容器:通过一个离屏或临时的 DOM 容器接收
qrcode.js生成的二维码。 - 提取并绘制基础二维码:
qrcode.js生成完毕后,会向容器内部插入一个<canvas>(在某些旧版或特殊情况下会回退到<img>)。我们需要获取此 Canvas 图像作为底图。 - 异步加载 Logo 图像:通过 JavaScript 异步加载图片对象,并监听
onload回调,确保图片完全解码后再进行绘制。 - 计算坐标并绘制底衬与 Logo:在 Canvas 的中心位置先绘制一个带有圆角的白色底衬框(比 Logo 略大一圈),再将 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 像素的纯白色衬底,能够形成一道天然的“静区屏障”,确保二维码内部边界分明。
五、常见问题与排错指南
在实际项目集成过程中,经常有开发者反馈“代码写了但图片出不来”或者“生成的二维码无法识别”。以下列出最常见的问题根源与针对性排查策略:
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 的二维码,本质上是一个“高容错底层生成 + Canvas 2D 像素层叠加”的工程组合。虽然该库没有直接提供 Logo API,但通过结合 HTML5 Canvas,我们不仅能精准实现 Logo 的居中绘制与圆角美化,还可以自由扩展品牌文字、外边框及自定义背景等视觉特性。
在实际落地时,只要牢牢把握住三大铁律:一是容错率必须锁定为 Level H,为中央遮挡留下充裕的纠错空间;二是 Logo 边长严格控制在 20% 左右并保留白色隔离衬底,防止深色码块粘连;三是妥善处理图片异步加载与跨域标头,便能稳定高效地生成既符合品牌视觉规范、又具备极高扫码辨识度的优质二维码。
sigma.js 怎么实现节点点击、拖拽与悬停提示交互
2026-11-05 15:03:27
Matter.js 与 PixiJS/Three.js 集成:分离物理计算与渲染
2026-11-05 13:43:48
主流JS弹幕库选型指南:Danmaku.js等库的功能与体积对比
2026-11-05 09:28:19
JavaScript点击事件监听与交互效果实例
2026-11-05 09:10:19
自定义JS颜色选择器UI样式与交互细节
2026-11-05 09:09:06








