前言
qrcodejs 是用于制作二维码的 javascript 库,没有依赖项。它支持 HTML5 Canvas 和 DOM 中的 table 标签的跨浏览器。
在阅读 qrcodejs 源码之前,希望同学们已经对二维码的生成原理已有所了解。若是还未读过有关二维码的相关文章,这里为大家准备了传送门。这篇文章对二维码的生成原理做了详细的介绍,虽然是英文的,但并不妨碍阅读,毕竟浏览器都有一键翻译的呀!
源码解析
建议同学们在阅读文章时,下载源码,边调试边看。
QRCode——创建二维码的类
用过 qrcodejs 库的都知道,它对外暴露了一个类——QRCode,用于创建二维码。它的主要作用是:处理用户传入的配置信息和调用真正绘制二维码的函数。 下面是它的代码实现。
QRCode = function (el, vOption) {
// 默认配置项
this._htOption = {
width: 256, // 宽
height: 256, // 高
typeNumber: 4, // 二维码版本号
colorDark: "#000000", // 背景色
colorLight: "#ffffff", // 前景色
correctLevel: QRErrorCorrectLevel.H, // 纠错级别 - [L|M|Q|H]
};
// vOption 是字符串
if (typeof vOption === "string") {
vOption = {
text: vOption,
};
}
// 重写配置项
if (vOption) {
for (var i in vOption) {
this._htOption[i] = vOption[i];
}
}
// 获取元素
if (typeof el == "string") {
el = document.getElementById(el);
}
// 使用 SVG 绘制(感兴趣的同学可以查看源码,这里仅以 canvas 绘制为主)
if (this._htOption.useSVG) {
Drawing = svgDrawer;
}
this._android = _getAndroid(); // 获取 android 版本号
this._el = el; // 元素
this._oQRCode = null; // 初始为空
// 创建绘制二维码的对象,一般情况下都是使用 canvas 绘制
this._oDrawing = new Drawing(this._el, this._htOption);
// 若是没有传入 URL,则不绘制
if (this._htOption.text) {
// 制作二维码,makeCode 是 QRCode 原型上的方法
this.makeCode(this._htOption.text);
}
};
在使用 new QRCode()
生成二维码时,我们可以将其过程大致分为三个步骤:
- 处理配置信息,即判断用户是否传入配置信息,若有就使用,否则就用默认的值。
- 生成绘制二维码的对象。绘制方式分为 Canvas 和 table表格,代码内部会根据环境来判断使用那种方式(这里不做过多介绍,感兴趣的可以查看源码)。
- 调用 QRCode.prototype.makeCode 方法制作二维码。
- 调用 QRCode.prototype.makeImage 方法生成图片 url。
makeCode——制作二维码
QRCode.prototype.makeCode 是真正生成二维码的函数。下面,我将按照至上而下的顺序介绍其内部功能的作用。
QRCode.prototype.makeCode = function (sText) {
// 创建二维码实例对象
this._oQRCode = new QRCodeModel(
_getTypeNumber(sText, this._htOption.correctLevel), // 二维码版本号
this._htOption.correctLevel // 纠错级别
);
this._oQRCode.addData(sText); // 添加数据源,就是 URL 地址
this._oQRCode.make(); // 生成 QRCode 数据
this._el.title = sText;
this._oDrawing.draw(this._oQRCode); // 绘制 QRCode
this.makeImage(); // 生成图片
};
QRCodeModel 类
创建二维码实例对象。它的原型上挂载了制作二维码的方法,我会在接下来的解析中介绍。
function QRCodeModel(typeNumber, errorCorrectLevel) {
this.typeNumber = typeNumber; // 二维码版本号
this.errorCorrectLevel = errorCorrectLevel; // 纠错级别
this.modules = null; // 二维码码元数组
this.moduleCount = 0; // 二维码码元个数
this.dataCache = null; // 缓存数据
this.dataList = []; // 存储生成的二维码数据
}
_getTypeNumber
二维码存在 40 种版本(Version),这个函数就是用来计算二维码版本(通过转义后的字符串长度)的。此函数最终返回的 nType,就是所需要的二维码版本。
function _getTypeNumber(sText, nCorrectLevel) {
var nType = 1;
var length = _getUTF8Length(sText); // 获取转义后的字符串长度
for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) {
var nLimit = 0;
// 获取纠错级别对应的二进制字符数
// QRCodeLimitLength 中存储了各个二维码版本纠错级别对应的二进制字符数
switch (nCorrectLevel) {
case QRErrorCorrectLevel.L:
nLimit = QRCodeLimitLength[i][0];
break;
case QRErrorCorrectLevel.M:
nLimit = QRCodeLimitLength[i][1];
break;
case QRErrorCorrectLevel.Q:
nLimit = QRCodeLimitLength[i][2];
break;
case QRErrorCorrectLevel.H:
nLimit = QRCodeLimitLength[i][3];
break;
}
if (length <= nLimit) {
break;
} else {
nType++;
}
}
if (nType > QRCodeLimitLength.length) {
throw new Error("Too long data");
}
return nType; // nType 代表二维码版本
}
_getUTF8Length
这个函数主要是使用 encodeURI 来转义字符串,然后返回转义后的字符串长度。
function _getUTF8Length(sText) {
// 参考示例:encodeURI('mytes?你') ==> 'mytes?%E4%BD%A0'
var replacedText = encodeURI(sText)
.toString()
// 将 %20 这样转义后的字符都替换成 a
.replace(/\%[0-9a-fA-F]{2}/g, "a");
// 对于字符串转义后与原字符串不相等,需要加 3,个人认为:
// 转义后的字符串长度与计算二维码的版本有关。当其与原字符串长度不相等
// 时,则说明原字符串中有汉字(或特殊符号)。这里加上3,可能是考虑到
// 二维码版本越大,存储的数据就越大,所以宁大勿小,不然会导致信息缺失。
return replacedText.length + (replacedText.length != sText ? 3 : 0);
}
想必大家已注意到 QRCodeLimitLength 这个数组变量。它存储了二维码40个版本的纠错级别对应的二进制字符数。这些二进制字符数是固定的。下面列出的是二维码部分版本的二进制字符数。
// 为方便展示,这里仅列出了部分数据,大家可以下载源码查看具体数据。
var QRCodeLimitLength = [
[17, 14, 11, 7], // 版本 1
[32, 26, 20, 14], // 版本 2
[53, 42, 32, 24], // 版本 3,下面的以此类推
[78, 62, 46, 34],
[106, 84, 60, 44],
[134, 106, 74, 58],
[154, 122, 86, 64],
[192, 152, 108, 84],
// 略 ...
];
图中红色框的数字(仅截取了部分数据),就是关于二维码各个版本的纠错级别对应的二进制字符数。若是想查看全部数据,请点击详细介绍。
addData——添加数据
QRCodeModel 原型上的方法,主要是将 url 数据从 Unicode 编码转为 UTF-8 编码,然后添加到数组 dataList 中。
addData: function (data) {
// Unicode 编码转为 UTF-8 编码
var newData = new QR8bitByte(data);
// 将转义后的数据添加到 dataList 中
this.dataList.push(newData);
// 缓存数据,初始为 null
this.dataCache = null;
},
// 支持 UTF-8(Unicode 转 UTF-8)
function QR8bitByte(data) {
this.mode = QRMode.MODE_8BIT_BYTE; // 二维码编码模式
this.data = data; // data,就是 url
this.parsedData = []; // 存储数据
// UTF-8是一种变长的编码方法,字符长度从1个字节到4个字节不等。
// 越是常用的字符,字节越短,最前面的128个字符,只使用1个字节表示,与ASCII码完全相同。
for (var i = 0, l = this.data.length; i < l; i++) {
var byteArray = [];
// charCodeAt() 方法可返回指定位置的字符的 Unicode 编码,
// 返回值是 0 - 65535 之间的整数,表示给定索引处的 UTF-16 代码单元。
var code = this.data.charCodeAt(i);
// 十六进制转十进制:0x10000 ==> 65535 0x800 ==> 2048 0x80 ==> 128
if (code > 0x10000) { // 4个字节
byteArray[0] = 0xf0 | ((code & 0x1c0000) >>> 18); // 第 1 个字节
byteArray[1] = 0x80 | ((code & 0x3f000) >>> 12); // 第 2 个字节
byteArray[2] = 0x80 | ((code & 0xfc0) >>> 6); // 第 3 个字节
byteArray[3] = 0x80 | (code & 0x3f); // 第 4 个字节
} else if (code > 0x800) { // 3个字节
byteArray[0] = 0xe0 | ((code & 0xf000) >>> 12);
byteArray[1] = 0x80 | ((code & 0xfc0) >>> 6);
byteArray[2] = 0x80 | (code & 0x3f);
} else if (code > 0x80) { // 2个字节
byteArray[0] = 0xc0 | ((code & 0x7c0) >>> 6);
byteArray[1] = 0x80 | (code & 0x3f);
} else { // 1个字节
byteArray[0] = code;
}
this.parsedData.push(byteArray);
}
// 数组扁平化
this.parsedData = Array.prototype.concat.apply([], this.parsedData);
// 长度不相等,则存在汉字或特殊符号。但为何添加下面这三个特殊的符号,暂时还未搞懂。
// 在 UTF-8 编码中,一个汉字要三个字节来表示。
if (this.parsedData.length != this.data.length) {
// console.log(String.fromCodePoint(191, 187, 239));
// 输入结果: "¿»ï"
this.parsedData.unshift(191);
this.parsedData.unshift(187);
this.parsedData.unshift(239);
}
}
QR8bitByte 方法主要是将数据由 Unicode 编码转为 UTF-8 编码,关于这个过程的详细解读,我之前有专门写一篇文章来介绍它,所以这里就不在重复叙述。
make——绘制二维码的核心方法
make 和 makeImpl 都是 QRCodeModel 原型上的方法。从代码上可以看出,makeImpl 才是真正绘制二维码的核心方法。其过程大致分为三个部分:
- 绘制二维码的各种图案。
- 生成 QRCode 数据。
- 将数据添加到 QRCode 矩阵中。
QRCodeModel.prototype = {
make: function () {
// getBestMaskPattern 获取掩码图案
this.makeImpl(false, this.getBestMaskPattern());
},
makeImpl: function (test, maskPattern) {
this.moduleCount = this.typeNumber * 4 + 17; // 二维码码元数
this.modules = new Array(this.moduleCount); // 创建数组
// 初始化 modules
for (var row = 0; row < this.moduleCount; row++) {
this.modules[row] = new Array(this.moduleCount);
for (var col = 0; col < this.moduleCount; col++) {
this.modules[row][col] = null;
}
}
// 设置定位图案
this.setupPositionProbePattern(0, 0);
this.setupPositionProbePattern(this.moduleCount - 7, 0);
this.setupPositionProbePattern(0, this.moduleCount - 7);
// 设置校正图案
this.setupPositionAdjustPattern();
// 设置时序图案
this.setupTimingPattern();
// 设置格式信息
this.setupTypeInfo(test, maskPattern);
// 大于等于 7 的二维码版本,才有版本信息
if (this.typeNumber >= 7) {
this.setupTypeNumber(test); // 设置版本信息
}
if (this.dataCache == null) {
// 创建数据并缓存
this.dataCache = QRCodeModel.createData(
this.typeNumber,
this.errorCorrectLevel,
this.dataList
);
}
// 将数据位添加到 QRCode 矩阵(这些位以特定模式放置)
this.mapData(this.dataCache, maskPattern);
},
// 略 ...
};
getBestMaskPattern
这个方法是 QRCodeModel 原型上的方法,其主要用途就是获取合适的二维码掩码图案。二维码有八种掩码图案,经相应的计算规则计算后,选取分数(失分)最小的。
getBestMaskPattern: function () {
var minLostPoint = 0;
var pattern = 0;
for (var i = 0; i < 8; i++) {
this.makeImpl(true, i);
var lostPoint = QRUtil.getLostPoint(this); // 计算失分数
// 选择失分数最小的掩模图案
if (i == 0 || minLostPoint > lostPoint) {
minLostPoint = lostPoint;
pattern = i;
}
}
return pattern;
}
getLostPoint
掩码图案失分计算规则:
- 行(列)中有连续 a 个码元(a > 5)同色,每处计 3 + (a - 5) = a - 2 分。
- 同色码元构成的 m 行 n 列矩形(例如,2 * 2),每处计 3 * (m - 1) * (n - 1) 分。
- 行(列)中出现 1:1:3:1:1(黑 : 白 : 黑黑黑 : 白 : 黑)的码元,每处计 40 分。
- 黑色码元占总码元的比例,例如:x%,将 x% 乘以 100,然后计 10 * ( |x - 50| / 5 ) 分。注意,|x-50| 是在求取绝对值。
考虑到,结合规则和代码也不一定能看懂,所以这里准备了传送门。这篇文章有图有文,说得非常详细。
getLostPoint: function (qrCode) {
var moduleCount = qrCode.getModuleCount(); // 二维码码元总数
var lostPoint = 0; // 失分数
// 序号1——计算规则的代码实现
for (var row = 0; row < moduleCount; row++) {
for (var col = 0; col < moduleCount; col++) {
// 记录相同颜色的模块数
var sameCount = 0;
// 获取码元,若值为 true,则是黑色,否则是白色
var dark = qrCode.isDark(row, col);
for (var r = -1; r <= 1; r++) {
if (row + r < 0 || moduleCount <= row + r) {
continue;
}
for (var c = -1; c <= 1; c++) {
if (col + c < 0 || moduleCount <= col + c) {
continue;
}
if (r == 0 && c == 0) {
continue;
}
if (dark == qrCode.isDark(row + r, col + c)) {
sameCount++;
}
}
}
if (sameCount > 5) {
lostPoint += 3 + sameCount - 5;
}
}
}
// 序号2——计算规则的代码实现
// QR 二维码规范规定对于 m * n(至少包含 2 * 2 )同色码元区域,损失分将增加
// 3x(m-1)(n-1)。然而 QR 二维码规范并没有说明这种同色码元区域可以再以多种方式
// 划分小块时如何积分。因此,与其寻找大于 2 * 2 的同色码元区域,不如直接找 2 * 2
// 的同色码元区域,每发现一个,损失分就加 3,彼此相互覆盖的 2 * 2 区域也算。例如一个
// 3 * 2 的区域可以拆分为两个 2 * 2 的区域,那么损失分就为 6。
for (var row = 0; row < moduleCount - 1; row++) {
for (var col = 0; col < moduleCount - 1; col++) {
var count = 0;
// 四个 if 判断,就是 2 * 2 码元区域(黑或白)
if (qrCode.isDark(row, col)) count++;
if (qrCode.isDark(row + 1, col)) count++;
if (qrCode.isDark(row, col + 1)) count++;
if (qrCode.isDark(row + 1, col + 1)) count++;
// count == 0 为白色码元,count == 4 为黑色码元
if (count == 0 || count == 4) {
lostPoint += 3;
}
}
}
// 序号3——计算规则的代码实现
// 1:1:3:1:1(黑 : 白 : 黑黑黑 : 白 : 黑),黑——>true,白——>false
for (var row = 0; row < moduleCount; row++) {
for (var col = 0; col < moduleCount - 6; col++) {
if (
qrCode.isDark(row, col) &&
!qrCode.isDark(row, col + 1) &&
qrCode.isDark(row, col + 2) &&
qrCode.isDark(row, col + 3) &&
qrCode.isDark(row, col + 4) &&
!qrCode.isDark(row, col + 5) &&
qrCode.isDark(row, col + 6)
) {
lostPoint += 40;
}
}
}
// 序号3——计算规则的代码实现
// 1:1:3:1:1(黑 : 白 : 黑黑黑 : 白 : 黑),黑——>true,白——>false
for (var col = 0; col < moduleCount; col++) {
for (var row = 0; row < moduleCount - 6; row++) {
if (
qrCode.isDark(row, col) &&
!qrCode.isDark(row + 1, col) &&
qrCode.isDark(row + 2, col) &&
qrCode.isDark(row + 3, col) &&
qrCode.isDark(row + 4, col) &&
!qrCode.isDark(row + 5, col) &&
qrCode.isDark(row + 6, col)
) {
lostPoint += 40;
}
}
}
// 计算黑色码元的总数
var darkCount = 0;
for (var col = 0; col < moduleCount; col++) {
for (var row = 0; row < moduleCount; row++) {
if (qrCode.isDark(row, col)) {
darkCount++;
}
}
}
// 序号4——计算规则的代码实现
// 码元总数:moduleCount * moduleCount,所以要除两次 moduleCount
var ratio = Math.abs((100 * darkCount) / moduleCount / moduleCount - 50) / 5;
lostPoint += ratio * 10;
return lostPoint;
}
getModuleCount
QRCodeModel 原型上的方法,用于获取二维码码元总数。
// 获取二维码码元总数
getModuleCount: function () {
return this.moduleCount;
}
isDark
QRCodeModel 原型上的方法,用于获取二维码某个码元的值。这个值,只有两种:true(黑色) 或 false(白色)。
// 获取码元,“非黑即白”,一般为黑色(比较多)
isDark: function (row, col) {
if (
row < 0 ||
this.moduleCount <= row ||
col < 0 ||
this.moduleCount <= col
) {
throw new Error(row + "," + col);
}
return this.modules[row][col];
}
setupPositionProbePattern
QRCodeModel 原型上的方法,其功能是绘制定位图案。定位图案与二维码尺寸大小无关,且一定是一个 7×7 的矩阵,用于标记二维码矩形的大小。用三个定位图案即可标识并确定一个二维码矩形的位置和方向。这个函数同时也处理了定位图案分割符,就是定位图案周围的白边框,它的作用是将定位图案与其他区域区分开来。
定位图案位置:
- 左上角定位图案始终位于
(0, 0)
。 - 右上角定位图案始终位于
([(((V-1)*4)+21) - 7], 0)
- 左下角定位图案始终位于
(0, [(((V-1)*4)+21) - 7])
// 设置-定位图案
setupPositionProbePattern: function (row, col) {
// 定位图案 7x7 矩阵
for (var r = -1; r <= 7; r++) { // 行
// 进入下一轮循环
if (row + r <= -1 || this.moduleCount <= row + r) continue;
for (var c = -1; c <= 7; c++) { // 列
// 进入下一轮循环
if (col + c <= -1 || this.moduleCount <= col + c) continue;
// 为定位图案和定位图案分割符赋值
if (
(0 <= r && r <= 6 && (c == 0 || c == 6)) ||
(0 <= c && c <= 6 && (r == 0 || r == 6)) ||
(2 <= r && r <= 4 && 2 <= c && c <= 4)
) {
this.modules[row + r][col + c] = true;
} else {
this.modules[row + r][col + c] = false;
}
}
}
}
setupPositionAdjustPattern
QRCodeModel 原型上的方法,其功能是绘制校正图案。校正图案也是为了定位用的,它与二维码尺寸大小无关,且一定是一个 5×5 的矩阵。
校正图案必须放置的位置在校正图案位置表中有定义。下图显示的是版本 8 二维码的对齐图案,其中 6、24、42 这三个数字的所有组合,就是校正图案的坐标。
// 设置-校正图案
setupPositionAdjustPattern: function () {
// 校正图案 row/column 坐标
var pos = QRUtil.getPatternPosition(this.typeNumber);
for (var i = 0; i < pos.length; i++) {
for (var j = 0; j < pos.length; j++) {
var row = pos[i];
var col = pos[j];
// 若已赋值,则进行下一轮循环
if (this.modules[row][col] != null) {
continue;
}
// 校正图案 5x5 矩阵
for (var r = -2; r <= 2; r++) {
for (var c = -2; c <= 2; c++) {
if (
r == -2 ||
r == 2 ||
c == -2 ||
c == 2 ||
(r == 0 && c == 0)
) {
this.modules[row + r][col + c] = true;
} else {
this.modules[row + r][col + c] = false;
}
}
}
}
}
}
setupTimingPattern
QRCodeModel 原型上的方法,其功能是绘制时序图案。时序图案也是用于定位的,因为二维码尺寸过大会导致扫描出错,所以需要一条标准线来防止错误的发生。时序图案有两条:横(row)轴上一条,竖(column)轴上一条。
// 设置-时序图案
setupTimingPattern: function () {
// 处理 row
for (var r = 8; r < this.moduleCount - 8; r++) {
// 若是已赋过值(不能再被赋值),则进行下一轮循环
if (this.modules[r][6] != null) {
continue;
}
// r % 2 == 0,即 r 被 2 整除。可以整除的为 true,否则 false
this.modules[r][6] = r % 2 == 0;
}
// 处理 column
for (var c = 8; c < this.moduleCount - 8; c++) {
if (this.modules[6][c] != null) {
continue;
}
this.modules[6][c] = c % 2 == 0;
}
}
setupTypeInfo
QRCodeModel 原型上的方法,其功能是绘制格式信息。格式信息是对该二维码中的纠错级别和选用的掩码模式进行编码后的字符串。因为有 L,M,Q 和 H 四种纠错级别,以及 0 - 7 八种掩码模式,所以总共有 32 种可能的格式信息。详情介绍。
格式信息字符串生成步骤:
格式信息字符串总共 15 位长度,首先 5 位用来编码纠错级别和用到的掩码模式。然后使用这 5 位来生成一个 10 位的纠错码。最终的 15 位二进制串会和 101010000010010
进行 XOR 运算。
例如,如果我们使用了纠错级别 L 和掩码模式 4,则创建五位二进制序列如下:
- 01(纠错等级 L 的指标)
- 100(二进制为 4,即掩膜图案 4)
- 结果:01100
注意排列顺序:从左到右,纠错等级在前,掩码模式在后。
下图中的蓝色部分就是格式信息,它的位置在二维码中也是固定的。
// 设置-格式信息
setupTypeInfo: function (test, maskPattern) {
// 生成五位二进制
var data = (this.errorCorrectLevel << 3) | maskPattern;
// 生成最终格式字符串
var bits = QRUtil.getBCHTypeInfo(data);
// 格式信息字符串总共 15 位长度
for (var i = 0; i < 15; i++) {
var mod = !test && ((bits >> i) & 1) == 1;
if (i < 6) {
this.modules[i][8] = mod; // 左上角下侧——格式信息
} else if (i < 8) {
this.modules[i + 1][8] = mod; // 左上角下侧——格式信息
} else {
// 右上角下侧——格式信息
this.modules[this.moduleCount - 15 + i][8] = mod;
}
}
for (var i = 0; i < 15; i++) {
var mod = !test && ((bits >> i) & 1) == 1;
if (i < 8) {
// 左下角右侧——格式信息
this.modules[8][this.moduleCount - i - 1] = mod;
} else if (i < 9) {
// 所有 QR 码在左下角的定位图案旁边都有一个黑色模块。这个黑色模块,始终位于
// 坐标 ([(4 * V) + 9], 8) 处,其中 V 是二维码的版本。
this.modules[8][15 - i - 1 + 1] = mod; // 一个特定的黑色模块(参考上图)
} else {
this.modules[8][15 - i - 1] = mod; // 左上角右侧——格式信息
}
}
this.modules[this.moduleCount - 8][8] = !test;
}
getBCHTypeInfo
这个方法主要用来计算纠错位。详细介绍。
假设格式字符串位是 01100,生成多项式(二进制)是 10100110111
,也就是下面代码中的 QRUtil.G15
。在计算之前,先通过在格式字符串的右侧放置十个 0 来创建一个 15 位字符串,然后从其左侧删除 0,如下所示:
-
01100 --> 011000000000000
-
011000000000000 --> 11000000000000
完成上面的操作后,开始计算:
- 用 0 填充右侧的生成多项式字符串,使其与当前格式字符串的长度相同。
- 将填充的生成多项式字符串与当前格式字符串进行异或(XOR)。
- 将所得结果左侧的 0 全部删除。
- 重复上述 3 个步骤,直到生成的格式字符串长度为 10 位或更少。如果结果小于 10 位,我们将在左侧填充 0 以使其长度为 10 位。
- 用格式信息的原始五位(纠错级别指示符和掩码模式指示符)和生成的纠错位,创建一个字符串。
// 计算纠错位
getBCHTypeInfo: function (data) {
// 计算纠错位:用 5 位用于格式字符串,来生成 10 个纠错位
var d = data << 10;
while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) {
d ^=
QRUtil.G15 <<
// 这里左移运算就是在用 0 填充生成多项式字符串
(QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15));
}
// ((data << 10) | d),就是用格式信息的原始五位(纠错级别指示符和掩码模式指示符)和生成
// 的纠错位,创建一个字符串。
// QR 码规范表示要将格式和纠错位组合成的二进制字符串,与掩码字符串 (101010000010010),
// 也就是 G15_MASK 进行异或。
return ((data << 10) | d) ^ QRUtil.G15_MASK;
}
getBCHDigit: function (data) {
var digit = 0;
// 不断右移 data,直到它的值为 0,然后结束循环
while (data != 0) {
digit++;
data >>>= 1;
}
return digit;
}
QRUtil.G15
,是生成多项式的二进制。在生成格式字符串的纠错码字时,按照二维码规范要使用以下生成多项式: x^10 + x^8 + x^5 + x^4 + x^2 + x + 1。
我们可以通过只取每一项的系数将其转换为二进制字符串。x^10的系数为1,x^9的系数为0,因为 x^9 不存在于多项式中,依此类推。换句话说,代表这一步生成多项式的二进制串是 10100110111
。
QRUtil.G15
的代码:
var QRUtil = {
G15: (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0)
}
setupTypeNumber
QRCodeModel 原型上的方法,其功能是绘制版本信息。如果二维码为 7 或更高版本,则必须在二维码的左下角和右上角包含 18 位版本信息字符串。在二维码中,它是一个 6x3 矩形块。下图的蓝色区域,就是版本信息。
有关所有可能的版本信息字符串的完整列表,请参阅 格式和版本表。
// 设置-版本信息
setupTypeNumber: function (test) {
// 生成版本信息字符串
var bits = QRUtil.getBCHTypeNumber(this.typeNumber);
// 左下角——版本信息
for (var i = 0; i < 18; i++) {
var mod = !test && ((bits >> i) & 1) == 1;
this.modules[Math.floor(i / 3)][(i % 3) + this.moduleCount - 8 - 3] =
mod;
}
// 右上角——版本信息
for (var i = 0; i < 18; i++) {
var mod = !test && ((bits >> i) & 1) == 1;
this.modules[(i % 3) + this.moduleCount - 8 - 3][Math.floor(i / 3)] =
mod;
}
}
getBCHTypeNumber
这个方法主要用来生成版本信息字符串。详情介绍。
QR 码规范说对版本信息字符串使用 (18, 6) Golay 码。因此,版本信息字符串是一个 18 位的字符串,由一个编码 QR 版本的 6 位二进制字符串和一个 12 个纠错位的字符串组成。整个字符串是 18 位长。
二维码规范说在这一步使用以下生成多项式:
x^12 + x^11 + x^10 + x^9 + x^8 + x^5 + x^2 + 1
正如前面的格式字符串部分所述,我们可以用以下二进制字符串表示这个生成多项式:1111100100101
。
假设二维码版本是 7,那么它的六位二进制为:000111
。在计算之前,先在其右侧填充 0 将其转换为 18 位字符串,然后从其左侧删除 0,如下所示:
-
000111 --> 000111000000000000
-
000111000000000000 --> 111000000000000
完成上面的操作后,开始计算:
- 用 0 填充右侧的生成多项式字符串,使其与当前版本字符串的长度相同。
- 将填充的生成多项式字符串与当前版本字符串进行异或(XOR)。
- 若字符串的长度是 12,则不需要进一步划分。与格式信息字符串一样,如果结果小于 12,则必须在左侧填充 0 以使其长度为 12 位。
- 将原始的六位版本字符串放在上一步结果的左侧,创建一个字符串。
// 生成版本信息字符串
getBCHTypeNumber: function (data) {
var d = data << 12; // 左移 12 位
// QRUtil.G18 是生成多项式
while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) {
d ^=
QRUtil.G18 <<
// 这里左移运算就是在用 0 填充生成多项式字符串
(QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18));
}
// 将原始的六位版本字符串放在计算出的结果左侧,创建一个字符串
return (data << 12) | d;
}
QRUtil.G18
代码:
var QRUtil = {
G18: (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) |
(1 << 8) | (1 << 5) | (1 << 2) | (1 << 0)
}
createData
生成最终的字符串数据。它由字节模式指示器、字符计数指示器、编码数据,从左到右依次拼接而成。详情介绍。
// 创建-编码数据
QRCodeModel.createData = function (
typeNumber,
errorCorrectLevel,
dataList
) {
// 对纠错码分块(就是分组)
var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel);
// 创建字节对象
var buffer = new QRBitBuffer();
for (var i = 0; i < dataList.length; i++) {
var data = dataList[i];
// 最终字符串位: 由字节模式指示器、字符计数指示器、编码数据,从左
// 到右依次拼接而成。
// 字节模式指示器(data.mode)表示编码模式,而每种编码模式都有
// 一个四位模式指示器来标识它。
buffer.put(data.mode, 4);
// 字符计数指示器
buffer.put(
// 获取字符数
data.getLength(),
// 获取-字符计数指示器的长度
QRUtil.getLengthInBits(data.mode, typeNumber)
);
// 编码数据
data.write(buffer);
}
var totalDataCount = 0;
for (var i = 0; i < rsBlocks.length; i++) {
// 二维码所需的总位数
totalDataCount += rsBlocks[i].dataCount;
}
// 数据位 不能大于 二维码所需的总位数
if (buffer.getLengthInBits() > totalDataCount * 8) {
throw new Error(
"code length overflow. (" +
buffer.getLengthInBits() +
">" +
totalDataCount * 8 +
")"
);
}
// 如果数据位串比所需位的总数短,则必须在串的右侧添加最多四个 0 的终止符。
if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
buffer.put(0, 4);
}
// 添加终止符后,如果字符串中的位数不是8的倍数,则先在右边的字符串中填
// 充 0,使字符串的长度成为8的倍数。
while (buffer.getLengthInBits() % 8 != 0) {
buffer.putBit(false);
}
// 如果字符串仍然不够长,无法填满最大容量。那么二维码规范特别要求,在
// 字符串末尾添加以下字节: 11101100、00010001,且重复直到字符串达
// 到最大长度。这两个字节分别相当于 236 和 17。
while (true) {
if (buffer.getLengthInBits() >= totalDataCount * 8) {
break;
}
// PAD0 --> 11101100
buffer.put(QRCodeModel.PAD0, 8);
if (buffer.getLengthInBits() >= totalDataCount * 8) {
break;
}
// PAD1 --> 00010001
buffer.put(QRCodeModel.PAD1, 8);
}
return QRCodeModel.createBytes(buffer, rsBlocks);
};
getRSBlocks
这个方法用于获取纠错码并分块。
对于较大的二维码需要将数据码字分成更小的块,并为每个块单独生成纠错码字。但是,getRSBlocks 方法,主要是先将纠错码分块,至于为每个块单独生成纠错码字,则是在其它的方法中操作。详细介绍。
// 获取纠错码并分块(就是分组)
QRRSBlock.getRSBlocks = function (
typeNumber,
errorCorrectLevel
) {
// 获取 QR 码相应版本和纠错级别需要生成的错码字数
var rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel);
if (rsBlock == undefined) {
throw new Error(
"bad rs block @ typeNumber:" +
typeNumber +
"/errorCorrectLevel:" +
errorCorrectLevel
);
}
var length = rsBlock.length / 3; // 分组(最多有两组)
var list = [];
for (var i = 0; i < length; i++) {
// 每一组(Group 1)中的块数
var count = rsBlock[i * 3 + 0];
// 每个块中的数据码字数 加上 每个块的 EC(纠错)码字数
var totalCount = rsBlock[i * 3 + 1];
// 每个块中的数据码字数
var dataCount = rsBlock[i * 3 + 2];
// 保存每个块中 totalCount 和 dataCount
for (var j = 0; j < count; j++) {
list.push(new QRRSBlock(totalCount, dataCount));
}
}
return list;
};
QRBitBuffer
存储生成的数据。
function QRBitBuffer() {
this.buffer = [];
this.length = 0;
}
QRBitBuffer.prototype = {
get: function (index) {
// 一个字节8bit
var bufIndex = Math.floor(index / 8);
return ((this.buffer[bufIndex] >>> (7 - (index % 8))) & 1) == 1;
},
put: function (num, length) {
for (var i = 0; i < length; i++) {
// 找出 num 二进制位中每个 1 的位置,以便在 putBit 中计算其值
this.putBit(((num >>> (length - i - 1)) & 1) == 1);
}
},
// 字符串位的总长度
getLengthInBits: function () {
return this.length;
},
putBit: function (bit) {
// 一个字节8bit
var bufIndex = Math.floor(this.length / 8);
if (this.buffer.length <= bufIndex) {
this.buffer.push(0); // 初始为 0
}
if (bit) {
// 0x80 --> 1000 0000
// 最终字符串: 由 字节模式指示器、字符计数指示器、编码数据,从左
// 到右依次拼接而成。
// 右移 0x80,然后在与之前的值进行按位或,来得到一个完整的字节。
// 例如,0100 是字节模式指示器, 00011010 是字符计数指示器,从
// 左到右每 8bit 为一个字节,所以应该是 01000001 1010。
// 下面这行代码,做得就是将所有的字节拼在一起,然后从左到右每
// 8bit 为一个字节划分。
this.buffer[bufIndex] |= 0x80 >>> this.length % 8;
}
this.length++;
},
};
QRUtil.getLengthInBits
这个方法用于获取字符计数指示器的长度。
字符计数指示器是一串位,表示正在编码的字符数,且必须放在模式指示器之后。此外,字符计数指示器必须是特定位数的长度,这取决于QR版本。
下面代码中展示的是每个模式和版本对应的字符计数指示器的大小。详情介绍。
// 每种编码模式都有一个四位模式指示器来标识它。
// 编码数据必须以适当的模式指示器开始,该指示器指定用于其后位的模式。
// 下面列出了每种模式及其对应的模式指示器
var QRMode = {
MODE_NUMBER: 1 << 0, // 数字模式: 0001
MODE_ALPHA_NUM: 1 << 1, // 字母数字模式: 0010
MODE_8BIT_BYTE: 1 << 2, // 字节模式: 0100
MODE_KANJI: 1 << 3, // Kanji 模式: 1000
};
var QRUtil = {
getLengthInBits: function (mode, type) {
if (1 <= type && type < 10) { // QR版本 1-9
switch (mode) {
case QRMode.MODE_NUMBER:
return 10;
case QRMode.MODE_ALPHA_NUM:
return 9;
case QRMode.MODE_8BIT_BYTE:
return 8;
case QRMode.MODE_KANJI:
return 8;
default:
throw new Error("mode:" + mode);
}
} else if (type < 27) { // QR版本 10-26
switch (mode) {
case QRMode.MODE_NUMBER:
return 12;
case QRMode.MODE_ALPHA_NUM:
return 11;
case QRMode.MODE_8BIT_BYTE:
return 16;
case QRMode.MODE_KANJI:
return 10;
default:
throw new Error("mode:" + mode);
}
} else if (type < 41) { // QR版本 27-40
switch (mode) {
case QRMode.MODE_NUMBER:
return 14;
case QRMode.MODE_ALPHA_NUM:
return 13;
case QRMode.MODE_8BIT_BYTE:
return 16;
case QRMode.MODE_KANJI:
return 12;
default:
throw new Error("mode:" + mode);
}
} else {
throw new Error("type:" + type);
}
}
}
createBytes
生成最终数据。
QRCodeModel.createBytes = function (buffer, rsBlocks) {
var offset = 0;
var maxDcCount = 0;
var maxEcCount = 0;
var dcdata = new Array(rsBlocks.length); // 编码数据数组
var ecdata = new Array(rsBlocks.length); // 纠错数据数组
for (var r = 0; r < rsBlocks.length; r++) {
var dcCount = rsBlocks[r].dataCount; // 当前块的数据码个数
var ecCount = rsBlocks[r].totalCount - dcCount; // 当前块的纠错码个数
maxDcCount = Math.max(maxDcCount, dcCount);
maxEcCount = Math.max(maxEcCount, ecCount);
dcdata[r] = new Array(dcCount);
for (var i = 0; i < dcdata[r].length; i++) {
// 0xff --> 11111111
// buffer 中的每个元素都是一个8bit的字节,最大只能是 0xff
// 只要这个元素没有大于 0xff,它是不会被改变的
dcdata[r][i] = 0xff & buffer.buffer[i + offset]; // 当前块的数据码
}
offset += dcCount;
// 纠错码的生成多项式
var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
// 消息多项式
var rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1);
// 消息多项式 与 生成多项式 相除(异或运算)后,剩下的余数就是纠错码。
var modPoly = rawPoly.mod(rsPoly);
// 存储纠错码
ecdata[r] = new Array(rsPoly.getLength() - 1);
for (var i = 0; i < ecdata[r].length; i++) {
var modIndex = i + modPoly.getLength() - ecdata[r].length;
ecdata[r][i] = modIndex >= 0 ? modPoly.get(modIndex) : 0;
}
}
// 交错数据码和纠错码,且将交错后的纠错码放在已交错的数据码之后
var totalCodeCount = 0;
for (var i = 0; i < rsBlocks.length; i++) {
totalCodeCount += rsBlocks[i].totalCount;
}
var data = new Array(totalCodeCount);
var index = 0;
// 交错数据码
for (var i = 0; i < maxDcCount; i++) {
for (var r = 0; r < rsBlocks.length; r++) {
if (i < dcdata[r].length) {
data[index++] = dcdata[r][i];
}
}
}
// 交错纠错码
for (var i = 0; i < maxEcCount; i++) {
for (var r = 0; r < rsBlocks.length; r++) {
if (i < ecdata[r].length) {
data[index++] = ecdata[r][i];
}
}
}
return data;
};
getErrorCorrectPolynomial
这个函数用于生成纠错码的多项式。详情介绍。
getErrorCorrectPolynomial: function (errorCorrectLength) {
var a = new QRPolynomial([1], 0);
for (var i = 0; i < errorCorrectLength; i++) {
// 合并同类项
a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0));
}
return a;
},
QRPolynomial
对于多项式的处理操作这一块,需要你仔细阅读文章中关于它的原理解说,否则很难看懂下面的代码。
function QRPolynomial(num, shift) {
if (num.length == undefined) {
throw new Error(num.length + "/" + shift);
}
var offset = 0;
while (offset < num.length && num[offset] == 0) {
offset++;
}
// 存储传入的数据
this.num = new Array(num.length - offset + shift);
for (var i = 0; i < num.length - offset; i++) {
this.num[i] = num[i + offset];
}
}
QRPolynomial.prototype = {
get: function (index) {
return this.num[index];
},
getLength: function () {
return this.num.length;
},
// multiply-合并同类项
// GF(256) 中的加法是通过 XOR 执行的,合并同类项需要对常数项进行XOR。
// 当指数大于或等于 256 时,在组合类似项之前应用对 255 取模(QRMath.gexp 中做了取模处理)
multiply: function (e) {
var num = new Array(this.getLength() + e.getLength() - 1);
for (var i = 0; i < this.getLength(); i++) {
for (var j = 0; j < e.getLength(); j++) {
// 通过 GF(256) 算法中使用的对数和反对数值,生成 QR 码所需的纠错码。
num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i)) + QRMath.glog(e.get(j)));
}
}
return new QRPolynomial(num, 0);
},
// 将消息多项式除以生成多项式
// 1.找到合适的项来乘以生成多项式。乘法的结果应该与消息多项式(在第一个乘法步骤中)
// 或余数(在所有后续乘法步骤中)具有相同的第一项。
// 2. 将结果与消息多项式(在第一个乘法步骤中)或余数(在所有后续乘法步骤中)进行异或。
// 3. 执行这些步骤 n 次,其中 n 是数据码字的数量。
mod: function (e) {
// 对应第三个步骤
if (this.getLength() - e.getLength() < 0) {
return this;
}
// 消息多项式 和 生成多项式,两者第一项的指数差值
var ratio = QRMath.glog(this.get(0)) - QRMath.glog(e.get(0));
var num = new Array(this.getLength());
for (var i = 0; i < this.getLength(); i++) {
num[i] = this.get(i); // 填充消息多项式
}
for (var i = 0; i < e.getLength(); i++) {
// 让 消息多项式 和 生成多项式具有相同的第一项,然后进行异或
num[i] ^= QRMath.gexp(QRMath.glog(e.get(i)) + ratio);
}
return new QRPolynomial(num, 0).mod(e);
},
};
mapData
将数据位添加到 QR 矩阵。
数据位从矩阵的右下角开始放置,并在 2 个模块宽的列中向上进行。使用白色像素为 0,黑色像素为 1。 当列到达顶部时,下一个 2 模块列立即从前一列的左侧开始并继续向下。每当当前列到达矩阵的边缘时,移动到下一个 2 模块列并改变方向。如果遇到功能模式或保留区,则数据位将放置在下一个未使用的模块中。详情介绍。
// 将数据位添加到 QR 矩阵(这些位以特定模式放置)
mapData: function (data, maskPattern) {
var inc = -1;
var row = this.moduleCount - 1;
var bitIndex = 7;
var byteIndex = 0;
// 有多少码元,就循环多少次
for (var col = this.moduleCount - 1; col > 0; col -= 2) {
if (col == 6) col--;
while (true) {
// 2 个模块宽
for (var c = 0; c < 2; c++) {
// 跳过已经赋值的码元
if (this.modules[row][col - c] == null) {
var dark = false;
if (byteIndex < data.length) {
// 运算结果为true,则为黑色,否则白色。
// 在向二维码中填充数据位时,可以将其分为多个小模块。这些个小模块可以分为
// 常规模块和非常规模块,每个模块的容量都为 8(一个字节,8bit)。通过右移
// 来给码元赋值(bitIndex 是逐渐减少的,这样做是为了对字节的每个bit位进
// 行判断)。
dark = ((data[byteIndex] >>> bitIndex) & 1) == 1;
}
// 每个掩码模式使用一个公式来确定是否更改当前位的颜色
var mask = QRUtil.getMask(maskPattern, row, col - c);
if (mask) {
dark = !dark;
}
// 当前码元的值 true 或 false(即黑或白),一个码元就是一个bit位
this.modules[row][col - c] = dark;
bitIndex--;
if (bitIndex == -1) { // 重置
byteIndex++;
bitIndex = 7;
}
}
}
row += inc; // 逐次减少或增加行
// 改变方向,向上或向下
if (row < 0 || this.moduleCount <= row) {
row -= inc;
inc = -inc;
break;
}
}
}
}
getMask
掩码图案公式。详情介绍。
二维码规范定义了八种可应用于二维码的掩码图案。每个掩码图案使用一个公式来确定是否更改当前位的颜色。若是你将一个坐标放入公式中,如果结果为 0,则在该坐标处使用相反的位。例如,如果坐标 (0,3) 的位为 1,并且该坐标的公式等于 0,则在 (0,3) 处放置 0 而不是 1。
// 掩码图案-序号
var QRMaskPattern = {
PATTERN000: 0,
PATTERN001: 1,
PATTERN010: 2,
PATTERN011: 3,
PATTERN100: 4,
PATTERN101: 5,
PATTERN110: 6,
PATTERN111: 7,
};
var QRUtil = {
// 根据掩码图案选择公式
getMask: function (maskPattern, i, j) {
switch (maskPattern) {
case QRMaskPattern.PATTERN000:
return (i + j) % 2 == 0;
case QRMaskPattern.PATTERN001:
return i % 2 == 0;
case QRMaskPattern.PATTERN010:
return j % 3 == 0;
case QRMaskPattern.PATTERN011:
return (i + j) % 3 == 0;
case QRMaskPattern.PATTERN100:
return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0;
case QRMaskPattern.PATTERN101:
return ((i * j) % 2) + ((i * j) % 3) == 0;
case QRMaskPattern.PATTERN110:
return (((i * j) % 2) + ((i * j) % 3)) % 2 == 0;
case QRMaskPattern.PATTERN111:
return (((i * j) % 3) + ((i + j) % 2)) % 2 == 0;
default:
throw new Error("bad maskPattern:" + maskPattern);
}
}
}
绘制 QRCode
通过 Drawing.prototype.draw 方法,绘制 QRCode。
// done: 绘制 QRCode
Drawing.prototype.draw = function (oQRCode) {
var _elImage = this._elImage; // img 元素对象
var _oContext = this._oContext; // canvas 上下文
var _htOption = this._htOption; // 配置选项
var nCount = oQRCode.getModuleCount(); // 获取码元数
var nWidth = _htOption.width / nCount;
var nHeight = _htOption.height / nCount;
var nRoundedWidth = Math.round(nWidth);
var nRoundedHeight = Math.round(nHeight);
_elImage.style.display = "none"; // 隐藏 img 元素
this.clear(); // 清除 QRCode(绘制之前,先清除)
for (var row = 0; row < nCount; row++) {
for (var col = 0; col < nCount; col++) {
// bIsDark,不是 true,就是 false。
var bIsDark = oQRCode.isDark(row, col);
var nLeft = col * nWidth;
var nTop = row * nHeight;
// colorDark(背景色),默认是黑色;colorLight(前景色),默认是白色。
// 设置绘图笔触的颜色
_oContext.strokeStyle = bIsDark
? _htOption.colorDark
: _htOption.colorLight;
_oContext.lineWidth = 1; // 线条宽度
// 设置填充绘画的颜色
_oContext.fillStyle = bIsDark
? _htOption.colorDark
: _htOption.colorLight;
// 绘制图案(码元方块)
_oContext.fillRect(nLeft, nTop, nWidth, nHeight);
// 抗干扰处理
_oContext.strokeRect(
Math.floor(nLeft) + 0.5,
Math.floor(nTop) + 0.5,
nRoundedWidth,
nRoundedHeight
);
_oContext.strokeRect(
Math.ceil(nLeft) - 0.5,
Math.ceil(nTop) - 0.5,
nRoundedWidth,
nRoundedHeight
);
}
}
this._bIsPainted = true;
};
生成图片
生成图片的核心方法,实际上是——Drawing.prototype.makeImage
。
QRCode.prototype.makeImage = function () {
// Android 3.x 版本以下,不支持 Data-URI spec
if (
typeof this._oDrawing.makeImage == "function" &&
(!this._android || this._android >= 3)
) {
// this._oDrawing 就是 Drawing 的实例对象
this._oDrawing.makeImage(); // 生成图片
}
};
Drawing.prototype.makeImage = function () {
// _bIsPainted 是 Drawing 实例对象中用来标记二维码绘制是否已完成
if (this._bIsPainted) {
// 生成 URL 时,检查浏览器是否支持 data URI
_safeSetDataURI.call(this, _onMakeImage);
}
};
_safeSetDataURI
这个方法是用来检查用户的浏览器是否支持 data URI。
function _safeSetDataURI(fSuccess, fFail) {
var self = this;
self._fFail = fFail;
self._fSuccess = fSuccess;
// 仅检查一次
if (self._bSupportDataURI === null) {
var el = document.createElement("img");
// 自定义失败事件函数
var fOnError = function () {
self._bSupportDataURI = false;
if (self._fFail) {
self._fFail.call(self);
}
};
// 自定义成功事件函数
var fOnSuccess = function () {
self._bSupportDataURI = true;
if (self._fSuccess) {
self._fSuccess.call(self);
}
};
// Image 对象事件
el.onabort = fOnError; // 用户放弃图像的装载时调用的事件
el.onerror = fOnError; // 在装载图像的过程中发生错误时调用的事件
el.onload = fOnSuccess; // 图像装载完毕时调用的事件
// 默认,1px 的图像数据。
el.src ="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
return;
} else if (self._bSupportDataURI === true && self._fSuccess) {
self._fSuccess.call(self);
} else if (self._bSupportDataURI === false && self._fFail) {
self._fFail.call(self);
}
}
_onMakeImage
生成包含图片展示的 data URI。
function _onMakeImage() {
// Canvas.toDataURL 方法返回一个包含图片展示的 data URI
this._elImage.src = this._elCanvas.toDataURL("image/png");
this._elImage.style.display = "block"; // 显示图片
this._elCanvas.style.display = "none"; // 隐藏 canvas
}
最后
整篇文章,实际上就是二维码的原理与各个模块的代对应上,然后加以解释。当然,可能说得不是很详细,但是作为参考,相信还是能对小伙们有所帮助的。若是有说得不对的地方,还请各位加以指出🙏。