QR码详解

  1. 1. 前言
  2. 2. QR码的结构
    1. 2.1. 符号版本和规格
  3. 3. 功能图形
    1. 3.1. 循象图形
    2. 3.2. 分隔符
    3. 3.3. 定位图形
    4. 3.4. 校正图形
  4. 4. 编码区
    1. 4.1. 纠错等级
    2. 4.2. 掩膜
    3. 4.3. 格式信息
    4. 4.4. 版本信息
    5. 4.5. 数据编码
      1. 4.5.1. 数字模式
      2. 4.5.2. 字母数字模式
      3. 4.5.3. 8 位字节模式
      4. 4.5.4. 终止符
      5. 4.5.5. 10位流到码字的转换
    6. 4.6. Reed–Solomon
    7. 4.7. 码字在矩阵中的布置
    8. 4.8. 添加掩膜
    9. 4.9. 掩模结果的评价
  5. 5. 结语

前言

在正式的介绍之前,可以查看本文最终的编写结果:二维码生成。接下来会逐步介绍QR码的各个部分,并带你从零开始编写一个QR码生成器(javascript)。

QR码的结构

QR码是一种二维条码,由二维码的版本号、定位信息、纠错等级、掩码信息和编码信息组成,下图是版本7的QR码符号的结构图。

QR码符号的结构

图中的功能图型主要是定位和校正图形,编码区是存储数据信息、格式信息(纠错等级和掩码信息)、版本信息。

初始化项目

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./style.css">
<title>QRCode</title>
</head>
<body>
<canvas></canvas>
<script type="text/javascript" src="./painter.js"></script>
<script type="text/javascript" src="./index.js"></script>
</body>
</html>

style.css

1
2
3
4
5
6
7
8
9
10
11
12
13
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const canvas = document.querySelector("canvas");
const SIZE = Math.min(window.innerWidth, window.innerHeight) / 6 * 5;
canvas.width = SIZE;
canvas.height = SIZE;

// 版本
const VERSION = 10;
const painter = new Painter(canvas, "2d", VERSION);

// 绘制网格
painter.drawGrid();

// 根据QR码的结构逐步绘制

// 添加定位图案
painter.addLocation();
// 添加对齐图案
painter.addAlignment();
// 添加时序图(定位图形)
painter.addTimeline();
// 添加格式信息
painter.addFormatInfo();
// 添加版本信息
painter.addVersionInfo();
// 添加数据
painter.addData();
// 添加掩膜
painter.addMask();
// 绘制
painter.drawCode();

painter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Painter {
/**
* @param {HTMLCanvasElement} canvas
* @param {string} contextId
* @param {number} version
*/
constructor(canvas, contextId = '2d', version = 1) {
this.canvas = canvas;
this.ctx = canvas.getContext(contextId);
this.version = version;
}

// 绘制网格
drawGrid() {}

// 添加定位
addLocation() {}

// 添加对齐
addAlignment() {}

// 添加时序图
addTimeline() {}

// 添加格式信息
addFormatInfo() {}

// 添加版本信息
addVersionInfo() {}

// 添加数据
addData() {}

// 添加掩膜
addMask() {}

// 绘制
drawCode() {}
}

现在根据QR码结构搭建起一个框架,这里的步骤顺序并不是固定的,为了方便我会按照这个顺序来介绍各个部分,并逐步完成这里面的各个部分。

符号版本和规格

QR码符号共有40种规格(版本),其中版本1的规格为 ,版本2为 (如下图所示),以此类推,每一版本符号比前一版本每边增加 个模块,直到版本

版本一和版本二的规格大小

绘制网格与绘制数据

painter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Painter {
constructor(canvas, contextId = '2d', version = 1) {
// ...

// 大小
this.size = version * 4 + 17;
// 间隔
this._spacing = Math.min(canvas.width, canvas.height) / this.size;
// 结束长度
this.end = Math.min(canvas.width, canvas.height);
}

// 绘制网格
drawGrid(color = '#000') {
this.ctx.lineWidth = 0.5;
this.ctx.strokeStyle = color;
let end = this.end + this._spacing;
for (let i = 0; i < end; i += this._spacing) {
this.ctx.beginPath();
this.ctx.moveTo(i, 0);
this.ctx.lineTo(i, this.end);
this.ctx.stroke();
}
for (let i = 0; i < end; i += this._spacing) {
this.ctx.beginPath();
this.ctx.moveTo(0, i);
this.ctx.lineTo(this.end, i);
this.ctx.stroke();
}
}
}

现在我们得到了一个根据版本生成的网格,在这之后需要一个对应大小的数组,根据数据来绘制图案。

painter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class Painter {
static ONE_CHANGE = true; // 是否开启一次写入

constructor(canvas, contextId = '2d', version = 1) {
// ...

// 对应的数据数组(这里需要代理[Proxy],也可以自定义get、set方法。我们需要一个记录访问过的数据的数组,通过 ONE_CHANGE 来确定值是否可以修改)
this._codeMap = [];
for (let i = 0; i < this.size; i++) {
let row = []; // 当前行数据
row.flag = []; // 访问记录数组
this._codeMap.push(row);
for (let j = 0; j < this.size; j++) {
row.push(0);
row.flag.push(false);
}
}
// 代理模式
this._codeMap = new Proxy(this._codeMap, {
get(target, prop) {
// 如果不是数字(访问属性),则直接返回该属性
if (isNaN(Number(prop))) {
return target[prop];
}
// 给当前行数组挂上一个 y 属性,表示当前行。
target[prop].y = Number(prop);
// 行代理
return new Proxy(target[prop], {
set(target, prop, value) {
// 如果不是数字(访问属性),则直接返回该属性
if (isNaN(Number(prop))) {
return target[prop];
}

// 如果该位置没有被访问过
if (target.flag[Number(prop)] === false) {
// 如果开启一次写入,则将该位置设置为已访问
if (Painter.ONE_CHANGE) {
target.flag[Number(prop)] = true;
}
target[prop] = value;
}
return true;
}
});
}
});
}

// 绘制
drawCode(color = "#000000") {
this.ctx.fillStyle = color;
for (let i = 0; i < this.size; i++) {
for (let j = 0; j < this.size; j++) {
// 如果为1,则填充颜色
if (this._codeMap[i][j]) {
this.ctx.fillRect(j * this._spacing, i * this._spacing, this._spacing, this._spacing);
}
}
}
}
}

功能图形

循象图形

寻象图形包括三个相同的位置探测图形,分别位于符号的左上角、右上角和左下角,每个位置探测图形由7×7个模块组成如下图所示。

位置探测图形的结构

分隔符

在每个位置探测图形和编码区域之间有宽度为 1 个模块的分隔符。

定位图形

水平和垂直定位图形分别为一个模块宽的一行和一列,由深色浅色模块交替组成,其开始和结尾都是深色模块。符号的第6行和第6列。它们的作用是确定符号的密度和版本,提供决定模块坐标的基准位置。

校正图形

校正图形作为一个固定的参照图形,在图像有一定程度损坏的情况下,译码软件可以通过它同步图像模块的坐标映像。每个校正图形由5×5模块组成,版本 2 以上(含版本 2)的符号均有校正图形,各个版本校正图形位置将会在接下来的代码给出。

校正图形

各个功能图形如下图所示,红色为寻象图形、白色为分隔符、蓝色为定位图形、绿色为校正图形。(版本10)

版本10的图形

添加功能图形

painter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
class Painter {
// 各个版本校正图形的位置
static LOCATION = {
1: [6, 14],
2: [6, 18],
3: [6, 22],
4: [6, 26],
5: [6, 30],
6: [6, 34],
7: [6, 22, 38],
8: [6, 24, 42],
9: [6, 26, 46],
10: [6, 28, 50],
11: [6, 30, 54],
12: [6, 32, 58],
13: [6, 34, 62],
14: [6, 26, 46, 66],
15: [6, 26, 48, 70],
16: [6, 26, 50, 74],
17: [6, 30, 54, 78],
18: [6, 30, 56, 82],
19: [6, 30, 58, 86],
20: [6, 34, 62, 90],
21: [6, 28, 50, 72, 94],
22: [6, 26, 50, 74, 98],
23: [6, 30, 54, 78, 102],
24: [6, 28, 54, 80, 106],
25: [6, 32, 58, 84, 110],
26: [6, 30, 58, 86, 114],
27: [6, 34, 62, 90, 118],
28: [6, 26, 50, 74, 98, 122],
29: [6, 30, 54, 78, 102, 126],
30: [6, 26, 52, 78, 104, 130],
31: [6, 30, 56, 82, 108, 134],
32: [6, 34, 60, 86, 112, 138],
33: [6, 30, 58, 86, 114, 142],
34: [6, 34, 62, 90, 118, 146],
35: [6, 30, 54, 78, 102, 126, 150],
36: [6, 24, 50, 76, 102, 128, 154],
37: [6, 28, 54, 80, 106, 132, 158],
38: [6, 32, 58, 84, 110, 136, 162],
39: [6, 26, 54, 82, 110, 138, 166],
40: [6, 30, 58, 86, 114, 142, 170],
}

// 添加定位
addLocation() {
// 获取当前版本的校正图形位置
let location = Painter.LOCATION[this.version];
// 添加寻象图形(包括分隔符,以寻象图形的左上角x,y开始)
function setLocation(x, y, codeMap) {
// 设置外边框
for (let i = 0; i < 7; i++) {
codeMap[y][x + i] = 1;
codeMap[y + 6][x + i] = 1;
codeMap[y + i][x] = 1;
codeMap[y + i][x + 6] = 1;
}
// 设置内边框(3 * 3区域)
for (let i = x + 2; i < x + 5; i++) {
for (let j = y + 2; j < y + 5; j++) {
codeMap[j][i] = 1;
}
}
// 其余的设置为0(原本位置也是0,为了是一次设置,将该方格所占用)
for (let i = x + 1; i < x + 6; i++) {
for (let j = y + 1; j < y + 6; j++) {
if (codeMap[j][i] !== 1) {
codeMap[j][i] = 0;
}
}
}
// 以下皆为设置分隔符的位置
if (y - 1 >= 0) {
for (let i = 0; i < 8; i++) {
if (x + i < codeMap.length) {
codeMap[y - 1][x + i] = 0;
}
}
}
if (x + 7 < codeMap.length) {
for (let i = 0; i <= 8; i++) {
if (y + i < codeMap.length) {
codeMap[y + i][x + 7] = 0;
}
}
}
if (y + 7 < codeMap.length) {
for (let i = 0; i < 8; i++) {
if (x + i < codeMap.length) {
codeMap[y + 7][x + i] = 0;
}
}
}
if (x - 1 >= 0) {
for (let i = 0; i <= 8; i++) {
if (y + i < codeMap.length) {
codeMap[y + i][x - 1] = 0;
}
}
}
}
// 定位图案
setLocation(0, 0, this._codeMap); // 左上
setLocation(location[location.length - 1], 0, this._codeMap); // 右上
setLocation(0, location[location.length - 1], this._codeMap); // 左下
}

// 添加对齐
addAlignment() {
// 版本2(不包括)以下无对齐元素
if (this.version < 2) {
return;
}
// 获取该版本的校正图形位置
let location = Painter.LOCATION[this.version];
// 设置校正图形(以中心点为x,y)
function setLocation(x, y, codeMap) {
// 设置外边框
for (let i = -2; i < 3; i++) {
codeMap[y - 2][x + i] = 1;
codeMap[y + 2][x + i] = 1;
codeMap[y + i][x - 2] = 1;
codeMap[y + i][x + 2] = 1;
}
// 内边框
codeMap[y][x] = 1;
// 其余设置为0(原本位置也是0,为了是一次设置,将该方格所占用)
for (let i = y - 1; i <= y + 1; i++) {
for (let j = x - 1; j <= x + 1; j++) {
if (codeMap[i][j] !== 1) {
codeMap[i][j] = 0;
}
}
}
}

// 循环位置,如果不是左上、右上、左下,则设置校正图形
for (let i = 0; i < location.length; i++) {
for (let j = 0; j < location.length; j++) {
if ((i === 0 && j === 0) || (i === 0 && j === location.length - 1) || (i === location.length - 1 && j === 0)) {
continue;
}
setLocation(location[i], location[j], this._codeMap);
}
}
}

// 添加时序图(定位图形)
addTimeline() {
// 获取当前版本的校正图形位置
let location = Painter.LOCATION[this.version];
// 设置定位图形
for (let i = 7; i < location[location.length - 1]; i += 2) {
// 黑白间隔
this._codeMap[6][i] = 0;
this._codeMap[6][i + 1] = 1;
this._codeMap[i][6] = 0;
this._codeMap[i + 1][6] = 1;
}
}
}

现在已将功能图形全部设置完了。

编码区

编码区有两个重要的概念,分别为纠错等级和掩膜。

纠错等级

纠错共有 4 个等级,如下表所示:

等级 恢复的容量 % (近似值) 编码
L 7% 01
M 15% 00
Q 25% 11
H 30% 10

掩膜

在最后数据需要跟掩膜进行异或操作,经过掩膜后整体看起来就比较零散了,掩膜共有 8 种,如下表所示:

编码 条件(i为行,j为列)
000 (i+j) mod 2 = 0
001 i mod 2 = 0
010 j mod 3 = 0
011 (i + j) mod 3 = 0
100 (floor(i / 2) + floor(j / 3)) mod 2 = 0
101 (i × j) mod 2 + (i × j) mod 3 = 0
110 ((i × j) mod 2 + (i × j) mod 3) mod 2 = 0
111 ((i × j) mod 3 + (i + j) mod 2) mod 2 = 0
获取掩膜

painter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class MaskPattern {
constructor(size) {
this.size = size;
}

getMask(maskPatternReference) {
let maskPattern = []; // 掩膜
// 根据编码获取条件
let maskPatternFunction = {
0: this.MaskPattern000(),
1: this.MaskPattern001(),
2: this.MaskPattern010(),
3: this.MaskPattern011(),
4: this.MaskPattern100(),
5: this.MaskPattern101(),
6: this.MaskPattern110(),
7: this.MaskPattern111(),
}
// 根据条件获取掩膜
for (let i = 0; i < this.size; i++) {
maskPattern.push([]);
for (let j = 0; j < this.size; j++) {
maskPattern[i][j] = maskPatternFunction[maskPatternReference](i, j);
}
}
return maskPattern;
}

MaskPattern000() {
return (i, j) => {
return (i + j) % 2 === 0;
}
}

MaskPattern001() {
return (i, j) => {
return i % 2 === 0;
}
}

MaskPattern010() {
return (i, j) => {
return j % 3 === 0;
}
}

MaskPattern011() {
return (i, j) => {
return (i + j) % 3 === 0;
}
}

MaskPattern100() {
return (i, j) => {
return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 === 0;
}
}

MaskPattern101() {
return (i, j) => {
return ((i * j) % 2) + ((i * j) % 3) === 0;
}
}

MaskPattern110() {
return (i, j) => {
return (((i * j) % 2) + ((i * j) % 3)) % 2 === 0;
}
}

MaskPattern111() {
return (i, j) => {
return (((i * j) % 3) + ((i + j) % 2)) % 2 === 0;
}
}
}

格式信息

格式信息为 15 位,其中有 5 个数据位,10 个是用 BCH(15,5)编码计算得到的纠错位,在将15位格式信息与 101010000010010 进行异或运算,以确保结果不全是 0。

  • 数据位:分别为两位纠错等级和三位掩膜编码组合而成。(例:假设纠错等级为M,掩膜编码为101,则数据位为00101)

  • 纠错位:根据数据位计算 BCH(15,5)编码得到。

计算得到结果后填入格式信息位置即可,共需要填两遍。

格式信息位置

添加格式信息

painter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class Painter {
// 添加格式信息(需要提供纠错等级和掩膜编码,默认为M和0)
addFormatInfo(errorCorrectionLevel = "M", maskPatternReference = 0) {
// 如果纠错等级不合法,则抛出错误
if (errorCorrectionLevel !== "L" && errorCorrectionLevel !== "M" && errorCorrectionLevel !== "Q" && errorCorrectionLevel !== "H") {
throw new Error("errorCorrectionLevel must be L, M, Q or H");
}
// 如果掩膜编码不合法,则抛出错误
if (maskPatternReference < 0 || maskPatternReference > 7) {
throw new Error("maskPatternReference must be between 0 and 7");
}
// 添加到实例对象上,后面需要用到
this.errorCorrectionLevel = errorCorrectionLevel;
this.maskPatternReference = maskPatternReference;
// 纠错级别
switch (errorCorrectionLevel) {
case "L": errorCorrectionLevel = 0b01; break;
case "M": errorCorrectionLevel = 0b00; break;
case "Q": errorCorrectionLevel = 0b11; break;
case "H": errorCorrectionLevel = 0b10; break;
}
// 数据位(纠错级别 + 掩膜编码)
let dataBit = (errorCorrectionLevel << 3) | maskPatternReference;
// BCH(15, 5)
function BCH_15_To_5_Encode(data) {
const g = 0x537;
data <<= 10; // 填充10个0
let encode = data; // 保留原始数据
// 累除计算余数
for (let i = 4; i >= 0; i--) {
if ((data & (1 << (i + 10))) != 0) {
data ^= g << i;
}
}
// 与原结果进行异或运算
return encode ^ data;
}
// 纠错位(根据数据位计算BCH(15, 5))
let errorCorrectionBit = BCH_15_To_5_Encode(dataBit);
// 15位异或101010000010010
dataBit ^= 0b10101;
errorCorrectionBit ^= 0b0000010010;
// 有低到高存储位数据
let formatInfo = [];
// 添加纠错位
for (let i = 0; i < 10; i++) {
formatInfo.push((errorCorrectionBit & (1 << i)) !== 0 ? 1 : 0);
}
// 添加数据位
for (let i = 0; i < 5; i++) {
formatInfo.push((dataBit & (1 << i)) !== 0 ? 1 : 0);
}

// 添加15位信息(0对应位置0,低到高存储位数据)
this._codeMap[0][8] = formatInfo[0];
this._codeMap[1][8] = formatInfo[1];
this._codeMap[2][8] = formatInfo[2];
this._codeMap[3][8] = formatInfo[3];
this._codeMap[4][8] = formatInfo[4];
this._codeMap[5][8] = formatInfo[5];
this._codeMap[7][8] = formatInfo[6];
this._codeMap[8][8] = formatInfo[7];
this._codeMap[8][7] = formatInfo[8];
this._codeMap[8][5] = formatInfo[9];
this._codeMap[8][4] = formatInfo[10];
this._codeMap[8][3] = formatInfo[11];
this._codeMap[8][2] = formatInfo[12];
this._codeMap[8][1] = formatInfo[13];
this._codeMap[8][0] = formatInfo[14];

// 添加15位信息(0对应位置0,低到高存储位数据)
for (let i = 1; i <= 8; i++) {
this._codeMap[8][this.size - i] = formatInfo[i - 1];
}
for (let i = 0; i < 7; i++) {
this._codeMap[this.size - 7 + i][8] = formatInfo[i + 8];
}
// 深色模块
this._codeMap[this.size - 8][8] = 1;
}
}

版本信息

版本信息为 18 位,其中,6 位数据位,通过 BCH(18,6)编码计算出 12 个纠错位,只有版本 7~40 的符号包含版本信息。

  • 数据位:为版本号,共 6 位。

  • 纠错位:根据数据位计算 BCH(18,6)编码得到。

版本信息位置如下图所示:

版本信息位置

版本信息的模块布置

与格式信息一样,计算得到结果后填入格式信息位置即可,共需要填两遍。

添加版本信息

painter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Painter {
// 添加版本信息(经过该步骤后将ONE_CHANGE设置为false,将不在记录写入情况)
addVersionInfo() {
// 版本7以下没有版本信息
if (this.version < 7) {
Painter.ONE_CHANGE = false;
return;
}
// BCH(18, 6),与BCH(15, 5)类似,其中g修改为0x1f25
function BCH_18_To_6_Encode(data) {
const g = 0x1f25;
data <<= 12;
let encode = data;
for (let i = 5; i >= 0; i--) {
if ((data & (1 << (i + 12))) != 0) {
data ^= g << i;
}
}
return encode ^ data;
}
// 版本信息(6位数据位 + 12位纠错位)
let versionInfoBit = (this.version << 12) | BCH_18_To_6_Encode(this.version);
// 由低到高存储位数据
let versionInfo = [];
for (let i = 0; i < 18; i++) {
versionInfo.push((versionInfoBit & (1 << i)) !== 0 ? 1 : 0);
}
// 填入版本信息(其中0对应位置0)
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 3; j++) {
this._codeMap[this.size - 11 + j][i] = versionInfo[i * 3 + j];
this._codeMap[i][this.size - 11 + j] = versionInfo[i * 3 + j];
}
}
Painter.ONE_CHANGE = false;
}
}

数据编码

数据编码模式共有以下类型,这里只介绍数字模式、字母数字模式和8位字节模式。

编码模式:

模式 指示符
ECI 0111
数字 0001
字母数字 0010
8位字节 0100
日本汉字 1000
中国汉字 1101
结构链接 0011
FNC1 0101 (第一位置)
FNC1 1001 (第一位置)
终止符(信息结尾) 0000

字符计数指示符的位数:

版本 数字模式 字母数字模式 8 位字节模式 日本汉字模式 中国汉字模式
1~9 10 9 8 8
10~26 12 11 16 10
27~40 14 13 16 12

数字模式

将输入的数据每三位分为一组,将每组数据转换为 10 位二进制数。如果所输入的数据的位数不是 3 的整数倍,所余的 1 位或 2 位数字应分别转换为 4 位或 7 位二进制数。将二进制数据连接起来并在前面加上模式指示符和字符计数指示符。

例:(符号版本 1-H)

  • 输入的数据: 01234567

  1. 分为 3 位一组: 012 345 67

  2. 将每组转换为二进制:
    012→0000001100
    345→0101011001
    67 →1000011

  3. 将二进制数连接为一个序列:0000001100 0101011001 1000011

  4. 将字符计数指示符转换为二进制(版本 1-H 为 10 位):字符数为:8→0000001000

  5. 加入模式指示符 0001 以及字符计数指示符的二进制数据:
    0001 0000001000 0000001100 0101011001 1000011

字母数字模式

按照下表将输入的数据分为两个字符一组,用 11 位二进制表示。将前面字符的值乘以 45 与第二个字符的值相加,将所得的结果转换为 11 位二进制数。如果输入的数据的字符数不是 2 的整数倍,将最后一个字符编码为 6 位二进制数。将所得的二进制数据连接起来并在前面加上模式指示符和字符计数指示符。

字母数字模式的编码/译码表

例:(符号版本 1-H)

  • 输入的数据: AC-42

  1. 根据表 5 查出字符的值: AC-41→(10,12,41,4,2)

  2. 将结果分为 2 个一组: (10,12)(41,4)(2)

  3. 将每组数据转换为 11 位二进制数:(10,12)1045+12→462→00111001110
    (41,4)41
    45+4→1849→11100111001
    (2)→2→000010

  4. 二进制数据顺次连接: 00111001110 11100111001 000010

  5. 将字符计数指示符转换为二进制(版本 1-H 为 9 位):输入的字符数 5→000000101

  6. 在二进制数据前加上模式指示符 0010 和字符计数指示符:
    0010 000000101 00111001110 11100111001 000010

8 位字节模式

将输入的数据转换为 8 位二进制数,并在前面加上模式指示符和字符计数指示符。

终止符

符号的数据结尾由紧跟在最后一个模式段后面的终止符序列 0000 表示,当数据位流数量正好填满符号的容量时,它可以省略,或者当符号所余的容量不足 4 位时它可以截短。

数据编码

painter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
class DataEncode {
/**
* 版本 | 数字模式 | 字母数字模式 | 8位字节模式 | 日本汉字模式 | 中国汉字模式
* 1~9 | 10 | 9 | 8 | 8 |
* 10~26 | 12 | 11 | 16 | 10 |
* 27~40 | 14 | 13 | 16 | 12 |
*/
static CHRE_COUNT_BITS = [
[10, 9, 8, 8],
[12, 11, 16, 10],
[14, 13, 16, 12]
];
/* 字母数字表格 */
static LETTER_NUMBER_ENCODE_TABLE = {
"0": 0,
"1": 1,
"2": 2,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"A": 10,
"B": 11,
"C": 12,
"D": 13,
"E": 14,
"F": 15,
"G": 16,
"H": 17,
"I": 18,
"J": 19,
"K": 20,
"L": 21,
"M": 22,
"N": 23,
"O": 24,
"P": 25,
"Q": 26,
"R": 27,
"S": 28,
"T": 29,
"U": 30,
"V": 31,
"W": 32,
"X": 33,
"Y": 34,
"Z": 35,
" ": 36,
"$": 37,
"%": 38,
"*": 39,
"+": 40,
"-": 41,
".": 42,
"/": 43,
":": 44,
}

/**
* ECI 0b0111
* 数字 0b0001
* 字母数字 0b0010
* 8位字节 0b0100
* 日本汉字 0b1000
* 中国汉字 0b1101
* 结构链接 0b0011
* FNC1 0b0101 (第一位置)
* 0b1001 (第二位置)
* 终止符(信息结尾) 0b0000
*/
/**
* @param {Object} data 数据
* @param {number} version 版本
* @param {number} mode 模式
*/
static getDataEncode(data, version, mode) {
let modeIndex = -1; // 对应的模式下标
let versionIndex = -1; // 对应的版本下标
// 根据表格设置
switch (mode) {
case 0b0001: modeIndex = 0; break;
case 0b0010: modeIndex = 1; break;
case 0b0100: modeIndex = 2; break;
case 0b1000: modeIndex = 3; break;
}
if (version >= 1 && version <= 9) {
versionIndex = 0;
} else if (version >= 10 && version <= 26) {
versionIndex = 1;
} else if (version >= 27 && version <= 40) {
versionIndex = 2;
}
if (modeIndex == -1) {
throw new Error("没有找到对应的模式");
}
if (versionIndex == -1) {
throw new Error("没有找到对应的版本");
}
// 数字编码
let dataArray = [];
let dataBitArray = [];
// 添加编码模式
for (let i = 3; i >= 0; i--) {
dataBitArray.push((mode >> i) & 1);
}
// 添加长度
let charCountLength = DataEncode.CHRE_COUNT_BITS[versionIndex][modeIndex];
let dataLength = data.length;
for (let i = charCountLength - 1; i >= 0; i--) {
dataBitArray.push((dataLength >> i) & 1);
}

// 根据模式调用不同的方法
switch (mode) {
case 0b0001: DataEncode.numberEncode(data, dataBitArray); break;
case 0b0010: DataEncode.letterNumberEncode(data, dataBitArray); break;
case 0b0100: DataEncode.byteEncode(data, dataBitArray); break;
case 0b1000: DataEncode.japaneseEncode(data, dataBitArray); break;
}


// 添加结束符
for (let i = 0; i < 4; i++) {
dataBitArray.push(0);
}

// 补为长度为8的倍数
while (dataBitArray.length % 8 != 0) {
dataBitArray.push(0);
}

// 将数据转换为8位二进制数组
let i = 0, end = dataBitArray.length;
while (i < end) {
let value = 0;
for (let j = 0; j < 8; j++) {
value += dataBitArray[i + j] << (7 - j);
}
dataArray.push(value);
i += 8;
}

return dataArray;
}

/**
* 数字编码
* @param {string} data 数据
* @param {Array} dataBitArray 数据位数组
*/
static numberEncode(data, dataBitArray) {
// 每3个一位
let charArray = [];
for (let i = 0; i < data.length; i += 3) {
charArray.push(data.substring(i, i + 3));
}
// 添加数据
for (let i = 0; i < charArray.length; i++) {
let num = Number(charArray[i]);
let bitLength = 0;
if (charArray[i].length === 3) {
bitLength = 10;
} else if (charArray[i].length === 2) {
bitLength = 7;
} else if (charArray[i].length === 1) {
bitLength = 4;
}

for (let i = bitLength - 1; i >= 0; i--) {
dataBitArray.push((num >> i) & 1);
}
}
return dataBitArray;
}

/**
* 字母数字编码
* @param {*} data
* @param {*} dataBitArray
*/
static letterNumberEncode(data, dataBitArray) {
let charArray = [];
// 每两位一组
for (let i = 0; i < data.length; i += 2) {
charArray.push(data.substring(i, i + 2));
}
// 查表添加数据
for (let i = 0; i < charArray.length; i++) {
let num, bitLength;
if (charArray[i].length == 2) {
num = DataEncode.LETTER_NUMBER_ENCODE_TABLE[charArray[i][0]] * 45 + DataEncode.LETTER_NUMBER_ENCODE_TABLE[charArray[i][1]];
bitLength = 11;
} else {
num = DataEncode.LETTER_NUMBER_ENCODE_TABLE[charArray[i][0]];
bitLength = 6;
}
for (let i = bitLength - 1; i >= 0; i--) {
dataBitArray.push((num >> i) & 1);
}
}
return dataBitArray;
}

/**
* 8位字节编码
* @param {*} data
* @param {*} dataBitArray
*/
static byteEncode(data, dataBitArray) {
// 根据字符编码添加数据
for (let i = 0; i < data.length; i++) {
let num = data.charCodeAt(i);
for (let j = 7; j >= 0; j--) {
dataBitArray.push((num >> j) & 1);
}
}
}

/**
* 日本汉字编码
* @param {*} data
* @param {*} dataBitArray
*/
static japaneseEncode(data, dataBitArray) {
throw new Error("暂不支持");
}
}

10位流到码字的转换

每个模式段的位流需要按顺序连接在一起,最后添加终止符,除非数据位流正好填满符号容量。所得的数据位流将被分为一个个码字;所有的码字的长度都是 8 位,如果位流长度最后一个码字不足 8 位,则用二进制值为 0 的填充位填充至 8 位,填充位应加在数据位流最后 1 位(最低位)的后面。然后根据各个版本的版本号和纠错等级交替添加填充码字11101100 和 00010001,将数据位流扩展,以填满符号的数据容量(具体数据在代码里编写)。

数据编码

painter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
class PaddingData {
// 版本号和纠错等级对应的数据长度
static PADDING_DATA = {
"1-L": 152,
"1-M": 128,
"1-Q": 104,
"1-H": 72,
"2-L": 272,
"2-M": 224,
"2-Q": 176,
"2-H": 128,
"3-L": 440,
"3-M": 352,
"3-Q": 272,
"3-H": 208,
"4-L": 640,
"4-M": 512,
"4-Q": 384,
"4-H": 288,
"5-L": 864,
"5-M": 688,
"5-Q": 496,
"5-H": 368,
"6-L": 1088,
"6-M": 864,
"6-Q": 608,
"6-H": 480,
"7-L": 1248,
"7-M": 992,
"7-Q": 704,
"7-H": 528,
"8-L": 1552,
"8-M": 1232,
"8-Q": 880,
"8-H": 688,
"9-L": 1856,
"9-M": 1456,
"9-Q": 1056,
"9-H": 800,
"10-L": 2192,
"10-M": 1728,
"10-Q": 1232,
"10-H": 976,
"11-L": 2592,
"11-M": 2032,
"11-Q": 1440,
"11-H": 1120,
"12-L": 2960,
"12-M": 2320,
"12-Q": 1648,
"12-H": 1264,
"13-L": 3424,
"13-M": 2672,
"13-Q": 1952,
"13-H": 1440,
"14-L": 3688,
"14-M": 3920,
"14-Q": 2088,
"14-H": 1576,
"15-L": 4184,
"15-M": 3320,
"15-Q": 2360,
"15-H": 1784,
"16-L": 4712,
"16-M": 3624,
"16-Q": 2600,
"16-H": 2024,
"17-L": 5176,
"17-M": 4056,
"17-Q": 2936,
"17-H": 2264,
"18-L": 5768,
"18-M": 4504,
"18-Q": 3176,
"18-H": 2504,
"19-L": 6360,
"19-M": 5016,
"19-Q": 3560,
"19-H": 2728,
"20-L": 6888,
"20-M": 5352,
"20-Q": 3880,
"20-H": 3080,
"21-L": 7456,
"21-M": 5712,
"21-Q": 4096,
"21-H": 3248,
"22-L": 8048,
"22-M": 6256,
"22-Q": 4544,
"22-H": 3536,
"23-L": 8752,
"23-M": 6880,
"23-Q": 4912,
"23-H": 3712,
"24-L": 9392,
"24-M": 7312,
"24-Q": 5312,
"24-H": 4112,
"25-L": 10208,
"25-M": 8000,
"25-Q": 5744,
"25-H": 4304,
"26-L": 10960,
"26-M": 8496,
"26-Q": 6032,
"26-H": 4768,
"27-L": 11744,
"27-M": 9024,
"27-Q": 6464,
"27-H": 5024,
"28-L": 12248,
"28-M": 9544,
"28-Q": 6968,
"28-H": 5288,
"29-L": 13048,
"29-M": 10136,
"29-Q": 7288,
"29-H": 5608,
"30-L": 13880,
"30-M": 10984,
"30-Q": 7880,
"30-H": 5960,
"31-L": 14744,
"31-M": 11640,
"31-Q": 8264,
"31-H": 6344,
"32-L": 15640,
"32-M": 12328,
"32-Q": 8920,
"32-H": 6760,
"33-L": 16568,
"33-M": 13048,
"33-Q": 9368,
"33-H": 7208,
"34-L": 17528,
"34-M": 13800,
"34-Q": 9848,
"34-H": 7688,
"35-L": 18448,
"35-M": 14496,
"35-Q": 10288,
"35-H": 7888,
"36-L": 19472,
"36-M": 15312,
"36-Q": 10832,
"36-H": 8432,
"37-L": 20528,
"37-M": 15936,
"37-Q": 11408,
"37-H": 8768,
"38-L": 21616,
"38-M": 16816,
"38-Q": 12016,
"38-H": 9136,
"39-L": 22496,
"39-M": 17728,
"39-Q": 12656,
"39-H": 9776,
"40-L": 23648,
"40-M": 18672,
"40-Q": 13328,
"40-H": 10208
}

static padding(data, version, errorCorrectionLevel) {
// 根据版本号和纠错等级获取对应的数据长度
let paddingData = PaddingData.PADDING_DATA[`${version}-${errorCorrectionLevel}`] / 8;
let dataLength = data.length; // 数据长度
// 填充字符
const a = 0b11101100;
const b = 0b00010001;
// 填充到 paddingData 位
while (dataLength < paddingData) {
// 填充a
data.push(a);
dataLength += 1;
// 如果长度够了则退出
if (dataLength >= paddingData) {
break;
}
// 填充b
data.push(b);
dataLength += 1;
}
return data;
}
}

Reed–Solomon

在计算完数据码字后,需要将数据按照版本号和纠错等级分块进行 Reed–Solomon 算法计算纠错码,并安照如下顺序进行拼接(具体分块数据在代码里编写)。

最终码字序列

Reed–Solomon

painter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
class ECC {
static PRIM = 0x11d;
static GF_EXP = new Array(512); // 逆对数(指数)表
static GF_LOG = new Array(256); // 对数表

// 码字总数(blocksNumber:块数,dataCodes对应的纠错代码)
static DATA_BLOCKS_NUMBER = {
"1-L": {
blocksNumber: [1],
dataCodes: [
[26, 12, 2]
]
},
"1-M": {
blocksNumber: [1],
dataCodes: [
[26, 16, 4]
]
},
"1-Q": {
blocksNumber: [1],
dataCodes: [
[26, 13, 6]
]
},
"1-H": {
blocksNumber: [1],
dataCodes: [
[26, 9, 8]
]
},
"2-L": {
blocksNumber: [1],
dataCodes: [
[44, 34, 4]
]
},
"2-M": {
blocksNumber: [1],
dataCodes: [
[44, 28, 8]
]
},
"2-Q": {
blocksNumber: [1],
dataCodes: [
[44, 22, 11]
]
},
"2-H": {
blocksNumber: [1],
dataCodes: [
[44, 16, 14]
]
},
"3-L": {
blocksNumber: [1],
dataCodes: [
[70, 55, 7]
]
},
"3-M": {
blocksNumber: [1],
dataCodes: [
[70, 44, 13]
]
},
"3-Q": {
blocksNumber: [2],
dataCodes: [
[35, 17, 9]
]
},
"3-H": {
blocksNumber: [2],
dataCodes: [
[35, 13, 11]
]
},
"4-L": {
blocksNumber: [1],
dataCodes: [
[100, 80, 10]
]
},
"4-M": {
blocksNumber: [2],
dataCodes: [
[50, 32, 9]
]
},
"4-Q": {
blocksNumber: [2],
dataCodes: [
[50, 24, 13]
]
},
"4-H": {
blocksNumber: [4],
dataCodes: [
[25, 9, 8]
]
},
"5-L": {
blocksNumber: [1],
dataCodes: [
[134, 108, 13]
]
},
"5-M": {
blocksNumber: [2],
dataCodes: [
[67, 43, 12]
]
},
"5-Q": {
blocksNumber: [2, 2],
dataCodes: [
[33, 15, 9],
[34, 16, 9]
]
},
"5-H": {
blocksNumber: [2, 2],
dataCodes: [
[33, 11, 11],
[34, 12, 11]
]
},
"6-L": {
blocksNumber: [2],
dataCodes: [
[86, 68, 9]
]
},
"6-M": {
blocksNumber: [4],
dataCodes: [
[43, 27, 8]
]
},
"6-Q": {
blocksNumber: [4],
dataCodes: [
[43, 19, 12]
]
},
"6-H": {
blocksNumber: [4],
dataCodes: [
[43, 15, 14]
]
},
"7-L": {
blocksNumber: [2],
dataCodes: [
[98, 78, 10]
]
},
"7-M": {
blocksNumber: [4],
dataCodes: [
[49, 31, 9]
]
},
"7-Q": {
blocksNumber: [2, 4],
dataCodes: [
[32, 14, 9],
[33, 15, 9]
]
},
"7-H": {
blocksNumber: [4, 1],
dataCodes: [
[39, 13, 13],
[40, 14, 13]
]
},
"8-L": {
blocksNumber: [2],
dataCodes: [
[121, 97, 12]
]
},
"8-M": {
blocksNumber: [2, 2],
dataCodes: [
[60, 38, 11],
[61, 39, 11]
]
},
"8-Q": {
blocksNumber: [4, 2],
dataCodes: [
[40, 18, 11],
[41, 19, 11]
]
},
"8-H": {
blocksNumber: [4, 2],
dataCodes: [
[40, 14, 13],
[41, 15, 13]
]
},
"9-L": {
blocksNumber: [2],
dataCodes: [
[146, 116, 15]
]
},
"9-M": {
blocksNumber: [3, 2],
dataCodes: [
[58, 36, 11],
[59, 37, 11]
]
},
"9-Q": {
blocksNumber: [4, 4],
dataCodes: [
[36, 16, 10],
[37, 17, 10]
]
},
"9-H": {
blocksNumber: [4, 4],
dataCodes: [
[36, 12, 12],
[37, 13, 12]
]
},
"10-L": {
blocksNumber: [2, 2],
dataCodes: [
[86, 68, 9],
[87, 69, 9]
]
},
"10-M": {
blocksNumber: [4, 1],
dataCodes: [
[69, 43, 13],
[70, 44, 13]
]
},
"10-Q": {
blocksNumber: [6, 2],
dataCodes: [
[43, 19, 12],
[44, 20, 12]
]
},
"10-H": {
blocksNumber: [6, 2],
dataCodes: [
[43, 15, 14],
[44, 16, 14]
]
},
"11-L": {
blocksNumber: [4],
dataCodes: [
[101, 81, 10]
]
},
"11-M": {
blocksNumber: [1, 4],
dataCodes: [
[80, 50, 15],
[81, 51, 15]
]
},
"11-Q": {
blocksNumber: [4, 4],
dataCodes: [
[50, 22, 14],
[51, 23, 14]
]
},
"11-H": {
blocksNumber: [3, 8],
dataCodes: [
[36, 12, 12],
[37, 13, 12]
]
},
"12-L": {
blocksNumber: [2, 2],
dataCodes: [
[116, 92, 12],
[117, 93, 12]
]
},
"12-M": {
blocksNumber: [6, 2],
dataCodes: [
[58, 36, 11],
[59, 37, 11]
]
},
"12-Q": {
blocksNumber: [4, 6],
dataCodes: [
[46, 20, 13],
[47, 21, 13]
]
},
"12-H": {
blocksNumber: [7, 4],
dataCodes: [
[42, 14, 14],
[43, 15, 14]
]
},
"13-L": {
blocksNumber: [4],
dataCodes: [
[133, 107, 13]
]
},
"13-M": {
blocksNumber: [8, 1],
dataCodes: [
[59, 37, 11],
[60, 38, 11]
]
},
"13-Q": {
blocksNumber: [8, 4],
dataCodes: [
[44, 20, 12],
[45, 21, 12]
]
},
"13-H": {
blocksNumber: [12, 4],
dataCodes: [
[33, 11, 11],
[34, 12, 11]
]
},
"14-L": {
blocksNumber: [3, 1],
dataCodes: [
[145, 115, 15],
[146, 116, 15]
]
},
"14-M": {
blocksNumber: [4, 5],
dataCodes: [
[64, 40, 12],
[65, 41, 12]
]
},
"14-Q": {
blocksNumber: [11, 5],
dataCodes: [
[36, 16, 10],
[37, 17, 10]
]
},
"14-H": {
blocksNumber: [11, 5],
dataCodes: [
[36, 12, 12],
[37, 13, 12]
]
},
"15-L": {
blocksNumber: [5, 1],
dataCodes: [
[109, 87, 11],
[110, 88, 11]
]
},
"15-M": {
blocksNumber: [5, 5],
dataCodes: [
[65, 41, 12],
[66, 42, 12]
]
},
"15-Q": {
blocksNumber: [5, 7],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"15-H": {
blocksNumber: [11, 7],
dataCodes: [
[36, 12, 12],
[37, 13, 12]
]
},
"16-L": {
blocksNumber: [5, 1],
dataCodes: [
[122, 98, 12],
[123, 99, 12]
]
},
"16-M": {
blocksNumber: [7, 3],
dataCodes: [
[73, 45, 14],
[74, 46, 14]
]
},
"16-Q": {
blocksNumber: [15, 2],
dataCodes: [
[43, 19, 12],
[44, 20, 12]
]
},
"16-H": {
blocksNumber: [3, 13],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
},
"17-L": {
blocksNumber: [1, 5],
dataCodes: [
[135, 107, 14],
[136, 108, 14]
]
},
"17-M": {
blocksNumber: [10, 1],
dataCodes: [
[74, 46, 14],
[75, 47, 14]
]
},
"17-Q": {
blocksNumber: [1, 15],
dataCodes: [
[50, 22, 14],
[51, 23, 14]
]
},
"17-H": {
blocksNumber: [2, 17],
dataCodes: [
[42, 14, 14],
[43, 15, 14]
]
},
"18-L": {
blocksNumber: [5, 1],
dataCodes: [
[150, 120, 15],
[151, 121, 15]
]
},
"18-M": {
blocksNumber: [9, 4],
dataCodes: [
[69, 43, 13],
[70, 44, 13]
]
},
"18-Q": {
blocksNumber: [17, 1],
dataCodes: [
[50, 22, 14],
[51, 23, 14]
]
},
"18-H": {
blocksNumber: [2, 19],
dataCodes: [
[42, 14, 14],
[43, 15, 14]
]
},
"19-L": {
blocksNumber: [3, 4],
dataCodes: [
[141, 113, 14],
[142, 114, 14]
]
},
"19-M": {
blocksNumber: [3, 11],
dataCodes: [
[70, 44, 13],
[71, 45, 13]
]
},
"19-Q": {
blocksNumber: [17, 4],
dataCodes: [
[47, 21, 13],
[48, 22, 13]
]
},
"19-H": {
blocksNumber: [9, 16],
dataCodes: [
[39, 13, 13],
[40, 14, 13]
]
},
"20-L": {
blocksNumber: [3, 5],
dataCodes: [
[135, 107, 14],
[136, 108, 14]
]
},
"20-M": {
blocksNumber: [3, 13],
dataCodes: [
[67, 41, 13],
[68, 42, 13]
]
},
"20-Q": {
blocksNumber: [15, 5],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"20-H": {
blocksNumber: [15, 10],
dataCodes: [
[43, 15, 14],
[44, 16, 14]
]
},
"21-L": {
blocksNumber: [4, 4],
dataCodes: [
[144, 116, 14],
[145, 117, 14]
]
},
"21-M": {
blocksNumber: [17],
dataCodes: [
[68, 42, 13]
]
},
"21-Q": {
blocksNumber: [17, 6],
dataCodes: [
[50, 22, 14],
[51, 23, 14]
]
},
"21-H": {
blocksNumber: [19, 6],
dataCodes: [
[46, 16, 15],
[47, 17, 15]
]
},
"22-L": {
blocksNumber: [2, 7],
dataCodes: [
[139, 111, 14],
[140, 112, 14]
]
},
"22-M": {
blocksNumber: [17],
dataCodes: [
[74, 46, 14]
]
},
"22-Q": {
blocksNumber: [7, 16],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"22-H": {
blocksNumber: [34],
dataCodes: [
[37, 13, 12]
]
},
"23-L": {
blocksNumber: [4, 5],
dataCodes: [
[151, 121, 15],
[152, 122, 15]
]
},
"23-M": {
blocksNumber: [4, 14],
dataCodes: [
[75, 47, 14],
[76, 48, 14]
]
},
"23-Q": {
blocksNumber: [11, 14],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"23-H": {
blocksNumber: [16, 14],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
},
"24-L": {
blocksNumber: [6, 4],
dataCodes: [
[147, 117, 15],
[148, 118, 15]
]
},
"24-M": {
blocksNumber: [6, 14],
dataCodes: [
[73, 45, 14],
[74, 46, 14]
]
},
"24-Q": {
blocksNumber: [11, 16],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"24-H": {
blocksNumber: [30, 2],
dataCodes: [
[46, 16, 15],
[47, 17, 15]
]
},
"25-L": {
blocksNumber: [8, 4],
dataCodes: [
[132, 106, 13],
[133, 107, 13]
]
},
"25-M": {
blocksNumber: [8, 13],
dataCodes: [
[75, 47, 14],
[76, 48, 14]
]
},
"25-Q": {
blocksNumber: [7, 22],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"25-H": {
blocksNumber: [22, 13],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
},
"26-L": {
blocksNumber: [10, 2],
dataCodes: [
[142, 114, 14],
[143, 115, 14]
]
},
"26-M": {
blocksNumber: [19, 4],
dataCodes: [
[74, 46, 14],
[75, 47, 14]
]
},
"26-Q": {
blocksNumber: [28, 6],
dataCodes: [
[50, 22, 14],
[51, 23, 14]
]
},
"26-H": {
blocksNumber: [33, 4],
dataCodes: [
[46, 16, 15],
[47, 17, 15]
]
},
"27-L": {
blocksNumber: [8, 4],
dataCodes: [
[152, 122, 15],
[153, 123, 15]
]
},
"27-M": {
blocksNumber: [22, 3],
dataCodes: [
[73, 45, 14],
[74, 46, 14]
]
},
"27-Q": {
blocksNumber: [8, 26],
dataCodes: [
[53, 23, 15],
[54, 24, 15]
]
},
"27-H": {
blocksNumber: [12, 28],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
},
"28-L": {
blocksNumber: [3, 10],
dataCodes: [
[147, 117, 15],
[148, 118, 15]
]
},
"28-M": {
blocksNumber: [3, 23],
dataCodes: [
[73, 45, 14],
[74, 46, 14]
]
},
"28-Q": {
blocksNumber: [55, 25],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"28-H": {
blocksNumber: [11, 31],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
},
"29-L": {
blocksNumber: [7, 7],
dataCodes: [
[146, 116, 15],
[147, 117, 15]
]
},
"29-M": {
blocksNumber: [21, 7],
dataCodes: [
[73, 45, 14],
[74, 46, 14]
]
},
"29-Q": {
blocksNumber: [1, 37],
dataCodes: [
[53, 23, 15],
[54, 24, 15]
]
},
"29-H": {
blocksNumber: [19, 26],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
},
"30-L": {
blocksNumber: [5, 10],
dataCodes: [
[145, 115, 15],
[146, 116, 15]
]
},
"30-M": {
blocksNumber: [19, 10],
dataCodes: [
[75, 47, 14],
[76, 48, 14]
]
},
"30-Q": {
blocksNumber: [15, 25],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"30-H": {
blocksNumber: [23, 25],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
},
"31-L": {
blocksNumber: [13, 3],
dataCodes: [
[145, 115, 15],
[146, 116, 15]
]
},
"31-M": {
blocksNumber: [2, 29],
dataCodes: [
[74, 46, 14],
[75, 47, 14]
]
},
"31-Q": {
blocksNumber: [42, 1],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"31-H": {
blocksNumber: [23, 28],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
},
"32-L": {
blocksNumber: [17],
dataCodes: [
[145, 115, 15]
]
},
"32-M": {
blocksNumber: [10, 23],
dataCodes: [
[74, 46, 14],
[75, 47, 14]
]
},
"32-Q": {
blocksNumber: [10, 35],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"32-H": {
blocksNumber: [19, 35],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
},
"33-L": {
blocksNumber: [17, 1],
dataCodes: [
[145, 115, 15],
[146, 116, 15]
]
},
"33-M": {
blocksNumber: [14, 21],
dataCodes: [
[74, 46, 14],
[75, 47, 14]
]
},
"33-Q": {
blocksNumber: [29, 19],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"33-H": {
blocksNumber: [11, 46],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
},
"34-L": {
blocksNumber: [13, 6],
dataCodes: [
[145, 115, 15],
[146, 116, 15]
]
},
"34-M": {
blocksNumber: [14, 23],
dataCodes: [
[74, 46, 14],
[75, 47, 14]
]
},
"34-Q": {
blocksNumber: [44, 7],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"34-H": {
blocksNumber: [59, 1],
dataCodes: [
[46, 16, 15],
[47, 17, 15]
]
},
"35-L": {
blocksNumber: [12, 7],
dataCodes: [
[151, 121, 15],
[152, 122, 15]
]
},
"35-M": {
blocksNumber: [12, 26],
dataCodes: [
[75, 47, 14],
[76, 48, 14]
]
},
"35-Q": {
blocksNumber: [39, 14],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"35-H": {
blocksNumber: [22, 41],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
},
"36-L": {
blocksNumber: [6, 14],
dataCodes: [
[151, 121, 15],
[152, 122, 15]
]
},
"36-M": {
blocksNumber: [6, 34],
dataCodes: [
[75, 47, 14],
[76, 48, 14]
]
},
"36-Q": {
blocksNumber: [46, 10],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"36-H": {
blocksNumber: [2, 64],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
},
"37-L": {
blocksNumber: [17, 4],
dataCodes: [
[152, 122, 15],
[153, 123, 15]
]
},
"37-M": {
blocksNumber: [29, 14],
dataCodes: [
[74, 46, 14],
[75, 47, 14]
]
},
"37-Q": {
blocksNumber: [49, 10],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"37-H": {
blocksNumber: [24, 46],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
},
"38-L": {
blocksNumber: [4, 18],
dataCodes: [
[152, 122, 15],
[153, 123, 15]
]
},
"38-M": {
blocksNumber: [13, 32],
dataCodes: [
[74, 46, 14],
[75, 47, 14]
]
},
"38-Q": {
blocksNumber: [48, 14],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"38-H": {
blocksNumber: [42, 32],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
},
"39-L": {
blocksNumber: [20, 4],
dataCodes: [
[147, 117, 15],
[148, 118, 15]
]
},
"39-M": {
blocksNumber: [40, 7],
dataCodes: [
[75, 47, 14],
[76, 48, 14]
]
},
"39-Q": {
blocksNumber: [43, 22],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"39-H": {
blocksNumber: [10, 67],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
},
"40-L": {
blocksNumber: [19, 6],
dataCodes: [
[148, 118, 15],
[149, 119, 15]
]
},
"40-M": {
blocksNumber: [18, 31],
dataCodes: [
[75, 47, 14],
[76, 48, 14]
]
},
"40-Q": {
blocksNumber: [34, 34],
dataCodes: [
[54, 24, 15],
[55, 25, 15]
]
},
"40-H": {
blocksNumber: [20, 61],
dataCodes: [
[45, 15, 15],
[46, 16, 15]
]
}
};

constructor() {
let x = 1;
for (let i = 0; i < 256; i++) {
ECC.GF_EXP[i] = x;
ECC.GF_LOG[x] = i;
x = this.Gf_MultNoLUT(x, 2);
}
for (let i = 255; i < 512; i++) {
ECC.GF_EXP[i] = ECC.GF_EXP[i - 255];
}
}

//伽罗华域乘法
Gf_MultNoLUT(a, b) {
let p = 0;
while (b != 0) {
if ((b & 1) != 0) {
p ^= a;
}
b >>= 1;
a <<= 1;
if ((a & 256) != 0) {
a ^= ECC.PRIM;
}
}
return p;
}

//伽罗华域乘法
Gf_Mul(a, b) {
if (a === 0 || b === 0) {
return 0;
}
return ECC.GF_EXP[ECC.GF_LOG[a] + ECC.GF_LOG[b]];
}

//伽罗华域幂
Gf_Pow(x, power) {
return ECC.GF_EXP[(ECC.GF_LOG[x] * power) % 255];
}

// 多项式乘法
Gf_PolyMul(p, q) {
let result = new Array(p.length + q.length - 1);
for (let i = 0; i < q.length; i++) {
for (let j = 0; j < p.length; j++) {
result[i + j] ^= this.Gf_Mul(p[j], q[i]);
}
}
return result;
}

// 获取纠错码字的生成多项式
RsGeneratorPoly(nsym) {
let g = [1];
for (let i = 0; i < nsym; i++) {
g = this.Gf_PolyMul(g, [1, this.Gf_Pow(2, i)]);
}
return g;
}

// 生成纠错码,并添加在数据码字之后
RsEncodeMsg(msgIn, nsym) {
if (msgIn.length + nsym > 255) {
throw new Error("Array length > 255");
}
let gen = this.RsGeneratorPoly(nsym);
let msgOut = new Array(msgIn.length + gen.length - 1);
for (let i = 0; i < msgIn.length; i++) {
msgOut[i] = msgIn[i];
}
for (let i = 0; i < msgIn.length; i++) {
let coef = msgOut[i];
if (coef != 0) {
for (let j = 1; j < gen.length; j++) {
msgOut[i + j] ^= this.Gf_Mul(gen[j], coef);
}
}
}
for (let i = 0; i < msgIn.length; i++) {
msgOut[i] = msgIn[i];
}
return msgOut;
}

// 根据码字总数生成纠错码
getECC(data, version, errorCorrectionLevel) {
let eccBlock = ECC.DATA_BLOCKS_NUMBER[`${version}-${errorCorrectionLevel}`];
let result = []; // 最终码字
let dataBlocks = []; // 数据码字
let eccBlocks = []; // 纠错码
let currentIndex = 0;
for (let i = 0; i < eccBlock.blocksNumber.length; i++) {
let blocksNumber = eccBlock.blocksNumber[i]; // 当前块数
let c = eccBlock.dataCodes[i][0];
let k = eccBlock.dataCodes[i][1];
for (let j = 0; j < blocksNumber; j++) {
let block = data.slice(currentIndex, currentIndex + k); // 当前块
dataBlocks.push(block); // 添加数据码字
eccBlocks.push(this.RsEncodeMsg(block, (c - k)).slice(k)); // 添加纠错码
currentIndex += k; // 下一个块
}
}
// 根据最终编码的介绍,计算最终编码
for (let i = 0; i < dataBlocks[dataBlocks.length - 1].length; i++) {
for (let j = 0; j < dataBlocks.length; j++) {
if (dataBlocks[j].length > i) {
result.push(dataBlocks[j][i]);
}
}
}
for (let i = 0; i < eccBlocks[eccBlocks.length - 1].length; i++) {
for (let j = 0; j < eccBlocks.length; j++) {
if (eccBlocks[j].length > i) {
result.push(eccBlocks[j][i]);
}
}
}
return result;
}
}

码字在矩阵中的布置

简单来说就是从右下角开始,交叉访问为访问过的数据,如下图所示:

码字在矩阵中的布置

添加数据

painter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
class Painter {
// 添加数据 ("0123456789", 0b0001)
addData(data, mode) {
// 先将数进行编码
let dataArray = DataEncode.getDataEncode(data, this.version, mode);
// 填充到指定位数
dataArray = PaddingData.padding(dataArray, this.version, this.errorCorrectionLevel);
// 加上纠错编码获得最终数据
dataArray = new ECC().getECC(dataArray, this.version, this.errorCorrectionLevel);

let y = this.size - 1; // y坐标
let x = this.size - 1; // x坐标
let direction = -1; // 方向
for (let i = 0; i < dataArray.length; i++) {
for (let j = 0; j < 8; j++) {
// 填充数据
this._codeMap[y][x] = (dataArray[i] & (1 << (7 - j))) !== 0 ? 1 : 0;
// 获取下一个位置
while (true) {
if (x < 0) {
break;
}
// x >= 6 和 x < 6 是相反的
if (x > 6) {
if (x % 2 === 0) {
// 偶数位
if (!this._codeMap[y].flag[x - 1]) {
x -= 1;
break;
} else {
x -= 1;
continue;
}
} else {
// 奇数位
if (y + direction < 0 || y + direction >= this.size) {
direction *= -1;
x -= 2;
y -= direction;
if (x <= 6) {
x -= 1;
}
continue;
}
if (!this._codeMap[y + direction].flag[x + 1]) {
x += 1;
y += direction;
break;
} else {
x += 1;
y += direction;
continue;
}
}
} else {
if (x % 2 === 1) {
if (!this._codeMap[y].flag[x - 1]) {
x -= 1;
break;
} else {
x -= 1;
continue;
}
} else {
if (y + direction < 0 || y + direction >= this.size) {
direction *= -1;
x -= 2;
y -= direction;
continue;
}
if (!this._codeMap[y + direction].flag[x + 1]) {
x += 1;
y += direction;
break;
} else {
x += 1;
y += direction;
continue;
}
}
}

}
}
}
}
}

添加掩膜

根据之前谈到的掩膜,来和数据进行异或就可以得到最后的QR码了。

添加掩膜

painter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Painter {
// 添加掩膜
addMask() {
let maskPattern = new MaskPattern(this.size);
let mask = maskPattern.getMask(this.maskPatternReference);
for (let i = 0; i < this.size; i++) {
for (let j = 0; j < this.size; j++) {
if (!this._codeMap[i].flag[j]) {
this._codeMap[i][j] ^= mask[i][j];
}
}
}
}
}

掩模结果的评价

在依次用每一个掩模图形进行掩模操作之后,要通过对每一次如下情况的出现进行罚点记分,以便对每一个结果进行评估,分数越高,其结果越不可用。在下表中,N1到 N4为对不好的特征所罚分数的权重(N1=3,N2=3,N3=40,N4=10),i 为紧邻的颜色相同模块数大于 5的次数,k 为符号深色模块所占比率离 50%的差距,步长为 5%。虽然掩模操作仅对编码区域进行,不包括格式信息,但评价是对整个符号进行的。推荐使用掩模结果中罚分最低的掩模图形用于符号掩模。

掩模结果的记分

结语

在这之中还有一些细节没有介绍,因为那些细节需要花费大量篇幅去介绍 (容我当次费马) ,最后提供这篇文章的完整代码